github.com/oam-dev/kubevela@v1.9.11/pkg/workflow/providers/multicluster/multicluster_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 multicluster 18 19 import ( 20 "context" 21 "encoding/json" 22 "testing" 23 24 "github.com/stretchr/testify/require" 25 corev1 "k8s.io/api/core/v1" 26 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 27 "k8s.io/apimachinery/pkg/runtime" 28 "sigs.k8s.io/controller-runtime/pkg/client/fake" 29 30 "github.com/kubevela/workflow/pkg/cue/model/value" 31 "github.com/kubevela/workflow/pkg/mock" 32 clusterv1alpha1 "github.com/oam-dev/cluster-gateway/pkg/apis/cluster/v1alpha1" 33 clustercommon "github.com/oam-dev/cluster-gateway/pkg/common" 34 35 apicommon "github.com/oam-dev/kubevela/apis/core.oam.dev/common" 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/apis/types" 39 "github.com/oam-dev/kubevela/pkg/multicluster" 40 "github.com/oam-dev/kubevela/pkg/utils/common" 41 ) 42 43 func TestMakePlacementDecisions(t *testing.T) { 44 multicluster.ClusterGatewaySecretNamespace = types.DefaultKubeVelaNS 45 testCases := []struct { 46 InputVal map[string]interface{} 47 OldCluster string 48 OldNamespace string 49 ExpectError string 50 ExpectCluster string 51 ExpectNamespace string 52 PreAddCluster string 53 }{{ 54 InputVal: map[string]interface{}{}, 55 ExpectError: "var(path=inputs.policyName) not exist", 56 }, { 57 InputVal: map[string]interface{}{ 58 "policyName": "example-policy", 59 }, 60 ExpectError: "var(path=inputs.envName) not exist", 61 }, { 62 InputVal: map[string]interface{}{ 63 "policyName": "example-policy", 64 "envName": "example-env", 65 }, 66 ExpectError: "var(path=inputs.placement) not exist", 67 }, { 68 InputVal: map[string]interface{}{ 69 "policyName": "example-policy", 70 "envName": "example-env", 71 "placement": "example-placement", 72 }, 73 ExpectError: "failed to parse placement while making placement decision", 74 }, { 75 InputVal: map[string]interface{}{ 76 "policyName": "example-policy", 77 "envName": "example-env", 78 "placement": map[string]interface{}{ 79 "namespaceSelector": map[string]interface{}{ 80 "labels": map[string]string{"key": "value"}, 81 }, 82 }, 83 }, 84 ExpectError: "namespace selector in cluster-gateway does not support label selector for now", 85 }, { 86 InputVal: map[string]interface{}{ 87 "policyName": "example-policy", 88 "envName": "example-env", 89 "placement": map[string]interface{}{ 90 "clusterSelector": map[string]interface{}{ 91 "labels": map[string]string{"key": "value"}, 92 }, 93 }, 94 }, 95 ExpectError: "cluster selector does not support label selector for now", 96 }, { 97 InputVal: map[string]interface{}{ 98 "policyName": "example-policy", 99 "envName": "example-env", 100 "placement": map[string]interface{}{}, 101 }, 102 ExpectError: "", 103 ExpectCluster: "local", 104 ExpectNamespace: "", 105 }, { 106 InputVal: map[string]interface{}{ 107 "policyName": "example-policy", 108 "envName": "example-env", 109 "placement": map[string]interface{}{ 110 "clusterSelector": map[string]interface{}{ 111 "name": "example-cluster", 112 }, 113 "namespaceSelector": map[string]interface{}{ 114 "name": "example-namespace", 115 }, 116 }, 117 }, 118 ExpectError: "failed to get cluster", 119 }, { 120 InputVal: map[string]interface{}{ 121 "policyName": "example-policy", 122 "envName": "example-env", 123 "placement": map[string]interface{}{ 124 "clusterSelector": map[string]interface{}{ 125 "name": "example-cluster", 126 }, 127 "namespaceSelector": map[string]interface{}{ 128 "name": "example-namespace", 129 }, 130 }, 131 }, 132 ExpectError: "", 133 ExpectCluster: "example-cluster", 134 ExpectNamespace: "example-namespace", 135 PreAddCluster: "example-cluster", 136 }, { 137 InputVal: map[string]interface{}{ 138 "policyName": "example-policy", 139 "envName": "example-env", 140 "placement": map[string]interface{}{ 141 "clusterSelector": map[string]interface{}{ 142 "name": "example-cluster", 143 }, 144 "namespaceSelector": map[string]interface{}{ 145 "name": "example-namespace", 146 }, 147 }, 148 }, 149 OldCluster: "old-cluster", 150 OldNamespace: "old-namespace", 151 ExpectError: "", 152 ExpectCluster: "example-cluster", 153 ExpectNamespace: "example-namespace", 154 PreAddCluster: "example-cluster", 155 }, { 156 InputVal: map[string]interface{}{ 157 "policyName": "example-policy", 158 "envName": "example-env", 159 "placement": map[string]interface{}{ 160 "clusterSelector": map[string]interface{}{ 161 "name": "example-cluster", 162 }, 163 "namespaceSelector": map[string]interface{}{ 164 "name": "example-namespace", 165 }, 166 }, 167 }, 168 ExpectError: "", 169 ExpectCluster: "example-cluster", 170 ExpectNamespace: "example-namespace", 171 PreAddCluster: "example-cluster", 172 }} 173 174 r := require.New(t) 175 for _, testCase := range testCases { 176 cli := fake.NewClientBuilder().WithScheme(common.Scheme).Build() 177 app := &v1beta1.Application{} 178 p := &provider{ 179 Client: cli, 180 app: app, 181 } 182 act := &mock.Action{} 183 v, err := value.NewValue("", nil, "") 184 r.NoError(err) 185 r.NoError(v.FillObject(testCase.InputVal, "inputs")) 186 if testCase.PreAddCluster != "" { 187 r.NoError(cli.Create(context.Background(), &corev1.Secret{ 188 ObjectMeta: metav1.ObjectMeta{ 189 Namespace: multicluster.ClusterGatewaySecretNamespace, 190 Name: testCase.PreAddCluster, 191 Labels: map[string]string{clustercommon.LabelKeyClusterCredentialType: string(clusterv1alpha1.CredentialTypeX509Certificate)}, 192 }, 193 })) 194 } 195 if testCase.OldNamespace != "" || testCase.OldCluster != "" { 196 pd := v1alpha1.PlacementDecision{ 197 Cluster: testCase.OldNamespace, 198 Namespace: testCase.OldCluster, 199 } 200 bs, err := json.Marshal(&v1alpha1.EnvBindingStatus{ 201 Envs: []v1alpha1.EnvStatus{{ 202 Env: "example-env", 203 Placements: []v1alpha1.PlacementDecision{pd}, 204 }}, 205 }) 206 r.NoError(err) 207 app.Status.PolicyStatus = []apicommon.PolicyStatus{{ 208 Name: "example-policy", 209 Type: v1alpha1.EnvBindingPolicyType, 210 Status: &runtime.RawExtension{Raw: bs}, 211 }} 212 } 213 err = p.MakePlacementDecisions(nil, nil, v, act) 214 if testCase.ExpectError == "" { 215 r.NoError(err) 216 } else { 217 r.Contains(err.Error(), testCase.ExpectError) 218 continue 219 } 220 outputs, err := v.LookupValue("outputs") 221 r.NoError(err) 222 md := map[string][]v1alpha1.PlacementDecision{} 223 r.NoError(outputs.UnmarshalTo(&md)) 224 r.Equal(1, len(md["decisions"])) 225 r.Equal(testCase.ExpectCluster, md["decisions"][0].Cluster) 226 r.Equal(testCase.ExpectNamespace, md["decisions"][0].Namespace) 227 r.Equal(1, len(app.Status.PolicyStatus)) 228 r.Equal(testCase.InputVal["policyName"], app.Status.PolicyStatus[0].Name) 229 r.Equal(v1alpha1.EnvBindingPolicyType, app.Status.PolicyStatus[0].Type) 230 status := &v1alpha1.EnvBindingStatus{} 231 r.NoError(json.Unmarshal(app.Status.PolicyStatus[0].Status.Raw, status)) 232 r.Equal(1, len(status.Envs)) 233 r.Equal(testCase.InputVal["envName"], status.Envs[0].Env) 234 r.Equal(1, len(status.Envs[0].Placements)) 235 r.Equal(testCase.ExpectNamespace, status.Envs[0].Placements[0].Namespace) 236 r.Equal(testCase.ExpectCluster, status.Envs[0].Placements[0].Cluster) 237 } 238 } 239 240 func TestPatchApplication(t *testing.T) { 241 baseApp := &v1beta1.Application{Spec: v1beta1.ApplicationSpec{ 242 Components: []apicommon.ApplicationComponent{{ 243 Name: "comp-1", 244 Type: "webservice", 245 Properties: &runtime.RawExtension{Raw: []byte(`{"image":"base"}`)}, 246 }, { 247 Name: "comp-3", 248 Type: "webservice", 249 Properties: &runtime.RawExtension{Raw: []byte(`{"image":"ext"}`)}, 250 Traits: []apicommon.ApplicationTrait{{ 251 Type: "scaler", 252 Properties: &runtime.RawExtension{Raw: []byte(`{"replicas":3}`)}, 253 }, { 254 Type: "env", 255 Properties: &runtime.RawExtension{Raw: []byte(`{"env":{"key":"value"}}`)}, 256 }, { 257 Type: "labels", 258 Properties: &runtime.RawExtension{Raw: []byte(`{"lKey":"lVal"}`)}, 259 }}, 260 }}, 261 }} 262 testCases := []struct { 263 InputVal map[string]interface{} 264 ExpectError string 265 ExpectComponents []apicommon.ApplicationComponent 266 }{{ 267 InputVal: map[string]interface{}{}, 268 ExpectError: "var(path=inputs.envName) not exist", 269 }, { 270 InputVal: map[string]interface{}{ 271 "envName": "example-env", 272 }, 273 ExpectComponents: baseApp.Spec.Components, 274 }, { 275 InputVal: map[string]interface{}{ 276 "envName": "example-env", 277 "patch": "bad patch", 278 }, 279 ExpectError: "failed to unmarshal patch for env", 280 }, { 281 InputVal: map[string]interface{}{ 282 "envName": "example-env", 283 "selector": "bad selector", 284 }, 285 ExpectError: "failed to unmarshal selector for env", 286 }, { 287 InputVal: map[string]interface{}{ 288 "envName": "example-env", 289 "patch": map[string]interface{}{ 290 "components": []map[string]interface{}{{ 291 "name": "comp-0", 292 "type": "webservice", 293 }, { 294 "name": "comp-1", 295 "type": "worker", 296 "properties": map[string]interface{}{ 297 "image": "patch", 298 "port": 8080, 299 }, 300 }, { 301 "name": "comp-3", 302 "type": "webservice", 303 "properties": map[string]interface{}{ 304 "image": "patch", 305 "port": 8090, 306 }, 307 "traits": []map[string]interface{}{{ 308 "type": "scaler", 309 "properties": map[string]interface{}{"replicas": 5}, 310 }, { 311 "type": "env", 312 "properties": map[string]interface{}{"env": map[string]string{"Key": "Value"}}, 313 }, { 314 "type": "annotations", 315 "properties": map[string]interface{}{"aKey": "aVal"}}, 316 }, 317 }, { 318 "name": "comp-4", 319 "type": "webservice", 320 }}, 321 }, 322 "selector": map[string]interface{}{ 323 "components": []string{"comp-2", "comp-1", "comp-3", "comp-0"}, 324 }, 325 }, 326 ExpectComponents: []apicommon.ApplicationComponent{{ 327 Name: "comp-1", 328 Type: "worker", 329 Properties: &runtime.RawExtension{Raw: []byte(`{"image":"patch","port":8080}`)}, 330 }, { 331 Name: "comp-3", 332 Type: "webservice", 333 Properties: &runtime.RawExtension{Raw: []byte(`{"image":"patch","port":8090}`)}, 334 Traits: []apicommon.ApplicationTrait{{ 335 Type: "scaler", 336 Properties: &runtime.RawExtension{Raw: []byte(`{"replicas":5}`)}, 337 }, { 338 Type: "env", 339 Properties: &runtime.RawExtension{Raw: []byte(`{"env":{"Key":"Value","key":"value"}}`)}, 340 }, { 341 Type: "labels", 342 Properties: &runtime.RawExtension{Raw: []byte(`{"lKey":"lVal"}`)}, 343 }, { 344 Type: "annotations", 345 Properties: &runtime.RawExtension{Raw: []byte(`{"aKey":"aVal"}`)}, 346 }}, 347 }, { 348 Name: "comp-0", 349 Type: "webservice", 350 }}, 351 }} 352 r := require.New(t) 353 for _, testCase := range testCases { 354 cli := fake.NewClientBuilder().WithScheme(common.Scheme).Build() 355 p := &provider{ 356 Client: cli, 357 app: baseApp, 358 } 359 act := &mock.Action{} 360 v, err := value.NewValue("", nil, "") 361 r.NoError(err) 362 r.NoError(v.FillObject(testCase.InputVal, "inputs")) 363 err = p.PatchApplication(nil, nil, v, act) 364 if testCase.ExpectError == "" { 365 r.NoError(err) 366 } else { 367 r.Contains(err.Error(), testCase.ExpectError) 368 continue 369 } 370 outputs, err := v.LookupValue("outputs") 371 r.NoError(err) 372 patchApp := &v1beta1.Application{} 373 r.NoError(outputs.UnmarshalTo(patchApp)) 374 r.Equal(len(testCase.ExpectComponents), len(patchApp.Spec.Components)) 375 for idx, comp := range testCase.ExpectComponents { 376 _comp := patchApp.Spec.Components[idx] 377 r.Equal(comp.Name, _comp.Name) 378 r.Equal(comp.Type, _comp.Type) 379 if comp.Properties == nil { 380 r.Equal(comp.Properties, _comp.Properties) 381 } else { 382 r.Equal(string(comp.Properties.Raw), string(_comp.Properties.Raw)) 383 } 384 r.Equal(len(comp.Traits), len(_comp.Traits)) 385 for _idx, trait := range comp.Traits { 386 _trait := _comp.Traits[_idx] 387 r.Equal(trait.Type, _trait.Type) 388 if trait.Properties == nil { 389 r.Equal(trait.Properties, _trait.Properties) 390 } else { 391 r.Equal(string(trait.Properties.Raw), string(_trait.Properties.Raw)) 392 } 393 } 394 } 395 } 396 } 397 398 func TestListClusters(t *testing.T) { 399 multicluster.ClusterGatewaySecretNamespace = types.DefaultKubeVelaNS 400 r := require.New(t) 401 cli := fake.NewClientBuilder().WithScheme(common.Scheme).Build() 402 clusterNames := []string{"cluster-a", "cluster-b"} 403 for _, secretName := range clusterNames { 404 secret := &corev1.Secret{} 405 secret.Name = secretName 406 secret.Namespace = multicluster.ClusterGatewaySecretNamespace 407 secret.Labels = map[string]string{clustercommon.LabelKeyClusterCredentialType: string(clusterv1alpha1.CredentialTypeX509Certificate)} 408 r.NoError(cli.Create(context.Background(), secret)) 409 } 410 app := &v1beta1.Application{} 411 p := &provider{ 412 Client: cli, 413 app: app, 414 } 415 act := &mock.Action{} 416 v, err := value.NewValue("", nil, "") 417 r.NoError(err) 418 r.NoError(p.ListClusters(nil, nil, v, act)) 419 outputs, err := v.LookupValue("outputs") 420 r.NoError(err) 421 obj := struct { 422 Clusters []string `json:"clusters"` 423 }{} 424 r.NoError(outputs.UnmarshalTo(&obj)) 425 r.Equal(clusterNames, obj.Clusters) 426 }