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  }