...

Source file src/sigs.k8s.io/cli-utils/pkg/config/initoptions_test.go

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

     1  // Copyright 2020 The Kubernetes Authors.
     2  // SPDX-License-Identifier: Apache-2.0
     3  
     4  package config
     5  
     6  import (
     7  	"fmt"
     8  	"os"
     9  	"path/filepath"
    10  	"regexp"
    11  	"strings"
    12  	"testing"
    13  
    14  	"github.com/stretchr/testify/assert"
    15  	"k8s.io/cli-runtime/pkg/genericclioptions"
    16  	cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
    17  )
    18  
    19  // writeFile writes a file under the test directory
    20  func writeFile(t *testing.T, path string, value []byte) {
    21  	err := os.WriteFile(path, value, 0600)
    22  	if !assert.NoError(t, err) {
    23  		assert.FailNow(t, err.Error())
    24  	}
    25  }
    26  
    27  var readFileA = []byte(`
    28  apiVersion: v1
    29  kind: Pod
    30  metadata:
    31    name: objA
    32    namespace: namespaceA
    33  `)
    34  
    35  var readFileB = []byte(`
    36  apiVersion: v1
    37  kind: Pod
    38  metadata:
    39    name: objB
    40    namespace: namespaceB
    41  `)
    42  
    43  var readFileC = []byte(`
    44  apiVersion: v1
    45  kind: Pod
    46  metadata:
    47    name: objC
    48  `)
    49  
    50  var readFileD = []byte(`
    51  apiVersion: v1
    52  kind: Pod
    53  metadata:
    54    name: objD
    55    namespace: namespaceD
    56    annotations:
    57      config.kubernetes.io/local-config: "true"
    58  `)
    59  
    60  var readFileE = []byte(`
    61  apiVersion: v1
    62  kind: Pod
    63  metadata:
    64    name: objE
    65    namespace: namespaceA
    66  `)
    67  
    68  var readFileF = []byte(`
    69  apiVersion: v1
    70  kind: Namespace
    71  metadata:
    72    name: namespaceA
    73  `)
    74  
    75  func TestComplete(t *testing.T) {
    76  	tests := map[string]struct {
    77  		args               []string
    78  		files              map[string][]byte
    79  		isError            bool
    80  		expectedErrMessage string
    81  		expectedNamespace  string
    82  	}{
    83  		"Empty args returns error": {
    84  			args:               []string{},
    85  			isError:            true,
    86  			expectedErrMessage: "need one 'directory' arg; have 0",
    87  		},
    88  		"More than one argument should fail": {
    89  			args:               []string{"foo", "bar"},
    90  			isError:            true,
    91  			expectedErrMessage: "need one 'directory' arg; have 2",
    92  		},
    93  		"Non-directory arg should fail": {
    94  			args:               []string{"foo"},
    95  			isError:            true,
    96  			expectedErrMessage: "invalid directory argument: foo",
    97  		},
    98  		"More than one namespace should fail": {
    99  			args: []string{},
   100  			files: map[string][]byte{
   101  				"a_test.yaml": readFileA,
   102  				"b_test.yaml": readFileB,
   103  			},
   104  			isError:            true,
   105  			expectedErrMessage: "resources belong to different namespaces",
   106  		},
   107  		"If at least one resource doesn't have namespace, it should use the default": {
   108  			args: []string{},
   109  			files: map[string][]byte{
   110  				"b_test.yaml": readFileB,
   111  				"c_test.yaml": readFileC,
   112  			},
   113  			isError:           false,
   114  			expectedNamespace: "foo",
   115  		},
   116  		"No resources without namespace should use the default namespace": {
   117  			args: []string{},
   118  			files: map[string][]byte{
   119  				"c_test.yaml": readFileC,
   120  			},
   121  			isError:           false,
   122  			expectedNamespace: "foo",
   123  		},
   124  		"Resources with the LocalConfig annotation should be ignored": {
   125  			args: []string{},
   126  			files: map[string][]byte{
   127  				"b_test.yaml": readFileB,
   128  				"d_test.yaml": readFileD,
   129  			},
   130  			isError:           false,
   131  			expectedNamespace: "foo",
   132  		},
   133  		"If all resources have the LocalConfig annotation use the default namespace": {
   134  			args: []string{},
   135  			files: map[string][]byte{
   136  				"d_test.yaml": readFileD,
   137  			},
   138  			isError:           false,
   139  			expectedNamespace: "foo",
   140  		},
   141  		"Cluster-scoped resources are ignored in namespace calculation": {
   142  			args: []string{},
   143  			files: map[string][]byte{
   144  				"a_test.yaml": readFileA,
   145  				"e_test.yaml": readFileE,
   146  				"f_test.yaml": readFileF,
   147  			},
   148  			isError:           false,
   149  			expectedNamespace: "foo",
   150  		},
   151  	}
   152  	for name, tc := range tests {
   153  		t.Run(name, func(t *testing.T) {
   154  			var err error
   155  			dir, err := os.MkdirTemp("", "test-dir")
   156  			if !assert.NoError(t, err) {
   157  				assert.FailNow(t, err.Error())
   158  			}
   159  			defer os.RemoveAll(dir)
   160  
   161  			for fileName, fileContent := range tc.files {
   162  				writeFile(t, filepath.Join(dir, fileName), fileContent)
   163  			}
   164  			if len(tc.files) > 0 {
   165  				tc.args = append(tc.args, dir)
   166  			}
   167  
   168  			tf := cmdtesting.NewTestFactory().WithNamespace("foo")
   169  			defer tf.Cleanup()
   170  			ioStreams, _, out, _ := genericclioptions.NewTestIOStreams()
   171  			io := NewInitOptions(tf, ioStreams)
   172  			err = io.Complete(tc.args)
   173  
   174  			if err != nil {
   175  				if !tc.isError {
   176  					t.Errorf("Expected error, but did not receive one")
   177  					return
   178  				}
   179  				assert.Contains(t, err.Error(), tc.expectedErrMessage)
   180  				return
   181  			}
   182  			assert.Contains(t, out.String(), tc.expectedNamespace)
   183  		})
   184  	}
   185  }
   186  
   187  func TestFindNamespace(t *testing.T) {
   188  	testCases := map[string]struct {
   189  		namespace         string
   190  		enforceNamespace  bool
   191  		files             map[string][]byte
   192  		expectedNamespace string
   193  	}{
   194  		"fallback to default": {
   195  			namespace:        "foo",
   196  			enforceNamespace: false,
   197  			files: map[string][]byte{
   198  				"a_test.yaml": readFileA,
   199  				"b_test.yaml": readFileB,
   200  			},
   201  			expectedNamespace: "foo",
   202  		},
   203  		"enforce namespace": {
   204  			namespace:        "bar",
   205  			enforceNamespace: true,
   206  			files: map[string][]byte{
   207  				"a_test.yaml": readFileA,
   208  			},
   209  			expectedNamespace: "bar",
   210  		},
   211  		"use namespace from resource if all the same": {
   212  			namespace:        "bar",
   213  			enforceNamespace: false,
   214  			files: map[string][]byte{
   215  				"a_test.yaml": readFileA,
   216  			},
   217  			expectedNamespace: "namespaceA",
   218  		},
   219  	}
   220  
   221  	for tn, tc := range testCases {
   222  		t.Run(tn, func(t *testing.T) {
   223  			var err error
   224  			dir, err := os.MkdirTemp("", "test-dir")
   225  			if !assert.NoError(t, err) {
   226  				assert.FailNow(t, err.Error())
   227  			}
   228  			defer os.RemoveAll(dir)
   229  
   230  			for fileName, fileContent := range tc.files {
   231  				writeFile(t, filepath.Join(dir, fileName), fileContent)
   232  			}
   233  
   234  			fakeLoader := &fakeNamespaceLoader{
   235  				namespace:        tc.namespace,
   236  				enforceNamespace: tc.enforceNamespace,
   237  			}
   238  
   239  			namespace, err := FindNamespace(fakeLoader, dir)
   240  			assert.NoError(t, err)
   241  			assert.Equal(t, tc.expectedNamespace, namespace)
   242  		})
   243  	}
   244  }
   245  
   246  type fakeNamespaceLoader struct {
   247  	namespace        string
   248  	enforceNamespace bool
   249  }
   250  
   251  func (f *fakeNamespaceLoader) Namespace() (string, bool, error) {
   252  	return f.namespace, f.enforceNamespace, nil
   253  }
   254  
   255  func TestDefaultInventoryID(t *testing.T) {
   256  	tf := cmdtesting.NewTestFactory().WithNamespace("foo")
   257  	defer tf.Cleanup()
   258  	ioStreams, _, _, _ := genericclioptions.NewTestIOStreams() //nolint:dogsled
   259  	io := NewInitOptions(tf, ioStreams)
   260  	actual, err := io.defaultInventoryID()
   261  	if err != nil {
   262  		t.Errorf("Unxpected error during UUID generation: %v", err)
   263  	}
   264  	// Example UUID: dd647113-a354-48fa-9b93-cc1b7a85aadb
   265  	var uuidRegexp = `^[a-z0-9]{8}\-[a-z0-9]{4}\-[a-z0-9]{4}\-[a-z0-9]{4}\-[a-z0-9]{12}$`
   266  	re := regexp.MustCompile(uuidRegexp)
   267  	if !re.MatchString(actual) {
   268  		t.Errorf("Expected UUID; got (%s)", actual)
   269  	}
   270  }
   271  
   272  func TestValidateInventoryID(t *testing.T) {
   273  	tests := map[string]struct {
   274  		inventoryID string
   275  		isValid     bool
   276  	}{
   277  		"Empty InventoryID fails": {
   278  			inventoryID: "",
   279  			isValid:     false,
   280  		},
   281  		"InventoryID greater than sixty-three chars fails": {
   282  			inventoryID: "88888888888888888888888888888888888888888888888888888888888888888",
   283  			isValid:     false,
   284  		},
   285  		"Non-allowed characters fails": {
   286  			inventoryID: "&foo",
   287  			isValid:     false,
   288  		},
   289  		"Initial dot fails": {
   290  			inventoryID: ".foo",
   291  			isValid:     false,
   292  		},
   293  		"Initial dash fails": {
   294  			inventoryID: "-foo",
   295  			isValid:     false,
   296  		},
   297  		"Initial underscore fails": {
   298  			inventoryID: "_foo",
   299  			isValid:     false,
   300  		},
   301  		"Trailing dot fails": {
   302  			inventoryID: "foo.",
   303  			isValid:     false,
   304  		},
   305  		"Trailing dash fails": {
   306  			inventoryID: "foo-",
   307  			isValid:     false,
   308  		},
   309  		"Trailing underscore fails": {
   310  			inventoryID: "foo_",
   311  			isValid:     false,
   312  		},
   313  		"Initial digit succeeds": {
   314  			inventoryID: "90-foo.bar_test",
   315  			isValid:     true,
   316  		},
   317  		"Allowed characters succeed": {
   318  			inventoryID: "f_oo90bar-t.est90",
   319  			isValid:     true,
   320  		},
   321  	}
   322  
   323  	for name, tc := range tests {
   324  		t.Run(name, func(t *testing.T) {
   325  			actualValid := validateInventoryID(tc.inventoryID)
   326  			if tc.isValid != actualValid {
   327  				t.Errorf("InventoryID: %s. Expected valid (%t), got (%t)", tc.inventoryID, tc.isValid, actualValid)
   328  			}
   329  		})
   330  	}
   331  }
   332  
   333  func TestFillInValues(t *testing.T) {
   334  	tests := map[string]struct {
   335  		namespace   string
   336  		inventoryID string
   337  	}{
   338  		"Basic namespace/inventoryID": {
   339  			namespace:   "foo",
   340  			inventoryID: "bar",
   341  		},
   342  	}
   343  
   344  	for name, tc := range tests {
   345  		t.Run(name, func(t *testing.T) {
   346  			tf := cmdtesting.NewTestFactory().WithNamespace("foo")
   347  			defer tf.Cleanup()
   348  			ioStreams, _, _, _ := genericclioptions.NewTestIOStreams() //nolint:dogsled
   349  			io := NewInitOptions(tf, ioStreams)
   350  			io.Namespace = tc.namespace
   351  			io.InventoryID = tc.inventoryID
   352  			actual := io.fillInValues()
   353  			expectedLabel := fmt.Sprintf("cli-utils.sigs.k8s.io/inventory-id: %s", tc.inventoryID)
   354  			if !strings.Contains(actual, expectedLabel) {
   355  				t.Errorf("\nExpected label (%s) not found in inventory object: %s\n", expectedLabel, actual)
   356  			}
   357  			expectedNamespace := fmt.Sprintf("namespace: %s", tc.namespace)
   358  			if !strings.Contains(actual, expectedNamespace) {
   359  				t.Errorf("\nExpected namespace (%s) not found in inventory object: %s\n", expectedNamespace, actual)
   360  			}
   361  			matched, err := regexp.MatchString(`name: inventory-\d{8}\n`, actual)
   362  			if err != nil {
   363  				t.Errorf("unexpected error parsing inventory name: %s", err)
   364  			}
   365  			if !matched {
   366  				t.Errorf("expected inventory name (e.g. inventory-12345678), got (%s)", actual)
   367  			}
   368  			if !strings.Contains(actual, "kind: ConfigMap") {
   369  				t.Errorf("\nExpected `kind: ConfigMap` not found in inventory object: %s\n", actual)
   370  			}
   371  		})
   372  	}
   373  }
   374  

View as plain text