github.com/oam-dev/kubevela@v1.9.11/pkg/workflow/providers/multicluster/deploy_test.go (about)

     1  /*
     2  Copyright 2022 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 multicluster
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"math/rand"
    23  	"sync"
    24  	"testing"
    25  	"time"
    26  
    27  	"github.com/stretchr/testify/require"
    28  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    29  	"k8s.io/apimachinery/pkg/runtime"
    30  
    31  	workflowv1alpha1 "github.com/kubevela/workflow/api/v1alpha1"
    32  	"github.com/kubevela/workflow/pkg/cue/model/value"
    33  
    34  	apicommon "github.com/oam-dev/kubevela/apis/core.oam.dev/common"
    35  
    36  	"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha1"
    37  	"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
    38  	"github.com/oam-dev/kubevela/pkg/oam"
    39  )
    40  
    41  func TestOverrideConfiguration(t *testing.T) {
    42  	testCases := map[string]struct {
    43  		Policies   []v1beta1.AppPolicy
    44  		Components []apicommon.ApplicationComponent
    45  		Outputs    []apicommon.ApplicationComponent
    46  		Error      string
    47  	}{
    48  		"invalid-policies": {
    49  			Policies: []v1beta1.AppPolicy{{
    50  				Name:       "override-policy",
    51  				Type:       "override",
    52  				Properties: &runtime.RawExtension{Raw: []byte(`bad value`)},
    53  			}},
    54  			Error: "failed to parse override policy",
    55  		},
    56  		"empty-policy": {
    57  			Policies: []v1beta1.AppPolicy{{
    58  				Name:       "override-policy",
    59  				Type:       "override",
    60  				Properties: nil,
    61  			}},
    62  			Error: "empty properties",
    63  		},
    64  		"normal": {
    65  			Policies: []v1beta1.AppPolicy{{
    66  				Name:       "override-policy",
    67  				Type:       "override",
    68  				Properties: &runtime.RawExtension{Raw: []byte(`{"components":[{"name":"comp","properties":{"x":5}}]}`)},
    69  			}},
    70  			Components: []apicommon.ApplicationComponent{{
    71  				Name:       "comp",
    72  				Traits:     []apicommon.ApplicationTrait{},
    73  				Properties: &runtime.RawExtension{Raw: []byte(`{"x":1}`)},
    74  			}},
    75  			Outputs: []apicommon.ApplicationComponent{{
    76  				Name:       "comp",
    77  				Traits:     []apicommon.ApplicationTrait{},
    78  				Properties: &runtime.RawExtension{Raw: []byte(`{"x":5}`)},
    79  			}},
    80  		},
    81  	}
    82  	for name, tt := range testCases {
    83  		t.Run(name, func(t *testing.T) {
    84  			r := require.New(t)
    85  			comps, err := overrideConfiguration(tt.Policies, tt.Components)
    86  			if tt.Error != "" {
    87  				r.NotNil(err)
    88  				r.Contains(err.Error(), tt.Error)
    89  			} else {
    90  				r.NoError(err)
    91  				r.Equal(tt.Outputs, comps)
    92  			}
    93  		})
    94  	}
    95  }
    96  
    97  func TestApplyComponentsDepends(t *testing.T) {
    98  	r := require.New(t)
    99  	const n, m = 50, 5
   100  	var components []apicommon.ApplicationComponent
   101  	var placements []v1alpha1.PlacementDecision
   102  	for i := 0; i < n*3; i++ {
   103  		comp := apicommon.ApplicationComponent{Name: fmt.Sprintf("comp-%d", i)}
   104  		if i%3 != 0 {
   105  			comp.DependsOn = append(comp.DependsOn, fmt.Sprintf("comp-%d", i-1))
   106  		}
   107  		if i%3 == 2 {
   108  			comp.DependsOn = append(comp.DependsOn, fmt.Sprintf("comp-%d", i-1))
   109  		}
   110  		components = append(components, comp)
   111  	}
   112  	for i := 0; i < m; i++ {
   113  		placements = append(placements, v1alpha1.PlacementDecision{Cluster: fmt.Sprintf("cluster-%d", i)})
   114  	}
   115  
   116  	applyMap := &sync.Map{}
   117  	apply := func(_ context.Context, comp apicommon.ApplicationComponent, patcher *value.Value, clusterName string, overrideNamespace string) (*unstructured.Unstructured, []*unstructured.Unstructured, bool, error) {
   118  		time.Sleep(time.Duration(rand.Intn(200)+25) * time.Millisecond)
   119  		applyMap.Store(fmt.Sprintf("%s/%s", clusterName, comp.Name), true)
   120  		return nil, nil, true, nil
   121  	}
   122  	healthCheck := func(_ context.Context, comp apicommon.ApplicationComponent, patcher *value.Value, clusterName string, overrideNamespace string) (bool, *unstructured.Unstructured, []*unstructured.Unstructured, error) {
   123  		_, found := applyMap.Load(fmt.Sprintf("%s/%s", clusterName, comp.Name))
   124  		return found, nil, nil, nil
   125  	}
   126  	parallelism := 10
   127  
   128  	countMap := func() int {
   129  		cnt := 0
   130  		applyMap.Range(func(key, value interface{}) bool {
   131  			cnt++
   132  			return true
   133  		})
   134  		return cnt
   135  	}
   136  	ctx := context.Background()
   137  	healthy, _, err := applyComponents(ctx, apply, healthCheck, components, placements, parallelism)
   138  	r.NoError(err)
   139  	r.False(healthy)
   140  	r.Equal(n*m, countMap())
   141  
   142  	healthy, _, err = applyComponents(ctx, apply, healthCheck, components, placements, parallelism)
   143  	r.NoError(err)
   144  	r.False(healthy)
   145  	r.Equal(2*n*m, countMap())
   146  
   147  	healthy, _, err = applyComponents(ctx, apply, healthCheck, components, placements, parallelism)
   148  	r.NoError(err)
   149  	r.True(healthy)
   150  	r.Equal(3*n*m, countMap())
   151  }
   152  
   153  func TestApplyComponentsIO(t *testing.T) {
   154  	r := require.New(t)
   155  
   156  	var (
   157  		parallelism = 10
   158  		applyMap    = new(sync.Map)
   159  		ctx         = context.Background()
   160  	)
   161  	apply := func(_ context.Context, comp apicommon.ApplicationComponent, patcher *value.Value, clusterName string, overrideNamespace string) (*unstructured.Unstructured, []*unstructured.Unstructured, bool, error) {
   162  		time.Sleep(time.Duration(rand.Intn(200)+25) * time.Millisecond)
   163  		applyMap.Store(fmt.Sprintf("%s/%s", clusterName, comp.Name), true)
   164  		return nil, nil, true, nil
   165  	}
   166  	healthCheck := func(_ context.Context, comp apicommon.ApplicationComponent, patcher *value.Value, clusterName string, overrideNamespace string) (bool, *unstructured.Unstructured, []*unstructured.Unstructured, error) {
   167  		_, found := applyMap.Load(fmt.Sprintf("%s/%s", clusterName, comp.Name))
   168  		return found, &unstructured.Unstructured{Object: map[string]interface{}{
   169  				"spec": map[string]interface{}{
   170  					"path": fmt.Sprintf("%s/%s", clusterName, comp.Name),
   171  				},
   172  			}}, []*unstructured.Unstructured{
   173  				{
   174  					Object: map[string]interface{}{
   175  						"metadata": map[string]interface{}{
   176  							"labels": map[string]interface{}{
   177  								oam.TraitResource: "obj",
   178  							},
   179  						},
   180  						"spec": map[string]interface{}{
   181  							"path": fmt.Sprintf("%s/%s", clusterName, comp.Name),
   182  						},
   183  					},
   184  				},
   185  			}, nil
   186  	}
   187  
   188  	resetStore := func() {
   189  		applyMap = &sync.Map{}
   190  	}
   191  	countMap := func() int {
   192  		cnt := 0
   193  		applyMap.Range(func(key, value interface{}) bool {
   194  			cnt++
   195  			return true
   196  		})
   197  		return cnt
   198  	}
   199  
   200  	t.Run("apply components with io successfully", func(t *testing.T) {
   201  		resetStore()
   202  		const n, m = 10, 5
   203  		var components []apicommon.ApplicationComponent
   204  		var placements []v1alpha1.PlacementDecision
   205  		for i := 0; i < n; i++ {
   206  			comp := apicommon.ApplicationComponent{
   207  				Name:       fmt.Sprintf("comp-%d", i),
   208  				Properties: &runtime.RawExtension{Raw: []byte(fmt.Sprintf(`{"placeholder":%d}`, i))},
   209  			}
   210  			if i != 0 {
   211  				comp.Inputs = workflowv1alpha1.StepInputs{
   212  					{
   213  						ParameterKey: "input_slot_1",
   214  						From:         fmt.Sprintf("var-output-%d", i-1),
   215  					},
   216  					{
   217  						ParameterKey: "input_slot_2",
   218  						From:         fmt.Sprintf("var-outputs-%d", i-1),
   219  					},
   220  				}
   221  			}
   222  			if i != n-1 {
   223  				comp.Outputs = workflowv1alpha1.StepOutputs{
   224  					{
   225  						ValueFrom: "output.spec.path",
   226  						Name:      fmt.Sprintf("var-output-%d", i),
   227  					},
   228  					{
   229  						ValueFrom: "outputs.obj.spec.path",
   230  						Name:      fmt.Sprintf("var-outputs-%d", i),
   231  					},
   232  				}
   233  			}
   234  			components = append(components, comp)
   235  		}
   236  		for i := 0; i < m; i++ {
   237  			placements = append(placements, v1alpha1.PlacementDecision{Cluster: fmt.Sprintf("cluster-%d", i)})
   238  		}
   239  
   240  		for i := 0; i < n; i++ {
   241  			healthy, _, err := applyComponents(ctx, apply, healthCheck, components, placements, parallelism)
   242  			r.NoError(err)
   243  			r.Equal((i+1)*m, countMap())
   244  			if i == n-1 {
   245  				r.True(healthy)
   246  			} else {
   247  				r.False(healthy)
   248  			}
   249  		}
   250  	})
   251  
   252  	t.Run("apply components with io failed", func(t *testing.T) {
   253  		resetStore()
   254  		components := []apicommon.ApplicationComponent{
   255  			{
   256  				Name: "comp-0",
   257  				Outputs: workflowv1alpha1.StepOutputs{
   258  					{
   259  						ValueFrom: "output.spec.error_path",
   260  						Name:      "var1",
   261  					},
   262  				},
   263  			},
   264  			{
   265  				Name: "comp-1",
   266  				Inputs: workflowv1alpha1.StepInputs{
   267  					{
   268  						ParameterKey: "input_slot_1",
   269  						From:         "var1",
   270  					},
   271  				},
   272  			},
   273  		}
   274  		placements := []v1alpha1.PlacementDecision{
   275  			{Cluster: "cluster-0"},
   276  		}
   277  		healthy, _, err := applyComponents(ctx, apply, healthCheck, components, placements, parallelism)
   278  		r.NoError(err)
   279  		r.False(healthy)
   280  		healthy, _, err = applyComponents(ctx, apply, healthCheck, components, placements, parallelism)
   281  		r.ErrorContains(err, "failed to lookup value")
   282  		r.False(healthy)
   283  	})
   284  
   285  	t.Run("apply components with io and replication", func(t *testing.T) {
   286  		// comp-0 ---> comp1-beijing  --> comp2-beijing
   287  		// 		   |-> comp1-shanghai --> comp2-shanghai
   288  		resetStore()
   289  		storeKey := func(clusterName string, comp apicommon.ApplicationComponent) string {
   290  			return fmt.Sprintf("%s/%s/%s", clusterName, comp.Name, comp.ReplicaKey)
   291  		}
   292  		type applyResult struct {
   293  			output  *unstructured.Unstructured
   294  			outputs []*unstructured.Unstructured
   295  		}
   296  		apply := func(_ context.Context, comp apicommon.ApplicationComponent, patcher *value.Value, clusterName string, overrideNamespace string) (*unstructured.Unstructured, []*unstructured.Unstructured, bool, error) {
   297  			time.Sleep(time.Duration(rand.Intn(200)+25) * time.Millisecond)
   298  			key := storeKey(clusterName, comp)
   299  			result := applyResult{
   300  				output: &unstructured.Unstructured{Object: map[string]interface{}{
   301  					"spec": map[string]interface{}{
   302  						"path":        key,
   303  						"anotherPath": key,
   304  					},
   305  				}}, outputs: []*unstructured.Unstructured{
   306  					{
   307  						Object: map[string]interface{}{
   308  							"metadata": map[string]interface{}{
   309  								"labels": map[string]interface{}{
   310  									oam.TraitResource: "obj",
   311  								},
   312  							},
   313  							"spec": map[string]interface{}{
   314  								"path": key,
   315  							},
   316  						},
   317  					},
   318  				},
   319  			}
   320  			applyMap.Store(storeKey(clusterName, comp), result)
   321  			return nil, nil, true, nil
   322  		}
   323  		healthCheck := func(_ context.Context, comp apicommon.ApplicationComponent, patcher *value.Value, clusterName string, overrideNamespace string) (bool, *unstructured.Unstructured, []*unstructured.Unstructured, error) {
   324  			key := storeKey(clusterName, comp)
   325  			r, found := applyMap.Load(key)
   326  			result, _ := r.(applyResult)
   327  			return found, result.output, result.outputs, nil
   328  		}
   329  
   330  		inputSlot := "input_slot"
   331  		components := []apicommon.ApplicationComponent{
   332  			{
   333  				Name: "comp-0",
   334  				Outputs: workflowv1alpha1.StepOutputs{
   335  					{
   336  						ValueFrom: "output.spec.path",
   337  						Name:      "var1",
   338  					},
   339  				},
   340  			},
   341  			{
   342  				Name: "comp-1",
   343  				Inputs: workflowv1alpha1.StepInputs{
   344  					{
   345  						ParameterKey: inputSlot,
   346  						From:         "var1",
   347  					},
   348  				},
   349  				Outputs: workflowv1alpha1.StepOutputs{
   350  					{
   351  						ValueFrom: "output.spec.anotherPath",
   352  						Name:      "var2",
   353  					},
   354  				},
   355  				ReplicaKey: "beijing",
   356  			},
   357  			{
   358  				Name: "comp-1",
   359  				Inputs: workflowv1alpha1.StepInputs{
   360  					{
   361  						ParameterKey: inputSlot,
   362  						From:         "var1",
   363  					},
   364  				},
   365  				Outputs: workflowv1alpha1.StepOutputs{
   366  					{
   367  						ValueFrom: "output.spec.anotherPath",
   368  						Name:      "var2",
   369  					},
   370  				},
   371  				ReplicaKey: "shanghai",
   372  			},
   373  			{
   374  				Name: "comp-2",
   375  				Inputs: workflowv1alpha1.StepInputs{
   376  					{
   377  						ParameterKey: inputSlot,
   378  						From:         "var2",
   379  					},
   380  				},
   381  				ReplicaKey: "beijing",
   382  			},
   383  			{
   384  				Name: "comp-2",
   385  				Inputs: workflowv1alpha1.StepInputs{
   386  					{
   387  						ParameterKey: inputSlot,
   388  						From:         "var2",
   389  					},
   390  				},
   391  				ReplicaKey: "shanghai",
   392  			},
   393  		}
   394  		placements := []v1alpha1.PlacementDecision{
   395  			{Cluster: "cluster-0"},
   396  		}
   397  		healthy, _, err := applyComponents(ctx, apply, healthCheck, components, placements, parallelism)
   398  		r.NoError(err)
   399  		r.False(healthy)
   400  
   401  		healthy, _, err = applyComponents(ctx, apply, healthCheck, components, placements, parallelism)
   402  		r.NoError(err)
   403  		r.False(healthy)
   404  
   405  		healthy, _, err = applyComponents(ctx, apply, healthCheck, components, placements, parallelism)
   406  		r.NoError(err)
   407  		r.True(healthy)
   408  
   409  	})
   410  }