...

Source file src/k8s.io/kubernetes/cmd/kubeadm/app/util/patches/patches_test.go

Documentation: k8s.io/kubernetes/cmd/kubeadm/app/util/patches

     1  /*
     2  Copyright 2020 The Kubernetes 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 patches
    18  
    19  import (
    20  	"bytes"
    21  	"io"
    22  	"os"
    23  	"path/filepath"
    24  	"reflect"
    25  	"testing"
    26  
    27  	utiltesting "k8s.io/client-go/util/testing"
    28  
    29  	"github.com/pkg/errors"
    30  
    31  	v1 "k8s.io/api/core/v1"
    32  	"k8s.io/apimachinery/pkg/types"
    33  )
    34  
    35  var testKnownTargets = []string{
    36  	"etcd",
    37  	"kube-apiserver",
    38  	"kube-controller-manager",
    39  	"kube-scheduler",
    40  	"kubeletconfiguration",
    41  }
    42  
    43  const testDirPattern = "patch-files"
    44  
    45  func TestParseFilename(t *testing.T) {
    46  	tests := []struct {
    47  		name               string
    48  		fileName           string
    49  		expectedTargetName string
    50  		expectedPatchType  types.PatchType
    51  		expectedWarning    bool
    52  		expectedError      bool
    53  	}{
    54  		{
    55  			name:               "valid: known target and patch type",
    56  			fileName:           "etcd+merge.json",
    57  			expectedTargetName: "etcd",
    58  			expectedPatchType:  types.MergePatchType,
    59  		},
    60  		{
    61  			name:               "valid: known target and default patch type",
    62  			fileName:           "etcd0.yaml",
    63  			expectedTargetName: "etcd",
    64  			expectedPatchType:  types.StrategicMergePatchType,
    65  		},
    66  		{
    67  			name:               "valid: known target and custom patch type",
    68  			fileName:           "etcd0+merge.yaml",
    69  			expectedTargetName: "etcd",
    70  			expectedPatchType:  types.MergePatchType,
    71  		},
    72  		{
    73  			name:            "invalid: unknown target",
    74  			fileName:        "foo.yaml",
    75  			expectedWarning: true,
    76  		},
    77  		{
    78  			name:            "invalid: unknown extension",
    79  			fileName:        "etcd.foo",
    80  			expectedWarning: true,
    81  		},
    82  		{
    83  			name:            "invalid: missing extension",
    84  			fileName:        "etcd",
    85  			expectedWarning: true,
    86  		},
    87  		{
    88  			name:          "invalid: unknown patch type",
    89  			fileName:      "etcd+foo.json",
    90  			expectedError: true,
    91  		},
    92  		{
    93  			name:          "invalid: missing patch type",
    94  			fileName:      "etcd+.json",
    95  			expectedError: true,
    96  		},
    97  	}
    98  
    99  	for _, tc := range tests {
   100  		t.Run(tc.name, func(t *testing.T) {
   101  			targetName, patchType, warn, err := parseFilename(tc.fileName, testKnownTargets)
   102  			if (err != nil) != tc.expectedError {
   103  				t.Errorf("expected error: %v, got: %v, error: %v", tc.expectedError, err != nil, err)
   104  			}
   105  			if (warn != nil) != tc.expectedWarning {
   106  				t.Errorf("expected warning: %v, got: %v, warning: %v", tc.expectedWarning, warn != nil, warn)
   107  			}
   108  			if targetName != tc.expectedTargetName {
   109  				t.Errorf("expected target name: %v, got: %v", tc.expectedTargetName, targetName)
   110  			}
   111  			if patchType != tc.expectedPatchType {
   112  				t.Errorf("expected patch type: %v, got: %v", tc.expectedPatchType, patchType)
   113  			}
   114  		})
   115  	}
   116  }
   117  
   118  func TestCreatePatchSet(t *testing.T) {
   119  	tests := []struct {
   120  		name             string
   121  		targetName       string
   122  		patchType        types.PatchType
   123  		expectedPatchSet *patchSet
   124  		data             string
   125  	}{
   126  		{
   127  
   128  			name:       "valid: YAML patches are separated and converted to JSON",
   129  			targetName: "etcd",
   130  			patchType:  types.StrategicMergePatchType,
   131  			data:       "foo: bar\n---\nfoo: baz\n",
   132  			expectedPatchSet: &patchSet{
   133  				targetName: "etcd",
   134  				patchType:  types.StrategicMergePatchType,
   135  				patches:    []string{`{"foo":"bar"}`, `{"foo":"baz"}`},
   136  			},
   137  		},
   138  		{
   139  			name:       "valid: JSON patches are separated",
   140  			targetName: "etcd",
   141  			patchType:  types.StrategicMergePatchType,
   142  			data:       `{"foo":"bar"}` + "\n---\n" + `{"foo":"baz"}`,
   143  			expectedPatchSet: &patchSet{
   144  				targetName: "etcd",
   145  				patchType:  types.StrategicMergePatchType,
   146  				patches:    []string{`{"foo":"bar"}`, `{"foo":"baz"}`},
   147  			},
   148  		},
   149  		{
   150  			name:       "valid: empty patches are ignored",
   151  			targetName: "etcd",
   152  			patchType:  types.StrategicMergePatchType,
   153  			data:       `{"foo":"bar"}` + "\n---\n     ---\n" + `{"foo":"baz"}`,
   154  			expectedPatchSet: &patchSet{
   155  				targetName: "etcd",
   156  				patchType:  types.StrategicMergePatchType,
   157  				patches:    []string{`{"foo":"bar"}`, `{"foo":"baz"}`},
   158  			},
   159  		},
   160  	}
   161  
   162  	for _, tc := range tests {
   163  		t.Run(tc.name, func(t *testing.T) {
   164  			ps, _ := createPatchSet(tc.targetName, tc.patchType, tc.data)
   165  			if !reflect.DeepEqual(ps, tc.expectedPatchSet) {
   166  				t.Fatalf("expected patch set:\n%+v\ngot:\n%+v\n", tc.expectedPatchSet, ps)
   167  			}
   168  		})
   169  	}
   170  }
   171  
   172  func TestGetPatchSetsForPathMustBeDirectory(t *testing.T) {
   173  	tempFile, err := os.CreateTemp("", "test-file")
   174  	if err != nil {
   175  		t.Errorf("error creating temporary file: %v", err)
   176  	}
   177  	defer utiltesting.CloseAndRemove(t, tempFile)
   178  
   179  	_, _, _, err = getPatchSetsFromPath(tempFile.Name(), testKnownTargets, io.Discard)
   180  	var pathErr *os.PathError
   181  	if !errors.As(err, &pathErr) {
   182  		t.Fatalf("expected os.PathError for non-directory path %q, but got %v", tempFile.Name(), err)
   183  	}
   184  }
   185  
   186  func TestGetPatchSetsForPath(t *testing.T) {
   187  	const patchData = `{"foo":"bar"}`
   188  
   189  	tests := []struct {
   190  		name                 string
   191  		filesToWrite         []string
   192  		expectedPatchSets    []*patchSet
   193  		expectedPatchFiles   []string
   194  		expectedIgnoredFiles []string
   195  		expectedError        bool
   196  		patchData            string
   197  	}{
   198  		{
   199  			name:         "valid: patch files are sorted and non-patch files are ignored",
   200  			filesToWrite: []string{"kube-scheduler+merge.json", "kube-apiserver+json.yaml", "etcd.yaml", "foo", "bar.json"},
   201  			patchData:    patchData,
   202  			expectedPatchSets: []*patchSet{
   203  				{
   204  					targetName: "etcd",
   205  					patchType:  types.StrategicMergePatchType,
   206  					patches:    []string{patchData},
   207  				},
   208  				{
   209  					targetName: "kube-apiserver",
   210  					patchType:  types.JSONPatchType,
   211  					patches:    []string{patchData},
   212  				},
   213  				{
   214  					targetName: "kube-scheduler",
   215  					patchType:  types.MergePatchType,
   216  					patches:    []string{patchData},
   217  				},
   218  			},
   219  			expectedPatchFiles:   []string{"etcd.yaml", "kube-apiserver+json.yaml", "kube-scheduler+merge.json"},
   220  			expectedIgnoredFiles: []string{"bar.json", "foo"},
   221  		},
   222  		{
   223  			name:                 "valid: empty files are ignored",
   224  			patchData:            "",
   225  			filesToWrite:         []string{"kube-scheduler.json"},
   226  			expectedPatchFiles:   []string{},
   227  			expectedIgnoredFiles: []string{"kube-scheduler.json"},
   228  			expectedPatchSets:    []*patchSet{},
   229  		},
   230  		{
   231  			name:          "invalid: bad patch type in filename returns and error",
   232  			filesToWrite:  []string{"kube-scheduler+foo.json"},
   233  			expectedError: true,
   234  		},
   235  	}
   236  
   237  	for _, tc := range tests {
   238  		t.Run(tc.name, func(t *testing.T) {
   239  			tempDir, err := os.MkdirTemp("", testDirPattern)
   240  			if err != nil {
   241  				t.Fatal(err)
   242  			}
   243  			defer os.RemoveAll(tempDir)
   244  
   245  			for _, file := range tc.filesToWrite {
   246  				filePath := filepath.Join(tempDir, file)
   247  				err := os.WriteFile(filePath, []byte(tc.patchData), 0644)
   248  				if err != nil {
   249  					t.Fatalf("could not write temporary file %q", filePath)
   250  				}
   251  			}
   252  
   253  			patchSets, patchFiles, ignoredFiles, err := getPatchSetsFromPath(tempDir, testKnownTargets, io.Discard)
   254  			if (err != nil) != tc.expectedError {
   255  				t.Fatalf("expected error: %v, got: %v, error: %v", tc.expectedError, err != nil, err)
   256  			}
   257  
   258  			if !reflect.DeepEqual(tc.expectedPatchFiles, patchFiles) {
   259  				t.Fatalf("expected patch files:\n%+v\ngot:\n%+v", tc.expectedPatchFiles, patchFiles)
   260  			}
   261  			if !reflect.DeepEqual(tc.expectedIgnoredFiles, ignoredFiles) {
   262  				t.Fatalf("expected ignored files:\n%+v\ngot:\n%+v", tc.expectedIgnoredFiles, ignoredFiles)
   263  			}
   264  			if !reflect.DeepEqual(tc.expectedPatchSets, patchSets) {
   265  				t.Fatalf("expected patch sets:\n%+v\ngot:\n%+v", tc.expectedPatchSets, patchSets)
   266  			}
   267  		})
   268  	}
   269  }
   270  
   271  func TestGetPatchManagerForPath(t *testing.T) {
   272  	type file struct {
   273  		name string
   274  		data string
   275  	}
   276  
   277  	tests := []struct {
   278  		name          string
   279  		files         []*file
   280  		patchTarget   *PatchTarget
   281  		expectedData  []byte
   282  		expectedError bool
   283  	}{
   284  		{
   285  			name: "valid: patch a kube-apiserver target using merge patch; json patch is applied first",
   286  			patchTarget: &PatchTarget{
   287  				Name:                      "kube-apiserver",
   288  				StrategicMergePatchObject: v1.Pod{},
   289  				Data:                      []byte("foo: bar\nbaz: qux\n"),
   290  			},
   291  			expectedData: []byte(`{"baz":"qux","foo":"patched"}`),
   292  			files: []*file{
   293  				{
   294  					name: "kube-apiserver+merge.yaml",
   295  					data: "foo: patched",
   296  				},
   297  				{
   298  					name: "kube-apiserver+json.json",
   299  					data: `[{"op": "replace", "path": "/foo", "value": "zzz"}]`,
   300  				},
   301  			},
   302  		},
   303  		{
   304  			name: "valid: kube-apiserver target is patched with json patch",
   305  			patchTarget: &PatchTarget{
   306  				Name:                      "kube-apiserver",
   307  				StrategicMergePatchObject: v1.Pod{},
   308  				Data:                      []byte("foo: bar\n"),
   309  			},
   310  			expectedData: []byte(`{"foo":"zzz"}`),
   311  			files: []*file{
   312  				{
   313  					name: "kube-apiserver+json.json",
   314  					data: `[{"op": "replace", "path": "/foo", "value": "zzz"}]`,
   315  				},
   316  			},
   317  		},
   318  		{
   319  			name: "valid: kubeletconfiguration target is patched with json patch",
   320  			patchTarget: &PatchTarget{
   321  				Name:                      "kubeletconfiguration",
   322  				StrategicMergePatchObject: nil,
   323  				Data:                      []byte("foo: bar\n"),
   324  			},
   325  			expectedData: []byte(`{"foo":"zzz"}`),
   326  			files: []*file{
   327  				{
   328  					name: "kubeletconfiguration+json.json",
   329  					data: `[{"op": "replace", "path": "/foo", "value": "zzz"}]`,
   330  				},
   331  			},
   332  		},
   333  		{
   334  			name: "valid: kube-apiserver target is patched with strategic merge patch",
   335  			patchTarget: &PatchTarget{
   336  				Name:                      "kube-apiserver",
   337  				StrategicMergePatchObject: v1.Pod{},
   338  				Data:                      []byte("foo: bar\n"),
   339  			},
   340  			expectedData: []byte(`{"foo":"zzz"}`),
   341  			files: []*file{
   342  				{
   343  					name: "kube-apiserver+strategic.json",
   344  					data: `{"foo":"zzz"}`,
   345  				},
   346  			},
   347  		},
   348  		{
   349  			name: "valid: etcd target is not changed because there are no patches for it",
   350  			patchTarget: &PatchTarget{
   351  				Name:                      "etcd",
   352  				StrategicMergePatchObject: v1.Pod{},
   353  				Data:                      []byte("foo: bar\n"),
   354  			},
   355  			expectedData: []byte("foo: bar\n"),
   356  			files: []*file{
   357  				{
   358  					name: "kube-apiserver+merge.yaml",
   359  					data: "foo: patched",
   360  				},
   361  			},
   362  		},
   363  		{
   364  			name: "invalid: cannot patch etcd target due to malformed json patch",
   365  			patchTarget: &PatchTarget{
   366  				Name:                      "etcd",
   367  				StrategicMergePatchObject: v1.Pod{},
   368  				Data:                      []byte("foo: bar\n"),
   369  			},
   370  			files: []*file{
   371  				{
   372  					name: "etcd+json.json",
   373  					data: `{"foo":"zzz"}`,
   374  				},
   375  			},
   376  			expectedError: true,
   377  		},
   378  	}
   379  
   380  	for _, tc := range tests {
   381  		t.Run(tc.name, func(t *testing.T) {
   382  			tempDir, err := os.MkdirTemp("", testDirPattern)
   383  			if err != nil {
   384  				t.Fatal(err)
   385  			}
   386  			defer os.RemoveAll(tempDir)
   387  
   388  			for _, file := range tc.files {
   389  				filePath := filepath.Join(tempDir, file.name)
   390  				err := os.WriteFile(filePath, []byte(file.data), 0644)
   391  				if err != nil {
   392  					t.Fatalf("could not write temporary file %q", filePath)
   393  				}
   394  			}
   395  
   396  			pm, err := GetPatchManagerForPath(tempDir, testKnownTargets, nil)
   397  			if err != nil {
   398  				t.Fatal(err)
   399  			}
   400  
   401  			err = pm.ApplyPatchesToTarget(tc.patchTarget)
   402  			if (err != nil) != tc.expectedError {
   403  				t.Fatalf("expected error: %v, got: %v, error: %v", tc.expectedError, err != nil, err)
   404  			}
   405  			if err != nil {
   406  				return
   407  			}
   408  
   409  			if !bytes.Equal(tc.patchTarget.Data, tc.expectedData) {
   410  				t.Fatalf("expected result:\n%s\ngot:\n%s", tc.expectedData, tc.patchTarget.Data)
   411  			}
   412  		})
   413  	}
   414  }
   415  
   416  func TestGetPatchManagerForPathCache(t *testing.T) {
   417  	tempDir, err := os.MkdirTemp("", testDirPattern)
   418  	if err != nil {
   419  		t.Fatal(err)
   420  	}
   421  	defer os.RemoveAll(tempDir)
   422  
   423  	pmOld, err := GetPatchManagerForPath(tempDir, testKnownTargets, nil)
   424  	if err != nil {
   425  		t.Fatal(err)
   426  	}
   427  	pmNew, err := GetPatchManagerForPath(tempDir, testKnownTargets, nil)
   428  	if err != nil {
   429  		t.Fatal(err)
   430  	}
   431  	if pmOld != pmNew {
   432  		t.Logf("path %q was not cached, expected pointer: %p, got: %p", tempDir, pmOld, pmNew)
   433  	}
   434  }
   435  

View as plain text