cli-utils/pkg/common/path_test.go

381 lines
10 KiB
Go

// Copyright 2020 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package common
import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"k8s.io/cli-runtime/pkg/genericclioptions"
"sigs.k8s.io/cli-utils/pkg/testutil"
)
const (
packageDir = "test-pkg-dir"
subFolder = "sub-folder"
inventoryFilename = "inventory.yaml"
secondInventoryFilename = "inventory-2.yaml"
podAFilename = "pod-a.yaml"
podBFilename = "pod-b.yaml"
configSeparator = "---"
)
var (
inventoryFilePath = filepath.Join(packageDir, inventoryFilename)
secondInventoryFilePath = filepath.Join(packageDir, subFolder, secondInventoryFilename)
podAFilePath = filepath.Join(packageDir, podAFilename)
podBFilePath = filepath.Join(packageDir, podBFilename)
)
func setupTestFilesystem(t *testing.T) testutil.TestFilesystem {
// Create the test filesystem, and add package config files
// to it.
t.Log("Creating test filesystem")
tf := testutil.Setup(t, packageDir)
t.Logf("Adding File: %s", inventoryFilePath)
tf.WriteFile(t, inventoryFilePath, inventoryConfigMap)
t.Logf("Adding File: %s", secondInventoryFilePath)
tf.WriteFile(t, secondInventoryFilePath, secondInventoryConfigMap)
t.Logf("Adding File: %s", podAFilePath)
tf.WriteFile(t, podAFilePath, podA)
t.Logf("Adding File: %s", podBFilePath)
tf.WriteFile(t, podBFilePath, podB)
return tf
}
var inventoryConfigMap = []byte(`
apiVersion: v1
kind: ConfigMap
metadata:
namespace: test-namespace
name: inventory
labels:
cli-utils.sigs.k8s.io/inventory-id: test-inventory
`)
var secondInventoryConfigMap = []byte(`
apiVersion: v1
kind: ConfigMap
metadata:
namespace: test-namespace
name: inventory-2
labels:
cli-utils.sigs.k8s.io/inventory-id: test-inventory
`)
var podA = []byte(`
apiVersion: v1
kind: Pod
metadata:
name: pod-a
namespace: test-namespace
labels:
name: test-pod-label
spec:
containers:
- name: kubernetes-pause
image: registry.k8s.io/pause:2.0
`)
var podB = []byte(`
apiVersion: v1
kind: Pod
metadata:
name: pod-b
namespace: test-namespace
labels:
name: test-pod-label
spec:
containers:
- name: kubernetes-pause
image: registry.k8s.io/pause:2.0
`)
func buildMultiResourceConfig(configs ...[]byte) []byte {
r := []byte{}
for i, config := range configs {
if i > 0 {
r = append(r, []byte(configSeparator)...)
}
r = append(r, config...)
}
return r
}
func TestProcessPaths(t *testing.T) {
tf := setupTestFilesystem(t)
defer tf.Clean()
trueVal := true
testCases := map[string]struct {
paths []string
expectedFileNameFlags genericclioptions.FileNameFlags
errFromDemandOneDirectory string
}{
"empty slice means reading from StdIn": {
paths: []string{},
expectedFileNameFlags: genericclioptions.FileNameFlags{
Filenames: &[]string{"-"},
},
},
"single file in slice is error; must be directory": {
paths: []string{podAFilePath},
expectedFileNameFlags: genericclioptions.FileNameFlags{
Filenames: nil,
Recursive: nil,
},
errFromDemandOneDirectory: "argument 'test-pkg-dir/pod-a.yaml' is not but must be a directory",
},
"single dir in slice": {
paths: []string{tf.GetRootDir()},
expectedFileNameFlags: genericclioptions.FileNameFlags{
Filenames: &[]string{tf.GetRootDir()},
Recursive: &trueVal,
},
},
"multiple arguments is an error": {
paths: []string{podAFilePath, podBFilePath},
expectedFileNameFlags: genericclioptions.FileNameFlags{
Filenames: nil,
Recursive: nil,
},
errFromDemandOneDirectory: "specify exactly one directory path argument; rejecting [test-pkg-dir/pod-a.yaml test-pkg-dir/pod-b.yaml]",
},
}
for tn, tc := range testCases {
t.Run(tn, func(t *testing.T) {
fileNameFlags, err := DemandOneDirectory(tc.paths)
assert.Equal(t, tc.expectedFileNameFlags, fileNameFlags)
if err != nil && err.Error() != tc.errFromDemandOneDirectory {
assert.Equal(t, err.Error(), tc.errFromDemandOneDirectory)
}
})
}
}
func TestFilterInputFile(t *testing.T) {
tf := testutil.Setup(t)
defer tf.Clean()
testCases := map[string]struct {
configObjects [][]byte
expectedObjects [][]byte
}{
"Empty config objects writes empty file": {
configObjects: [][]byte{},
expectedObjects: [][]byte{},
},
"Only inventory obj writes empty file": {
configObjects: [][]byte{inventoryConfigMap},
expectedObjects: [][]byte{},
},
"Only pods writes both pods": {
configObjects: [][]byte{podA, podB},
expectedObjects: [][]byte{podA, podB},
},
"Basic case of inventory obj and two pods": {
configObjects: [][]byte{inventoryConfigMap, podA, podB},
expectedObjects: [][]byte{podA, podB},
},
"Basic case of inventory obj and two pods in different order": {
configObjects: [][]byte{podB, inventoryConfigMap, podA},
expectedObjects: [][]byte{podB, podA},
},
}
for tn, tc := range testCases {
t.Run(tn, func(t *testing.T) {
// Build a single file of multiple resource configs, and
// call the tested function FilterInputFile. This writes
// the passed file to the test filesystem, filtering
// the inventory object if it exists in the passed file.
in := buildMultiResourceConfig(tc.configObjects...)
err := FilterInputFile(bytes.NewReader(in), tf.GetRootDir())
if err != nil {
t.Fatalf("Unexpected error in FilterInputFile: %s", err)
}
// Retrieve the files from the test filesystem.
actualFiles, err := os.ReadDir(tf.GetRootDir())
if err != nil {
t.Fatalf("Error reading test filesystem directory: %s", err)
}
// Since we remove the generated file for each test, there should
// not be more than one file in the test filesystem.
if len(actualFiles) > 1 {
t.Fatalf("Wrong number of files (%d) in dir: %s", len(actualFiles), tf.GetRootDir())
}
// If there is a generated file, then read it into actualStr.
actualStr := ""
if len(actualFiles) != 0 {
actualFilename := actualFiles[0].Name()
defer os.Remove(actualFilename)
actual, err := os.ReadFile(actualFilename)
if err != nil {
t.Fatalf("Error reading created file (%s): %s", actualFilename, err)
}
actualStr = strings.TrimSpace(string(actual))
}
// Build the expected string from the expectedObjects. This expected
// string should not have the inventory object config in it.
expected := buildMultiResourceConfig(tc.expectedObjects...)
expectedStr := strings.TrimSpace(string(expected))
if expectedStr != actualStr {
t.Errorf("Expected file contents (%s) not equal to actual file contents (%s)",
expectedStr, actualStr)
}
})
}
}
func TestExpandDir(t *testing.T) {
tf := setupTestFilesystem(t)
defer tf.Clean()
testCases := map[string]struct {
packageDirPath string
expandedInventory string
expandedPaths []string
isError bool
}{
"empty path is error": {
packageDirPath: "",
isError: true,
},
"path that is not dir is error": {
packageDirPath: "fakedir1",
isError: true,
},
"root package dir excludes inventory object": {
packageDirPath: tf.GetRootDir(),
expandedInventory: "inventory.yaml",
expandedPaths: []string{
"pod-a.yaml",
"pod-b.yaml",
},
isError: false,
},
}
for tn, tc := range testCases {
t.Run(tn, func(t *testing.T) {
actualInventory, actualPaths, err := ExpandDir(tc.packageDirPath)
if tc.isError {
if err == nil {
t.Fatalf("expected error but received none")
}
return
}
if err != nil {
t.Fatalf("received unexpected error %#v", err)
return
}
actualFilename := filepath.Base(actualInventory)
if tc.expandedInventory != actualFilename {
t.Errorf("expected inventory template filepath (%s), got (%s)", tc.expandedInventory, actualFilename)
}
if len(tc.expandedPaths) != len(actualPaths) {
t.Errorf("expected (%d) resource filepaths, got (%d)", len(tc.expandedPaths), len(actualPaths))
}
for _, expectedPath := range tc.expandedPaths {
found := false
for _, actualPath := range actualPaths {
actualFilename := filepath.Base(actualPath)
if expectedPath == actualFilename {
found = true
break
}
}
if !found {
t.Errorf("expected filename (%s) not found", expectedPath)
}
}
})
}
}
func TestExpandDirErrors(t *testing.T) {
tf := setupTestFilesystem(t)
defer tf.Clean()
testCases := map[string]struct {
packageDirPath []string
expandedPaths []string
isError bool
}{
"empty path is error": {
packageDirPath: []string{},
isError: true,
},
"more than one path is error": {
packageDirPath: []string{"fakedir1", "fakedir2"},
isError: true,
},
"path that is not dir is error": {
packageDirPath: []string{"fakedir1"},
isError: true,
},
"root package dir excludes inventory object": {
packageDirPath: []string{tf.GetRootDir()},
expandedPaths: []string{
filepath.Join(packageDir, "pod-a.yaml"),
filepath.Join(packageDir, "pod-b.yaml"),
},
isError: false,
},
}
for tn, tc := range testCases {
t.Run(tn, func(t *testing.T) {
trueVal := true
filenameFlags := genericclioptions.FileNameFlags{
Filenames: &tc.packageDirPath,
Recursive: &trueVal,
}
actualFlags, err := ExpandPackageDir(filenameFlags)
if tc.isError && err == nil {
t.Fatalf("expected error but received none")
}
if !tc.isError {
if err != nil {
t.Fatalf("unexpected error received: %v", err)
}
actualPaths := *actualFlags.Filenames
if len(tc.expandedPaths) != len(actualPaths) {
t.Errorf("expected config filepaths (%s), got (%s)",
tc.expandedPaths, actualPaths)
}
for _, expected := range tc.expandedPaths {
if !filepathExists(expected, actualPaths) {
t.Errorf("expected config filepath (%s) in actual filepaths (%s)",
expected, actualPaths)
}
}
// Check the inventory object is not in the filename flags.
for _, actualPath := range actualPaths {
if strings.Contains(actualPath, "inventory.yaml") {
t.Errorf("inventory object should be excluded")
}
}
}
})
}
}
// filepathExists returns true if the passed "filepath" is a substring
// of any of the passed full "filepaths"; false otherwise. For example:
// if filepath = "test/a.yaml", and filepaths includes "/tmp/test/a.yaml",
// this function returns true.
func filepathExists(filepath string, filepaths []string) bool {
for _, fp := range filepaths {
if strings.Contains(fp, filepath) {
return true
}
}
return false
}