...

Source file src/sigs.k8s.io/cli-utils/pkg/common/path_test.go

Documentation: sigs.k8s.io/cli-utils/pkg/common

     1  // Copyright 2020 The Kubernetes Authors.
     2  // SPDX-License-Identifier: Apache-2.0
     3  
     4  package common
     5  
     6  import (
     7  	"bytes"
     8  	"os"
     9  	"path/filepath"
    10  	"strings"
    11  	"testing"
    12  
    13  	"github.com/stretchr/testify/assert"
    14  	"k8s.io/cli-runtime/pkg/genericclioptions"
    15  	"sigs.k8s.io/cli-utils/pkg/testutil"
    16  )
    17  
    18  const (
    19  	packageDir              = "test-pkg-dir"
    20  	subFolder               = "sub-folder"
    21  	inventoryFilename       = "inventory.yaml"
    22  	secondInventoryFilename = "inventory-2.yaml"
    23  	podAFilename            = "pod-a.yaml"
    24  	podBFilename            = "pod-b.yaml"
    25  	configSeparator         = "---"
    26  )
    27  
    28  var (
    29  	inventoryFilePath       = filepath.Join(packageDir, inventoryFilename)
    30  	secondInventoryFilePath = filepath.Join(packageDir, subFolder, secondInventoryFilename)
    31  	podAFilePath            = filepath.Join(packageDir, podAFilename)
    32  	podBFilePath            = filepath.Join(packageDir, podBFilename)
    33  )
    34  
    35  func setupTestFilesystem(t *testing.T) testutil.TestFilesystem {
    36  	// Create the test filesystem, and add package config files
    37  	// to it.
    38  	t.Log("Creating test filesystem")
    39  	tf := testutil.Setup(t, packageDir)
    40  	t.Logf("Adding File: %s", inventoryFilePath)
    41  	tf.WriteFile(t, inventoryFilePath, inventoryConfigMap)
    42  	t.Logf("Adding File: %s", secondInventoryFilePath)
    43  	tf.WriteFile(t, secondInventoryFilePath, secondInventoryConfigMap)
    44  	t.Logf("Adding File: %s", podAFilePath)
    45  	tf.WriteFile(t, podAFilePath, podA)
    46  	t.Logf("Adding File: %s", podBFilePath)
    47  	tf.WriteFile(t, podBFilePath, podB)
    48  	return tf
    49  }
    50  
    51  var inventoryConfigMap = []byte(`
    52  apiVersion: v1
    53  kind: ConfigMap
    54  metadata:
    55    namespace: test-namespace
    56    name: inventory
    57    labels:
    58      cli-utils.sigs.k8s.io/inventory-id: test-inventory
    59  `)
    60  
    61  var secondInventoryConfigMap = []byte(`
    62  apiVersion: v1
    63  kind: ConfigMap
    64  metadata:
    65    namespace: test-namespace
    66    name: inventory-2
    67    labels:
    68      cli-utils.sigs.k8s.io/inventory-id: test-inventory
    69  `)
    70  
    71  var podA = []byte(`
    72  apiVersion: v1
    73  kind: Pod
    74  metadata:
    75    name: pod-a
    76    namespace: test-namespace
    77    labels:
    78      name: test-pod-label
    79  spec:
    80    containers:
    81    - name: kubernetes-pause
    82      image: k8s.gcr.io/pause:2.0
    83  `)
    84  
    85  var podB = []byte(`
    86  apiVersion: v1
    87  kind: Pod
    88  metadata:
    89    name: pod-b
    90    namespace: test-namespace
    91    labels:
    92      name: test-pod-label
    93  spec:
    94    containers:
    95    - name: kubernetes-pause
    96      image: k8s.gcr.io/pause:2.0
    97  `)
    98  
    99  func buildMultiResourceConfig(configs ...[]byte) []byte {
   100  	r := []byte{}
   101  	for i, config := range configs {
   102  		if i > 0 {
   103  			r = append(r, []byte(configSeparator)...)
   104  		}
   105  		r = append(r, config...)
   106  	}
   107  	return r
   108  }
   109  
   110  func TestProcessPaths(t *testing.T) {
   111  	tf := setupTestFilesystem(t)
   112  	defer tf.Clean()
   113  
   114  	trueVal := true
   115  	testCases := map[string]struct {
   116  		paths                     []string
   117  		expectedFileNameFlags     genericclioptions.FileNameFlags
   118  		errFromDemandOneDirectory string
   119  	}{
   120  		"empty slice means reading from StdIn": {
   121  			paths: []string{},
   122  			expectedFileNameFlags: genericclioptions.FileNameFlags{
   123  				Filenames: &[]string{"-"},
   124  			},
   125  		},
   126  		"single file in slice is error; must be directory": {
   127  			paths: []string{podAFilePath},
   128  			expectedFileNameFlags: genericclioptions.FileNameFlags{
   129  				Filenames: nil,
   130  				Recursive: nil,
   131  			},
   132  			errFromDemandOneDirectory: "argument 'test-pkg-dir/pod-a.yaml' is not but must be a directory",
   133  		},
   134  		"single dir in slice": {
   135  			paths: []string{tf.GetRootDir()},
   136  			expectedFileNameFlags: genericclioptions.FileNameFlags{
   137  				Filenames: &[]string{tf.GetRootDir()},
   138  				Recursive: &trueVal,
   139  			},
   140  		},
   141  		"multiple arguments is an error": {
   142  			paths: []string{podAFilePath, podBFilePath},
   143  			expectedFileNameFlags: genericclioptions.FileNameFlags{
   144  				Filenames: nil,
   145  				Recursive: nil,
   146  			},
   147  			errFromDemandOneDirectory: "specify exactly one directory path argument; rejecting [test-pkg-dir/pod-a.yaml test-pkg-dir/pod-b.yaml]",
   148  		},
   149  	}
   150  
   151  	for tn, tc := range testCases {
   152  		t.Run(tn, func(t *testing.T) {
   153  			fileNameFlags, err := DemandOneDirectory(tc.paths)
   154  			assert.Equal(t, tc.expectedFileNameFlags, fileNameFlags)
   155  			if err != nil && err.Error() != tc.errFromDemandOneDirectory {
   156  				assert.Equal(t, err.Error(), tc.errFromDemandOneDirectory)
   157  			}
   158  		})
   159  	}
   160  }
   161  
   162  func TestFilterInputFile(t *testing.T) {
   163  	tf := testutil.Setup(t)
   164  	defer tf.Clean()
   165  
   166  	testCases := map[string]struct {
   167  		configObjects   [][]byte
   168  		expectedObjects [][]byte
   169  	}{
   170  		"Empty config objects writes empty file": {
   171  			configObjects:   [][]byte{},
   172  			expectedObjects: [][]byte{},
   173  		},
   174  		"Only inventory obj writes empty file": {
   175  			configObjects:   [][]byte{inventoryConfigMap},
   176  			expectedObjects: [][]byte{},
   177  		},
   178  		"Only pods writes both pods": {
   179  			configObjects:   [][]byte{podA, podB},
   180  			expectedObjects: [][]byte{podA, podB},
   181  		},
   182  		"Basic case of inventory obj and two pods": {
   183  			configObjects:   [][]byte{inventoryConfigMap, podA, podB},
   184  			expectedObjects: [][]byte{podA, podB},
   185  		},
   186  		"Basic case of inventory obj and two pods in different order": {
   187  			configObjects:   [][]byte{podB, inventoryConfigMap, podA},
   188  			expectedObjects: [][]byte{podB, podA},
   189  		},
   190  	}
   191  	for tn, tc := range testCases {
   192  		t.Run(tn, func(t *testing.T) {
   193  			// Build a single file of multiple resource configs, and
   194  			// call the tested function FilterInputFile. This writes
   195  			// the passed file to the test filesystem, filtering
   196  			// the inventory object if it exists in the passed file.
   197  			in := buildMultiResourceConfig(tc.configObjects...)
   198  			err := FilterInputFile(bytes.NewReader(in), tf.GetRootDir())
   199  			if err != nil {
   200  				t.Fatalf("Unexpected error in FilterInputFile: %s", err)
   201  			}
   202  			// Retrieve the files from the test filesystem.
   203  			actualFiles, err := os.ReadDir(tf.GetRootDir())
   204  			if err != nil {
   205  				t.Fatalf("Error reading test filesystem directory: %s", err)
   206  			}
   207  			// Since we remove the generated file for each test, there should
   208  			// not be more than one file in the test filesystem.
   209  			if len(actualFiles) > 1 {
   210  				t.Fatalf("Wrong number of files (%d) in dir: %s", len(actualFiles), tf.GetRootDir())
   211  			}
   212  			// If there is a generated file, then read it into actualStr.
   213  			actualStr := ""
   214  			if len(actualFiles) != 0 {
   215  				actualFilename := actualFiles[0].Name()
   216  				defer os.Remove(actualFilename)
   217  				actual, err := os.ReadFile(actualFilename)
   218  				if err != nil {
   219  					t.Fatalf("Error reading created file (%s): %s", actualFilename, err)
   220  				}
   221  				actualStr = strings.TrimSpace(string(actual))
   222  			}
   223  			// Build the expected string from the expectedObjects. This expected
   224  			// string should not have the inventory object config in it.
   225  			expected := buildMultiResourceConfig(tc.expectedObjects...)
   226  			expectedStr := strings.TrimSpace(string(expected))
   227  			if expectedStr != actualStr {
   228  				t.Errorf("Expected file contents (%s) not equal to actual file contents (%s)",
   229  					expectedStr, actualStr)
   230  			}
   231  		})
   232  	}
   233  }
   234  
   235  func TestExpandDir(t *testing.T) {
   236  	tf := setupTestFilesystem(t)
   237  	defer tf.Clean()
   238  
   239  	testCases := map[string]struct {
   240  		packageDirPath    string
   241  		expandedInventory string
   242  		expandedPaths     []string
   243  		isError           bool
   244  	}{
   245  		"empty path is error": {
   246  			packageDirPath: "",
   247  			isError:        true,
   248  		},
   249  		"path that is not dir is error": {
   250  			packageDirPath: "fakedir1",
   251  			isError:        true,
   252  		},
   253  		"root package dir excludes inventory object": {
   254  			packageDirPath:    tf.GetRootDir(),
   255  			expandedInventory: "inventory.yaml",
   256  			expandedPaths: []string{
   257  				"pod-a.yaml",
   258  				"pod-b.yaml",
   259  			},
   260  			isError: false,
   261  		},
   262  	}
   263  
   264  	for tn, tc := range testCases {
   265  		t.Run(tn, func(t *testing.T) {
   266  			actualInventory, actualPaths, err := ExpandDir(tc.packageDirPath)
   267  			if tc.isError {
   268  				if err == nil {
   269  					t.Fatalf("expected error but received none")
   270  				}
   271  				return
   272  			}
   273  			if err != nil {
   274  				t.Fatalf("received unexpected error %#v", err)
   275  				return
   276  			}
   277  			actualFilename := filepath.Base(actualInventory)
   278  			if tc.expandedInventory != actualFilename {
   279  				t.Errorf("expected inventory template filepath (%s), got (%s)", tc.expandedInventory, actualFilename)
   280  			}
   281  			if len(tc.expandedPaths) != len(actualPaths) {
   282  				t.Errorf("expected (%d) resource filepaths, got (%d)", len(tc.expandedPaths), len(actualPaths))
   283  			}
   284  			for _, expectedPath := range tc.expandedPaths {
   285  				found := false
   286  				for _, actualPath := range actualPaths {
   287  					actualFilename := filepath.Base(actualPath)
   288  					if expectedPath == actualFilename {
   289  						found = true
   290  						break
   291  					}
   292  				}
   293  				if !found {
   294  					t.Errorf("expected filename (%s) not found", expectedPath)
   295  				}
   296  			}
   297  		})
   298  	}
   299  }
   300  
   301  func TestExpandDirErrors(t *testing.T) {
   302  	tf := setupTestFilesystem(t)
   303  	defer tf.Clean()
   304  
   305  	testCases := map[string]struct {
   306  		packageDirPath []string
   307  		expandedPaths  []string
   308  		isError        bool
   309  	}{
   310  		"empty path is error": {
   311  			packageDirPath: []string{},
   312  			isError:        true,
   313  		},
   314  		"more than one path is error": {
   315  			packageDirPath: []string{"fakedir1", "fakedir2"},
   316  			isError:        true,
   317  		},
   318  		"path that is not dir is error": {
   319  			packageDirPath: []string{"fakedir1"},
   320  			isError:        true,
   321  		},
   322  		"root package dir excludes inventory object": {
   323  			packageDirPath: []string{tf.GetRootDir()},
   324  			expandedPaths: []string{
   325  				filepath.Join(packageDir, "pod-a.yaml"),
   326  				filepath.Join(packageDir, "pod-b.yaml"),
   327  			},
   328  			isError: false,
   329  		},
   330  	}
   331  
   332  	for tn, tc := range testCases {
   333  		t.Run(tn, func(t *testing.T) {
   334  			trueVal := true
   335  			filenameFlags := genericclioptions.FileNameFlags{
   336  				Filenames: &tc.packageDirPath,
   337  				Recursive: &trueVal,
   338  			}
   339  			actualFlags, err := ExpandPackageDir(filenameFlags)
   340  			if tc.isError && err == nil {
   341  				t.Fatalf("expected error but received none")
   342  			}
   343  			if !tc.isError {
   344  				if err != nil {
   345  					t.Fatalf("unexpected error received: %v", err)
   346  				}
   347  				actualPaths := *actualFlags.Filenames
   348  				if len(tc.expandedPaths) != len(actualPaths) {
   349  					t.Errorf("expected config filepaths (%s), got (%s)",
   350  						tc.expandedPaths, actualPaths)
   351  				}
   352  				for _, expected := range tc.expandedPaths {
   353  					if !filepathExists(expected, actualPaths) {
   354  						t.Errorf("expected config filepath (%s) in actual filepaths (%s)",
   355  							expected, actualPaths)
   356  					}
   357  				}
   358  				// Check the inventory object is not in the filename flags.
   359  				for _, actualPath := range actualPaths {
   360  					if strings.Contains(actualPath, "inventory.yaml") {
   361  						t.Errorf("inventory object should be excluded")
   362  					}
   363  				}
   364  			}
   365  		})
   366  	}
   367  }
   368  
   369  // filepathExists returns true if the passed "filepath" is a substring
   370  // of any of the passed full "filepaths"; false otherwise. For example:
   371  // if filepath = "test/a.yaml", and filepaths includes "/tmp/test/a.yaml",
   372  // this function returns true.
   373  func filepathExists(filepath string, filepaths []string) bool {
   374  	for _, fp := range filepaths {
   375  		if strings.Contains(fp, filepath) {
   376  			return true
   377  		}
   378  	}
   379  	return false
   380  }
   381  

View as plain text