github.com/oam-dev/kubevela@v1.9.11/pkg/controller/utils/capability_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 18 package utils 19 20 import ( 21 "context" 22 "fmt" 23 "os" 24 "strings" 25 "testing" 26 27 "github.com/crossplane/crossplane-runtime/pkg/test" 28 "github.com/google/go-cmp/cmp" 29 "github.com/stretchr/testify/assert" 30 "sigs.k8s.io/controller-runtime/pkg/client" 31 "sigs.k8s.io/controller-runtime/pkg/client/fake" 32 33 "github.com/oam-dev/kubevela/apis/core.oam.dev/common" 34 "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" 35 "github.com/oam-dev/kubevela/pkg/appfile" 36 "github.com/oam-dev/kubevela/pkg/oam/util" 37 38 gitssh "github.com/go-git/go-git/v5/plumbing/transport/ssh" 39 "golang.org/x/crypto/ssh/testdata" 40 corev1 "k8s.io/api/core/v1" 41 v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 42 ) 43 44 const TestDir = "testdata/definition" 45 46 func TestGetOpenAPISchema(t *testing.T) { 47 type want struct { 48 data string 49 err error 50 } 51 cases := map[string]struct { 52 reason string 53 name string 54 data string 55 want want 56 }{ 57 "parameter in cue is a structure type,": { 58 reason: "Prepare a normal parameter cue file", 59 name: "workload1", 60 data: ` 61 project: { 62 name: string 63 } 64 65 parameter: { 66 min: int 67 } 68 `, 69 want: want{data: "{\"properties\":{\"min\":{\"title\":\"min\",\"type\":\"integer\"}},\"required\":[\"min\"],\"type\":\"object\"}", err: nil}, 70 }, 71 "parameter in cue is a dict type,": { 72 reason: "Prepare a normal parameter cue file", 73 name: "workload2", 74 data: ` 75 annotations: { 76 for k, v in parameter { 77 "\(k)": v 78 } 79 } 80 81 parameter: [string]: string 82 `, 83 want: want{data: `{"additionalProperties":{"type":"string"},"type":"object"}`, err: nil}, 84 }, 85 "parameter in cue is a string type,": { 86 reason: "Prepare a normal parameter cue file", 87 name: "workload3", 88 data: ` 89 annotations: { 90 "test":parameter 91 } 92 93 parameter:string 94 `, 95 want: want{data: "{\"type\":\"string\"}", err: nil}, 96 }, 97 "parameter in cue is a list type,": { 98 reason: "Prepare a list parameter cue file", 99 name: "workload4", 100 data: ` 101 annotations: { 102 "test":parameter 103 } 104 105 parameter:[...string] 106 `, 107 want: want{data: "{\"items\":{\"type\":\"string\"},\"type\":\"array\"}", err: nil}, 108 }, 109 "parameter in cue is an int type,": { 110 reason: "Prepare an int parameter cue file", 111 name: "workload5", 112 data: ` 113 annotations: { 114 "test":parameter 115 } 116 117 parameter: int 118 `, 119 want: want{data: "{\"type\":\"integer\"}", err: nil}, 120 }, 121 "parameter in cue is a float type,": { 122 reason: "Prepare a float parameter cue file", 123 name: "workload6", 124 data: ` 125 annotations: { 126 "test":parameter 127 } 128 129 parameter: float 130 `, 131 want: want{data: "{\"type\":\"number\"}", err: nil}, 132 }, 133 "parameter in cue is a bool type,": { 134 reason: "Prepare a bool parameter cue file", 135 name: "workload7", 136 data: ` 137 annotations: { 138 "test":parameter 139 } 140 141 parameter: bool 142 `, 143 want: want{data: "{\"type\":\"boolean\"}", err: nil}, 144 }, 145 "cue doesn't contain parameter section": { 146 reason: "Prepare a cue file which doesn't contain `parameter` section", 147 name: "invalidWorkload", 148 data: ` 149 project: { 150 name: string 151 } 152 153 noParameter: { 154 min: int 155 } 156 `, 157 want: want{data: "{\"type\":\"object\"}", err: nil}, 158 }, 159 "cue doesn't parse other sections except parameter": { 160 reason: "Prepare a cue file which contains `context.appName` field", 161 name: "withContextField", 162 data: ` 163 patch: { 164 spec: template: metadata: labels: { 165 for k, v in parameter { 166 "\(k)": v 167 } 168 } 169 spec: template: metadata: annotations: { 170 "route-name.oam.dev": #routeName 171 } 172 } 173 174 #routeName: "\(context.appName)-\(context.name)" 175 176 parameter: [string]: string 177 `, 178 want: want{data: `{"additionalProperties":{"type":"string"},"type":"object"}`, err: nil}, 179 }, 180 } 181 182 for name, tc := range cases { 183 t.Run(name, func(t *testing.T) { 184 schematic := &common.Schematic{ 185 CUE: &common.CUE{ 186 Template: tc.data, 187 }, 188 } 189 capability, _ := appfile.ConvertTemplateJSON2Object(tc.name, nil, schematic) 190 schema, err := getOpenAPISchema(capability) 191 if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { 192 t.Errorf("\n%s\ngetOpenAPISchema(...): -want error, +got error:\n%s", tc.reason, diff) 193 } 194 if tc.want.err == nil { 195 assert.Equal(t, string(schema), tc.want.data) 196 } 197 }) 198 } 199 } 200 201 func TestNewCapabilityComponentDef(t *testing.T) { 202 terraform := &common.Terraform{ 203 Configuration: "test", 204 } 205 componentDefinition := &v1beta1.ComponentDefinition{ 206 Spec: v1beta1.ComponentDefinitionSpec{ 207 Schematic: &common.Schematic{ 208 Terraform: terraform, 209 }, 210 }, 211 } 212 def := NewCapabilityComponentDef(componentDefinition) 213 assert.Equal(t, def.WorkloadType, util.TerraformDef) 214 assert.Equal(t, def.Terraform, terraform) 215 } 216 217 func TestGetOpenAPISchemaFromTerraformComponentDefinition(t *testing.T) { 218 type want struct { 219 subStr string 220 err error 221 } 222 cases := map[string]struct { 223 configuration string 224 want want 225 }{ 226 "valid": { 227 configuration: ` 228 module "rds" { 229 source = "terraform-alicloud-modules/rds/alicloud" 230 engine = "MySQL" 231 engine_version = "8.0" 232 instance_type = "rds.mysql.c1.large" 233 instance_storage = "20" 234 instance_name = var.instance_name 235 account_name = var.account_name 236 password = var.password 237 } 238 239 output "DB_NAME" { 240 value = module.rds.this_db_instance_name 241 } 242 output "DB_USER" { 243 value = module.rds.this_db_database_account 244 } 245 output "DB_PORT" { 246 value = module.rds.this_db_instance_port 247 } 248 output "DB_HOST" { 249 value = module.rds.this_db_instance_connection_string 250 } 251 output "DB_PASSWORD" { 252 value = module.rds.this_db_instance_port 253 } 254 255 variable "instance_name" { 256 description = "RDS instance name" 257 type = string 258 default = "poc" 259 } 260 261 variable "account_name" { 262 description = "RDS instance user account name" 263 type = "string" 264 default = "oam" 265 } 266 267 variable "password" { 268 description = "RDS instance account password" 269 type = "string" 270 default = "xxx" 271 } 272 273 variable "intVar" { 274 type = "number" 275 }`, 276 want: want{ 277 subStr: `"required":["intVar"]`, 278 err: nil, 279 }, 280 }, 281 "null type variable": { 282 configuration: ` 283 variable "name" { 284 default = "abc" 285 }`, 286 want: want{ 287 subStr: "abc", 288 err: nil, 289 }, 290 }, 291 "null type variable, while default value is a slice": { 292 configuration: ` 293 variable "name" { 294 default = [123] 295 }`, 296 want: want{ 297 subStr: "123", 298 err: nil, 299 }, 300 }, 301 "null type variable, while default value is a map": { 302 configuration: ` 303 variable "name" { 304 default = {a = 1} 305 }`, 306 want: want{ 307 subStr: "a", 308 err: nil, 309 }, 310 }, 311 "null type variable, while default value is number": { 312 configuration: ` 313 variable "name" { 314 default = 123 315 }`, 316 want: want{ 317 subStr: "123", 318 err: nil, 319 }, 320 }, 321 "complicated list variable": { 322 configuration: ` 323 variable "aaa" { 324 type = list(object({ 325 type = string 326 sourceArn = string 327 config = string 328 })) 329 default = [] 330 }`, 331 want: want{ 332 subStr: "aaa", 333 err: nil, 334 }, 335 }, 336 "complicated map variable": { 337 configuration: ` 338 variable "bbb" { 339 type = map({ 340 type = string 341 sourceArn = string 342 config = string 343 }) 344 default = [] 345 }`, 346 want: want{ 347 subStr: "bbb", 348 err: nil, 349 }, 350 }, 351 "not supported complicated variable": { 352 configuration: ` 353 variable "bbb" { 354 type = xxxxx(string) 355 }`, 356 want: want{ 357 subStr: "", 358 err: fmt.Errorf("the type `%s` of variable %s is NOT supported", "xxxxx(string)", "bbb"), 359 }, 360 }, 361 "any type, slice default": { 362 configuration: ` 363 variable "bbb" { 364 type = any 365 default = [] 366 }`, 367 want: want{ 368 subStr: "bbb", 369 err: nil, 370 }, 371 }, 372 "any type, map default": { 373 configuration: ` 374 variable "bbb" { 375 type = any 376 default = {} 377 }`, 378 want: want{ 379 subStr: "bbb", 380 err: nil, 381 }, 382 }, 383 } 384 385 for name, tc := range cases { 386 t.Run(name, func(t *testing.T) { 387 schema, err := GetOpenAPISchemaFromTerraformComponentDefinition(tc.configuration) 388 if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { 389 t.Errorf("\n%s\nGetOpenAPISchemaFromTerraformComponentDefinition(...): -want error, +got error:\n%s", name, diff) 390 } 391 if tc.want.err == nil { 392 data := string(schema) 393 assert.Equal(t, strings.Contains(data, tc.want.subStr), true) 394 } 395 }) 396 } 397 } 398 399 func TestGetGitSSHPublicKey(t *testing.T) { 400 sshAuth := make(map[string][]byte) 401 sshAuth[corev1.SSHAuthPrivateKey] = testdata.PEMBytes["rsa"] 402 sshAuth[GitCredsKnownHosts] = []byte(`github.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa 403 +PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7 404 VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKr 405 TJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==`) 406 407 pubKey, err := gitssh.NewPublicKeys("git", sshAuth[corev1.SSHAuthPrivateKey], "") 408 assert.NoError(t, err) 409 410 k8sClient := fake.NewClientBuilder().Build() 411 ctx := context.Background() 412 413 secret := corev1.Secret{ 414 ObjectMeta: v1.ObjectMeta{ 415 Name: "git-ssh-auth", 416 Namespace: "default", 417 }, 418 Data: sshAuth, 419 Type: corev1.SecretTypeSSHAuth, 420 } 421 err = k8sClient.Create(ctx, &secret) 422 assert.NoError(t, err) 423 424 secret = corev1.Secret{ 425 ObjectMeta: v1.ObjectMeta{ 426 Name: "git-ssh-auth-no-ssh-privatekey", 427 Namespace: "default", 428 }, 429 Data: map[string][]byte{ 430 GitCredsKnownHosts: sshAuth[GitCredsKnownHosts], 431 }, 432 } 433 err = k8sClient.Create(ctx, &secret) 434 assert.NoError(t, err) 435 436 secret = corev1.Secret{ 437 ObjectMeta: v1.ObjectMeta{ 438 Name: "git-ssh-auth-no-known_hosts", 439 Namespace: "default", 440 }, 441 Data: map[string][]byte{ 442 corev1.SSHAuthPrivateKey: sshAuth[corev1.SSHAuthPrivateKey], 443 }, 444 Type: corev1.SecretTypeSSHAuth, 445 } 446 err = k8sClient.Create(ctx, &secret) 447 assert.NoError(t, err) 448 449 type args struct { 450 k8sClient client.Client 451 GitCredentialsSecretReference *corev1.SecretReference 452 } 453 type want struct { 454 publicKey *gitssh.PublicKeys 455 err string 456 } 457 cases := []struct { 458 name string 459 args args 460 want want 461 }{ 462 { 463 name: "git credentials secret does not exist", 464 args: args{ 465 k8sClient: k8sClient, 466 GitCredentialsSecretReference: &corev1.SecretReference{ 467 Name: "git-ssh-auth-secret-not-exist", 468 Namespace: "default", 469 }, 470 }, 471 want: want{ 472 publicKey: nil, 473 err: "failed to get git credentials secret: secrets \"git-ssh-auth-secret-not-exist\" not found", 474 }, 475 }, 476 { 477 name: "ssh-privatekey not in git credentials secret", 478 args: args{ 479 k8sClient: k8sClient, 480 GitCredentialsSecretReference: &corev1.SecretReference{ 481 Name: "git-ssh-auth-no-ssh-privatekey", 482 Namespace: "default", 483 }, 484 }, 485 want: want{ 486 publicKey: nil, 487 err: fmt.Sprintf("'%s' not in git credentials secret", corev1.SSHAuthPrivateKey), 488 }, 489 }, 490 { 491 name: "known_hosts not in git credentials secret", 492 args: args{ 493 k8sClient: k8sClient, 494 GitCredentialsSecretReference: &corev1.SecretReference{ 495 Name: "git-ssh-auth-no-known_hosts", 496 Namespace: "default", 497 }, 498 }, 499 want: want{ 500 publicKey: nil, 501 err: fmt.Sprintf("'%s' not in git credentials secret", GitCredsKnownHosts), 502 }, 503 }, 504 { 505 name: "valid git credentials secret found", 506 args: args{ 507 k8sClient: k8sClient, 508 GitCredentialsSecretReference: &corev1.SecretReference{ 509 Name: "git-ssh-auth", 510 Namespace: "default", 511 }, 512 }, 513 want: want{ 514 publicKey: pubKey, 515 }, 516 }, 517 } 518 519 for _, tc := range cases { 520 t.Run(tc.name, func(t *testing.T) { 521 publicKey, err := GetGitSSHPublicKey(ctx, tc.args.k8sClient, tc.args.GitCredentialsSecretReference) 522 523 if len(tc.want.err) > 0 { 524 assert.Error(t, err, tc.want.err) 525 } 526 527 if tc.want.publicKey != nil { 528 assert.Equal(t, publicKey.Signer.PublicKey().Marshal(), tc.want.publicKey.Signer.PublicKey().Marshal()) 529 assert.Equal(t, publicKey.User, tc.want.publicKey.User) 530 known_hosts_filepath := os.Getenv("SSH_KNOWN_HOSTS") 531 known_hosts, err := os.ReadFile(known_hosts_filepath) 532 assert.NoError(t, err) 533 assert.Equal(t, known_hosts, sshAuth[GitCredsKnownHosts]) 534 } 535 }) 536 } 537 }