github.com/wmuizelaar/kpt@v0.0.0-20221018115725-bd564717b2ed/pkg/api/kptfile/v1/validation_test.go (about)

     1  // Copyright 2021 Google LLC
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //	http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  package v1
    15  
    16  import (
    17  	"os"
    18  	"path/filepath"
    19  	"testing"
    20  
    21  	"github.com/GoogleContainerTools/kpt/internal/types"
    22  	"github.com/stretchr/testify/assert"
    23  	"sigs.k8s.io/kustomize/kyaml/filesys"
    24  	"sigs.k8s.io/kustomize/kyaml/yaml"
    25  )
    26  
    27  func TestKptfileValidate(t *testing.T) {
    28  	type input struct {
    29  		name    string
    30  		kptfile KptFile
    31  		valid   bool
    32  	}
    33  
    34  	cases := []input{
    35  		{
    36  			name: "pipeline: empty",
    37  			kptfile: KptFile{
    38  				Pipeline: &Pipeline{},
    39  			},
    40  			valid: true,
    41  		},
    42  		{
    43  			name: "pipeline: validcase",
    44  			kptfile: KptFile{
    45  				Pipeline: &Pipeline{
    46  					Mutators: []Function{
    47  						{
    48  							Image: "patch-strategic-merge",
    49  						},
    50  						{
    51  							Image: "gcr.io/kpt-fn/set-annotations:v0.1",
    52  							ConfigMap: map[string]string{
    53  								"environment": "dev",
    54  							},
    55  						},
    56  					},
    57  					Validators: []Function{
    58  						{
    59  							Image: "gcr.io/kpt-fn/gatekeeper",
    60  						},
    61  					},
    62  				},
    63  			},
    64  			valid: true,
    65  		},
    66  		{
    67  			name: "pipeline: invalid image name",
    68  			kptfile: KptFile{
    69  				Pipeline: &Pipeline{
    70  					Mutators: []Function{
    71  						{
    72  							Image: "patch@_@strategic-merge",
    73  						},
    74  					},
    75  				},
    76  			},
    77  			valid: false,
    78  		},
    79  		{
    80  			name: "pipeline: more than 1 config",
    81  			kptfile: KptFile{
    82  				Pipeline: &Pipeline{
    83  					Mutators: []Function{
    84  						{
    85  							Image:      "image",
    86  							ConfigPath: "./config.yaml",
    87  							ConfigMap: map[string]string{
    88  								"foo": "bar",
    89  							},
    90  						},
    91  					},
    92  				},
    93  			},
    94  			valid: false,
    95  		},
    96  		{
    97  			name: "pipeline: absolute config path",
    98  			kptfile: KptFile{
    99  				Pipeline: &Pipeline{
   100  					Mutators: []Function{
   101  						{
   102  							Image:      "image",
   103  							ConfigPath: "/config.yaml",
   104  						},
   105  					},
   106  				},
   107  			},
   108  			valid: false,
   109  		},
   110  		{
   111  			name: "pipeline: configpath referring file in parent",
   112  			kptfile: KptFile{
   113  				Pipeline: &Pipeline{
   114  					Mutators: []Function{
   115  						{
   116  							Image:      "image",
   117  							ConfigPath: "../config.yaml",
   118  						},
   119  					},
   120  				},
   121  			},
   122  			valid: false,
   123  		},
   124  		{
   125  			name: "pipeline: cleaned configpath contains ..",
   126  			kptfile: KptFile{
   127  				Pipeline: &Pipeline{
   128  					Mutators: []Function{
   129  						{
   130  							Image:      "image",
   131  							ConfigPath: "a/b/../../../config.yaml",
   132  						},
   133  					},
   134  				},
   135  			},
   136  			valid: false,
   137  		},
   138  		{
   139  			name: "pipeline: configpath contains invalid .. references",
   140  			kptfile: KptFile{
   141  				Pipeline: &Pipeline{
   142  					Mutators: []Function{
   143  						{
   144  							Image:      "image",
   145  							ConfigPath: "a/.../config.yaml",
   146  						},
   147  					},
   148  				},
   149  			},
   150  			valid: false,
   151  		},
   152  	}
   153  
   154  	for _, c := range cases {
   155  		c := c
   156  		t.Run(c.name, func(t *testing.T) {
   157  			err := c.kptfile.Validate(filesys.FileSystemOrOnDisk{}, "")
   158  			if c.valid && err != nil {
   159  				t.Fatalf("kptfile should be valid, %s", err)
   160  			}
   161  			if !c.valid && err == nil {
   162  				t.Fatal("kptfile should not be valid")
   163  			}
   164  		})
   165  	}
   166  }
   167  
   168  func TestValidateFunctionName(t *testing.T) {
   169  	type input struct {
   170  		Name  string
   171  		Valid bool
   172  	}
   173  	inputs := []input{
   174  		{
   175  			"gcr.io/kpt-fn/generate-folders",
   176  			true,
   177  		},
   178  		{
   179  			"patch-strategic-merge",
   180  			true,
   181  		},
   182  		{
   183  			"gcr.io/kpt-fn/generate-folders:unstable",
   184  			true,
   185  		},
   186  		{
   187  			"patch-strategic-merge:v1.3_beta",
   188  			true,
   189  		},
   190  		{
   191  			"gcr.io/kpt-fn/generate-folders:v1.2.3-alpha1",
   192  			true,
   193  		},
   194  		{
   195  			"patch-strategic-merge:x.y.z",
   196  			true,
   197  		},
   198  		{
   199  			"patch-strategic-merge::@!",
   200  			false,
   201  		},
   202  		{
   203  			"patch-strategic-merge:",
   204  			false,
   205  		},
   206  		{
   207  			"a.b.c:1234/foo/bar/generate-folders",
   208  			true,
   209  		},
   210  		{
   211  			"ab-.b/c",
   212  			false,
   213  		},
   214  		{
   215  			"a/a/",
   216  			false,
   217  		},
   218  		{
   219  			"a//a/a",
   220  			false,
   221  		},
   222  		{
   223  			"example.com/.dots/myimage",
   224  			false,
   225  		},
   226  		{
   227  			"registry.io/foo/project--id.module--name.ver---sion--name",
   228  			true,
   229  		},
   230  		{
   231  			"Foo/FarB",
   232  			false,
   233  		},
   234  		{
   235  			"example.com/foo/generate-folders@sha256:3434a5299f8fcb2c2ade9975e56ca5a622427b9d5a9a971640765e830fb90a0e",
   236  			true,
   237  		},
   238  	}
   239  
   240  	for _, n := range inputs {
   241  		n := n
   242  		t.Run(n.Name, func(t *testing.T) {
   243  			err := ValidateFunctionImageURL(n.Name)
   244  			if n.Valid && err != nil {
   245  				t.Fatalf("function name %s should be valid", n.Name)
   246  			}
   247  			if !n.Valid && err == nil {
   248  				t.Fatalf("function name %s should not be valid", n.Name)
   249  			}
   250  		})
   251  	}
   252  }
   253  
   254  func TestValidatePath(t *testing.T) {
   255  	type input struct {
   256  		Path  string
   257  		Valid bool
   258  	}
   259  
   260  	cases := []input{
   261  		{
   262  			"a/b/c",
   263  			true,
   264  		},
   265  		{
   266  			"a/b/",
   267  			true,
   268  		},
   269  		{
   270  			"/a/b",
   271  			false,
   272  		},
   273  		{
   274  			"./a",
   275  			true,
   276  		},
   277  		{
   278  			"./a/.../b",
   279  			false,
   280  		},
   281  		{
   282  			".",
   283  			true,
   284  		},
   285  		{
   286  			"a\\b",
   287  			true,
   288  		},
   289  		{
   290  			"a\b",
   291  			true,
   292  		},
   293  		{
   294  			"a\v",
   295  			true,
   296  		},
   297  		{
   298  			"a:\\b\\c",
   299  			true,
   300  		},
   301  		{
   302  			"../a/../b",
   303  			false,
   304  		},
   305  		{
   306  			"a//b",
   307  			true,
   308  		},
   309  		{
   310  			"a/b/.",
   311  			true,
   312  		},
   313  		{
   314  			"././././",
   315  			true,
   316  		},
   317  		{
   318  			"",
   319  			false,
   320  		},
   321  		{
   322  			"\t \n",
   323  			false,
   324  		},
   325  		{
   326  			"a/b/../config.yaml",
   327  			true,
   328  		},
   329  	}
   330  
   331  	for _, c := range cases {
   332  		ret := validateFnConfigPathSyntax(c.Path)
   333  		if (ret == nil) != c.Valid {
   334  			t.Fatalf("returned value for path %q should be %t, got %t",
   335  				c.Path, c.Valid, (ret == nil))
   336  		}
   337  	}
   338  }
   339  
   340  func TestIsKustomization(t *testing.T) {
   341  	testcases := []struct {
   342  		name  string
   343  		input string
   344  		exp   bool
   345  	}{
   346  		{
   347  			"resource in a kustomization file is a kustomization",
   348  			`
   349  metadata:
   350    annotations:
   351      config.kubernetes.io/path: kustomization.yaml
   352  `,
   353  			true,
   354  		},
   355  		{
   356  			"resource in a kustomization file with .yml extn is a kustomization",
   357  			`
   358  metadata:
   359    annotations:
   360      config.kubernetes.io/path: kustomization.yml
   361  `,
   362  			true,
   363  		},
   364  		{
   365  			"resource in a kustomization file in a subdir is a kustomization",
   366  			`
   367  metadata:
   368    annotations:
   369      config.kubernetes.io/path: subdir/kustomization.yaml
   370  `,
   371  			true,
   372  		},
   373  		{
   374  			"resource in a non-kustomization file, with empty apigroup and Kustomization kind is a kustomization",
   375  			`apiVersion:
   376  kind: Kustomization
   377  `,
   378  			true,
   379  		},
   380  		{
   381  			"resource in a non-kustomization file with Kustomization APIGroup is a kustomization",
   382  			`apiVersion: kustomize.config.k8s.io/v1beta1
   383  `,
   384  			true,
   385  		},
   386  		{
   387  			"resource in a non-kustomization file with non-kustomize apigroup is not a kustomization",
   388  			`apiVersion: non.kubernetes.io/v1
   389  `,
   390  			false,
   391  		},
   392  	}
   393  	for _, tc := range testcases {
   394  		tc := tc
   395  		t.Run(tc.name, func(t *testing.T) {
   396  			got := isKustomization(yaml.MustParse(tc.input))
   397  			if got != tc.exp {
   398  				t.Fatalf("got %v expected %v", got, tc.exp)
   399  			}
   400  		})
   401  	}
   402  }
   403  
   404  func TestGetValidatedFnConfigFromPath(t *testing.T) {
   405  	testcases := []struct {
   406  		name   string
   407  		input  string
   408  		exp    string
   409  		errMsg string
   410  	}{
   411  		{
   412  			name: "normal resource",
   413  			input: `
   414  apiVersion: v1
   415  kind: Service
   416  metadata:
   417    name: myService
   418  spec:
   419    selector:
   420      app: bar
   421  `,
   422  			exp: `apiVersion: v1
   423  kind: Service
   424  metadata:
   425    name: myService
   426    annotations:
   427      config.kubernetes.io/index: '0'
   428      internal.config.kubernetes.io/index: '0'
   429      internal.config.kubernetes.io/seqindent: 'compact'
   430  spec:
   431    selector:
   432      app: bar
   433  `,
   434  		},
   435  		{
   436  			name: "multiple resources wrapped in List",
   437  			input: `
   438  apiVersion: v1
   439  kind: List
   440  metadata:
   441    name: upsert-multiple-resources-config
   442  items:
   443  - apiVersion: v1
   444    kind: Service
   445    metadata:
   446      name: myService
   447      namespace: mySpace
   448    spec:
   449      selector:
   450        app: bar
   451  - apiVersion: apps/v1
   452    kind: Deployment
   453    metadata:
   454      name: myDeployment2
   455      namespace: mySpace
   456    spec:
   457      replicas: 10
   458  `,
   459  			exp: `apiVersion: v1
   460  kind: List
   461  metadata:
   462    name: upsert-multiple-resources-config
   463    annotations:
   464      config.kubernetes.io/index: '0'
   465      internal.config.kubernetes.io/index: '0'
   466      internal.config.kubernetes.io/seqindent: 'compact'
   467  items:
   468  - apiVersion: v1
   469    kind: Service
   470    metadata:
   471      name: myService
   472      namespace: mySpace
   473    spec:
   474      selector:
   475        app: bar
   476  - apiVersion: apps/v1
   477    kind: Deployment
   478    metadata:
   479      name: myDeployment2
   480      namespace: mySpace
   481    spec:
   482      replicas: 10
   483  `,
   484  		},
   485  		{
   486  			name: "error for multiple resources",
   487  			input: `
   488  apiVersion: v1
   489  kind: Service
   490  metadata:
   491    name: myService
   492    namespace: mySpace
   493  ---
   494  apiVersion: v1
   495  kind: Service
   496  metadata:
   497    name: myService2
   498    namespace: mySpace
   499  `,
   500  			errMsg: `functionConfig "f1.yaml" must not contain more than one config, got 2`,
   501  		},
   502  	}
   503  	for _, tc := range testcases {
   504  		tc := tc
   505  		t.Run(tc.name, func(t *testing.T) {
   506  			d := t.TempDir()
   507  			err := os.WriteFile(filepath.Join(d, "f1.yaml"), []byte(tc.input), 0700)
   508  			assert.NoError(t, err)
   509  			got, err := GetValidatedFnConfigFromPath(filesys.FileSystemOrOnDisk{}, types.UniquePath(d), "f1.yaml")
   510  			if tc.errMsg != "" {
   511  				assert.Error(t, err)
   512  				assert.Equal(t, tc.errMsg, err.Error())
   513  				return
   514  			}
   515  			assert.NoError(t, err)
   516  			actual, err := got.String()
   517  			assert.NoError(t, err)
   518  			assert.Equal(t, tc.exp, actual)
   519  		})
   520  	}
   521  }