1
2
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
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()
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
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()
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