github.com/buildtool/build-tools@v0.2.29-0.20240322150259-6a1d0a553c23/pkg/kubectl/kubectl_test.go (about) 1 // MIT License 2 // 3 // Copyright (c) 2018 buildtool 4 // 5 // Permission is hereby granted, free of charge, to any person obtaining a copy 6 // of this software and associated documentation files (the "Software"), to deal 7 // in the Software without restriction, including without limitation the rights 8 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 // copies of the Software, and to permit persons to whom the Software is 10 // furnished to do so, subject to the following conditions: 11 // 12 // The above copyright notice and this permission notice shall be included in all 13 // copies or substantial portions of the Software. 14 // 15 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 // SOFTWARE. 22 23 package kubectl 24 25 import ( 26 "encoding/base64" 27 "errors" 28 "fmt" 29 "io" 30 "os" 31 "path/filepath" 32 "testing" 33 "time" 34 35 "github.com/apex/log" 36 "github.com/spf13/cobra" 37 "github.com/stretchr/testify/assert" 38 mocks "gitlab.com/unboundsoftware/apex-mocks" 39 "k8s.io/kubectl/pkg/cmd/util" 40 41 "github.com/buildtool/build-tools/pkg" 42 "github.com/buildtool/build-tools/pkg/cli" 43 "github.com/buildtool/build-tools/pkg/config" 44 ) 45 46 func TestNew(t *testing.T) { 47 logMock := mocks.New() 48 log.SetHandler(logMock) 49 log.SetLevel(log.InfoLevel) 50 k := New(&config.Target{Context: "missing", Namespace: "dev"}) 51 52 assert.Equal(t, "missing", k.(*kubectl).args["context"]) 53 assert.Equal(t, "dev", k.(*kubectl).args["namespace"]) 54 logMock.Check(t, []string{}) 55 } 56 57 func TestNew_NoNamespace(t *testing.T) { 58 logMock := mocks.New() 59 log.SetHandler(logMock) 60 log.SetLevel(log.DebugLevel) 61 62 calls = [][]string{} 63 newKubectlCmd = mockCmd 64 tempDir, _ := os.MkdirTemp(os.TempDir(), "build-tools") 65 66 k := &kubectl{args: map[string]string{"context": "missing"}, tempDir: tempDir, out: cli.NewWriter(logMock)} 67 68 err := k.Apply("") 69 assert.NoError(t, err) 70 assert.Equal(t, 1, len(calls)) 71 assert.Equal(t, []string{"apply", "--context", "missing", "--file", fmt.Sprintf("%s/content.yaml", tempDir), "--v=6", "--server-side", "--force-conflicts"}, calls[0]) 72 logMock.Check(t, []string{}) 73 } 74 func TestNew_NoContext(t *testing.T) { 75 logMock := mocks.New() 76 log.SetHandler(logMock) 77 log.SetLevel(log.InfoLevel) 78 calls = [][]string{} 79 newKubectlCmd = mockCmd 80 tempDir, _ := os.MkdirTemp(os.TempDir(), "build-tools") 81 82 k := &kubectl{args: map[string]string{"namespace": "namespace"}, tempDir: tempDir, out: cli.NewWriter(logMock)} 83 84 err := k.Apply("") 85 assert.NoError(t, err) 86 assert.Equal(t, 1, len(calls)) 87 assert.Equal(t, []string{"apply", "--namespace", "namespace", "--file", fmt.Sprintf("%s/content.yaml", tempDir), "--server-side", "--force-conflicts"}, calls[0]) 88 logMock.Check(t, []string{}) 89 } 90 91 func TestKubectl_Apply(t *testing.T) { 92 logMock := mocks.New() 93 log.SetHandler(logMock) 94 log.SetLevel(log.InfoLevel) 95 calls = [][]string{} 96 newKubectlCmd = mockCmd 97 tempDir, _ := os.MkdirTemp(os.TempDir(), "build-tools") 98 99 k := &kubectl{args: map[string]string{"context": "missing", "namespace": "default"}, tempDir: tempDir, out: cli.NewWriter(logMock)} 100 101 err := k.Apply("") 102 assert.NoError(t, err) 103 assert.Equal(t, 1, len(calls)) 104 assert.Equal(t, []string{"apply", "--context", "missing", "--namespace", "default", "--file", fmt.Sprintf("%s/content.yaml", tempDir), "--server-side", "--force-conflicts"}, calls[0]) 105 logMock.Check(t, []string{}) 106 } 107 108 func TestKubectl_UnableToCreateTempDir(t *testing.T) { 109 logMock := mocks.New() 110 log.SetHandler(logMock) 111 log.SetLevel(log.InfoLevel) 112 newKubectlCmd = mockCmd 113 114 k := &kubectl{args: nil, tempDir: "/missing", out: cli.NewWriter(logMock)} 115 116 err := k.Apply("") 117 assert.EqualError(t, err, "open /missing/content.yaml: no such file or directory") 118 logMock.Check(t, []string{}) 119 } 120 121 func TestKubectl_Target(t *testing.T) { 122 logMock := mocks.New() 123 log.SetHandler(logMock) 124 log.SetLevel(log.InfoLevel) 125 env := &config.Target{Context: "missing", Namespace: ""} 126 k := New(env) 127 128 assert.Equal(t, "", k.(*kubectl).args["namespace"]) 129 logMock.Check(t, []string{}) 130 } 131 132 func TestKubectl_DeploymentExistsTrue(t *testing.T) { 133 logMock := mocks.New() 134 log.SetHandler(logMock) 135 log.SetLevel(log.InfoLevel) 136 calls = [][]string{} 137 cmdError = nil 138 o := `NAME READY UP-TO-DATE AVAILABLE AGE 139 api 1/1 1 1 2d11h 140 ` 141 cmdOut = &o 142 newKubectlCmd = mockCmd 143 144 k := New(&config.Target{Context: "missing", Namespace: "default"}) 145 146 result := k.DeploymentExists("image") 147 assert.True(t, result) 148 assert.Equal(t, 1, len(calls)) 149 assert.Equal(t, []string{"get", "deployment", "image", "--context", "missing", "--namespace", "default", "--ignore-not-found"}, calls[0]) 150 logMock.Check(t, []string{}) 151 } 152 153 func TestKubectl_DeploymentExistsFalse(t *testing.T) { 154 logMock := mocks.New() 155 log.SetHandler(logMock) 156 log.SetLevel(log.DebugLevel) 157 calls = [][]string{} 158 e := "deployment not found" 159 cmdError = &e 160 newKubectlCmd = mockCmd 161 162 k := New(&config.Target{Context: "missing", Namespace: "default"}) 163 164 result := k.DeploymentExists("image") 165 assert.False(t, result) 166 assert.Equal(t, 1, len(calls)) 167 assert.Equal(t, []string{"get", "deployment", "image", "--context", "missing", "--namespace", "default", "--ignore-not-found", "--v=6"}, calls[0]) 168 logMock.Check(t, []string{"debug: kubectl --context missing --namespace default --v=6 get deployment image --ignore-not-found\n"}) 169 } 170 171 func TestKubectl_RolloutStatusSuccess(t *testing.T) { 172 logMock := mocks.New() 173 log.SetHandler(logMock) 174 log.SetLevel(log.InfoLevel) 175 calls = [][]string{} 176 cmdOut = nil 177 cmdError = nil 178 newKubectlCmd = mockCmd 179 180 k := New(&config.Target{Context: "missing", Namespace: "other"}) 181 182 result := k.RolloutStatus("image", "2m") 183 assert.True(t, result) 184 assert.Equal(t, 1, len(calls)) 185 assert.Equal(t, []string{"rollout", "status", "deployment", "image", "--context", "missing", "--namespace", "other", "--timeout", "2m0s"}, calls[0]) 186 logMock.Check(t, []string{}) 187 } 188 189 func TestKubectl_RolloutStatusFailure(t *testing.T) { 190 logMock := mocks.New() 191 log.SetHandler(logMock) 192 log.SetLevel(log.InfoLevel) 193 calls = [][]string{} 194 e := "rollout failed" 195 cmdError = &e 196 newKubectlCmd = mockCmd 197 198 k := New(&config.Target{Context: "missing", Namespace: "default"}) 199 200 result := k.RolloutStatus("image", "2m") 201 assert.False(t, result) 202 assert.Equal(t, 1, len(calls)) 203 assert.Equal(t, []string{"rollout", "status", "deployment", "image", "--context", "missing", "--namespace", "default", "--timeout", "2m0s"}, calls[0]) 204 logMock.Check(t, []string{}) 205 } 206 207 func TestKubectl_RolloutStatusFatal(t *testing.T) { 208 logMock := mocks.New() 209 log.SetHandler(logMock) 210 log.SetLevel(log.InfoLevel) 211 calls = [][]string{} 212 e := "rollout failed" 213 cmdError = &e 214 fatal = true 215 defer func() { fatal = false }() 216 217 newKubectlCmd = mockCmd 218 219 k := New(&config.Target{Context: "missing", Namespace: "default"}) 220 221 result := k.RolloutStatus("image", "3m") 222 assert.False(t, result) 223 assert.Equal(t, 1, len(calls)) 224 assert.Equal(t, []string{"rollout", "status", "deployment", "image", "--context", "missing", "--namespace", "default", "--timeout", "3m0s"}, calls[0]) 225 logMock.Check(t, []string{}) 226 } 227 228 func TestKubectl_KubeconfigSet(t *testing.T) { 229 yaml := `contexts: 230 - context: 231 cluster: k8s.prod 232 user: user@example.org 233 ` 234 defer pkg.SetEnv(envKubeconfigContent, yaml)() 235 k := New(&config.Target{}) 236 237 kubeconfigFile := filepath.Join(k.(*kubectl).tempDir, "kubeconfig") 238 fileContent, err := os.ReadFile(kubeconfigFile) 239 assert.NoError(t, err) 240 assert.Equal(t, "contexts:\n- context:\n cluster: k8s.prod\n user: user@example.org\n", string(fileContent)) 241 assert.Equal(t, kubeconfigFile, k.(*kubectl).args["kubeconfig"]) 242 k.Cleanup() 243 } 244 245 func TestKubectl_KubeconfigSetToEmptyValue(t *testing.T) { 246 yaml := `` 247 defer pkg.SetEnv(envKubeconfigContent, yaml)() 248 k := New(&config.Target{}) 249 250 assert.Equal(t, "", k.(*kubectl).args["kubeconfig"]) 251 k.Cleanup() 252 } 253 254 func TestKubectl_KubeconfigBase64Set(t *testing.T) { 255 yaml := `contexts: 256 - context: 257 cluster: k8s.prod 258 user: user@example.org 259 ` 260 defer pkg.SetEnv(envKubeconfigContent, base64.StdEncoding.EncodeToString([]byte(yaml)))() 261 k := New(&config.Target{}) 262 263 kubeconfigFile := filepath.Join(k.(*kubectl).tempDir, "kubeconfig") 264 fileContent, err := os.ReadFile(kubeconfigFile) 265 assert.NoError(t, err) 266 assert.Equal(t, "contexts:\n- context:\n cluster: k8s.prod\n user: user@example.org\n", string(fileContent)) 267 assert.Equal(t, kubeconfigFile, k.(*kubectl).args["kubeconfig"]) 268 k.Cleanup() 269 } 270 271 func TestKubectl_KubeconfigExistingFile(t *testing.T) { 272 273 name, _ := os.CreateTemp(os.TempDir(), "kubecontent") 274 defer func() { 275 _ = os.Remove(name.Name()) 276 }() 277 278 k := New(&config.Target{Kubeconfig: name.Name()}) 279 assert.Equal(t, name.Name(), k.(*kubectl).args["kubeconfig"]) 280 k.Cleanup() 281 } 282 283 func TestKubectl_DeploymentEvents_Error(t *testing.T) { 284 logMock := mocks.New() 285 log.SetHandler(logMock) 286 log.SetLevel(log.InfoLevel) 287 calls = [][]string{} 288 newKubectlCmd = mockCmd 289 e := "deployment not found" 290 cmdError = &e 291 292 k := New(&config.Target{Context: "missing", Namespace: "default"}) 293 294 result := k.DeploymentEvents("image") 295 assert.Equal(t, "deployment not found", result) 296 assert.Equal(t, 1, len(calls)) 297 assert.Equal(t, []string{"describe", "deployment", "image", "--context", "missing", "--namespace", "default", "--show-events", "true"}, calls[0]) 298 logMock.Check(t, []string{}) 299 } 300 301 func TestKubectl_DeploymentEvents_NoEvents(t *testing.T) { 302 logMock := mocks.New() 303 log.SetHandler(logMock) 304 log.SetLevel(log.InfoLevel) 305 calls = [][]string{} 306 cmdError = nil 307 newKubectlCmd = mockCmd 308 e := ` 309 Name: gpe-core 310 Namespace: default 311 Events: <none> 312 ` 313 cmdOut = &e 314 315 k := New(&config.Target{Context: "missing", Namespace: "default"}) 316 317 result := k.DeploymentEvents("image") 318 assert.Equal(t, "", result) 319 assert.Equal(t, 1, len(calls)) 320 assert.Equal(t, []string{"describe", "deployment", "image", "--context", "missing", "--namespace", "default", "--show-events", "true"}, calls[0]) 321 logMock.Check(t, []string{}) 322 } 323 324 func TestKubectl_DeploymentEvents_SomeEvents(t *testing.T) { 325 logMock := mocks.New() 326 log.SetHandler(logMock) 327 log.SetLevel(log.InfoLevel) 328 calls = [][]string{} 329 cmdError = nil 330 newKubectlCmd = mockCmd 331 e := ` 332 Name: gpe-core 333 Namespace: default 334 Events: 335 Type Reason Age From Message 336 ---- ------ ---- ---- ------- 337 Normal ScalingReplicaSet 9m deployment-controller Scaled up replica set gpe-core-5cb459ff7d to 1 338 Normal ScalingReplicaSet 9m deployment-controller Scaled down replica set gpe-core-7fc44679dc to 0 339 Normal ScalingReplicaSet 61s deployment-controller Scaled up replica set gpe-core-c8798ff88 to 1 340 Normal ScalingReplicaSet 61s deployment-controller Scaled down replica set gpe-core-5cb459ff7d to 0 341 ` 342 cmdOut = &e 343 344 k := New(&config.Target{Context: "missing", Namespace: "default"}) 345 346 result := k.DeploymentEvents("image") 347 assert.Equal(t, "Events:\n Type Reason Age From Message\n ---- ------ ---- ---- -------\n Normal ScalingReplicaSet 9m deployment-controller Scaled up replica set gpe-core-5cb459ff7d to 1\n Normal ScalingReplicaSet 9m deployment-controller Scaled down replica set gpe-core-7fc44679dc to 0\n Normal ScalingReplicaSet 61s deployment-controller Scaled up replica set gpe-core-c8798ff88 to 1\n Normal ScalingReplicaSet 61s deployment-controller Scaled down replica set gpe-core-5cb459ff7d to 0\n", result) 348 assert.Equal(t, 1, len(calls)) 349 assert.Equal(t, []string{"describe", "deployment", "image", "--context", "missing", "--namespace", "default", "--show-events", "true"}, calls[0]) 350 logMock.Check(t, []string{}) 351 } 352 353 func TestKubectl_PodEvents_Error(t *testing.T) { 354 logMock := mocks.New() 355 log.SetHandler(logMock) 356 log.SetLevel(log.InfoLevel) 357 calls = [][]string{} 358 newKubectlCmd = mockCmd 359 e := "pod not found" 360 cmdError = &e 361 362 k := New(&config.Target{Context: "missing", Namespace: "default"}) 363 364 result := k.PodEvents("image") 365 assert.Equal(t, "pod not found", result) 366 assert.Equal(t, 1, len(calls)) 367 assert.Equal(t, []string{"describe", "pods", "--context", "missing", "--namespace", "default", "--show-events", "true", "--selector", "app=image"}, calls[0]) 368 logMock.Check(t, []string{}) 369 } 370 371 func TestKubectl_PodEvents_NoEvents(t *testing.T) { 372 logMock := mocks.New() 373 log.SetHandler(logMock) 374 log.SetLevel(log.InfoLevel) 375 calls = [][]string{} 376 cmdError = nil 377 newKubectlCmd = mockCmd 378 e := ` 379 Name: gpe-core 380 Namespace: default 381 Events: <none> 382 ` 383 cmdOut = &e 384 385 k := New(&config.Target{Context: "missing", Namespace: "default"}) 386 387 result := k.PodEvents("image") 388 assert.Equal(t, "", result) 389 assert.Equal(t, 1, len(calls)) 390 assert.Equal(t, []string{"describe", "pods", "--context", "missing", "--namespace", "default", "--show-events", "true", "--selector", "app=image"}, calls[0]) 391 logMock.Check(t, []string{}) 392 } 393 394 func TestKubectl_PodEvents_SomeEvents(t *testing.T) { 395 logMock := mocks.New() 396 log.SetHandler(logMock) 397 log.SetLevel(log.InfoLevel) 398 calls = [][]string{} 399 cmdError = nil 400 newKubectlCmd = mockCmd 401 e := ` 402 Events: 403 Type Reason Age From Message 404 ---- ------ ---- ---- ------- 405 Normal Scheduled 61s default-scheduler Successfully assigned dev/gpe-core-c8798ff88-674tr to some-ip-somewhere 406 Normal Pulling 10s (x4 over 60s) kubelet, some-ip-somewhere pulling image "quay.io/somewhere/gpe-core:9cdb0243e82b9bfdf037627d9d59cbfcbf55406c" 407 Normal Pulled 9s (x4 over 57s) kubelet, some-ip-somewhere Successfully pulled image "quay.io/somewhere/gpe-core:9cdb0243e82b9bfdf037627d9d59cbfcbf55406c" 408 Normal Created 8s (x4 over 57s) kubelet, some-ip-somewhere Created container 409 Normal Started 8s (x4 over 57s) kubelet, some-ip-somewhere Started container 410 Warning BackOff 8s (x5 over 54s) kubelet, some-ip-somewhere Back-off restarting failed container` 411 cmdOut = &e 412 413 k := New(&config.Target{Context: "missing", Namespace: "default"}) 414 415 result := k.PodEvents("image") 416 assert.Equal(t, "Events:\n Type Reason Age From Message\n ---- ------ ---- ---- -------\n Normal Scheduled 61s default-scheduler Successfully assigned dev/gpe-core-c8798ff88-674tr to some-ip-somewhere\n Normal Pulling 10s (x4 over 60s) kubelet, some-ip-somewhere pulling image \"quay.io/somewhere/gpe-core:9cdb0243e82b9bfdf037627d9d59cbfcbf55406c\"\n Normal Pulled 9s (x4 over 57s) kubelet, some-ip-somewhere Successfully pulled image \"quay.io/somewhere/gpe-core:9cdb0243e82b9bfdf037627d9d59cbfcbf55406c\"\n Normal Created 8s (x4 over 57s) kubelet, some-ip-somewhere Created container\n Normal Started 8s (x4 over 57s) kubelet, some-ip-somewhere Started container\n Warning BackOff 8s (x5 over 54s) kubelet, some-ip-somewhere Back-off restarting failed container\n", result) 417 assert.Equal(t, 1, len(calls)) 418 assert.Equal(t, []string{"describe", "pods", "--context", "missing", "--namespace", "default", "--show-events", "true", "--selector", "app=image"}, calls[0]) 419 logMock.Check(t, []string{}) 420 } 421 422 var calls [][]string 423 var cmdError *string 424 var cmdOut *string 425 var fatal = false 426 427 func mockCmd(_ io.Reader, out, _ io.Writer, args []string) *cobra.Command { 428 var ctx, ns, file *string 429 var timeout *time.Duration 430 var showEvents *bool 431 var ignoreNotFound *bool 432 var selector *string 433 var kubeconfig *string 434 var verbose *string 435 var serverSide *bool 436 var forceConflicts *bool 437 438 cmd := cobra.Command{ 439 Use: "kubectl", 440 Args: func(cmd *cobra.Command, args []string) error { 441 var call = args 442 if *ctx != "" { 443 call = append(call, "--context", *ctx) 444 } 445 if *kubeconfig != "" { 446 call = append(call, "--kubeconfig", *kubeconfig) 447 } 448 if *ns != "" { 449 call = append(call, "--namespace", *ns) 450 } 451 if *file != "" { 452 call = append(call, "--file", *file) 453 } 454 if *timeout != time.Duration(0) { 455 call = append(call, "--timeout", timeout.String()) 456 } 457 if *showEvents { 458 call = append(call, "--show-events", "true") 459 } 460 if *ignoreNotFound { 461 call = append(call, "--ignore-not-found") 462 } 463 if *verbose != "0" { 464 call = append(call, fmt.Sprintf("--v=%s", *verbose)) 465 } 466 if *selector != "" { 467 call = append(call, "--selector", fmt.Sprintf("%v", *selector)) 468 } 469 if *serverSide { 470 call = append(call, "--server-side") 471 } 472 if *forceConflicts { 473 call = append(call, "--force-conflicts") 474 } 475 calls = append(calls, call) 476 return nil 477 }, 478 RunE: func(cmd *cobra.Command, args []string) error { 479 if fatal { 480 util.CheckErr(errors.New(*cmdError)) 481 } 482 if cmdError != nil { 483 return errors.New(*cmdError) 484 } 485 if cmdOut != nil { 486 _, _ = out.Write([]byte(*cmdOut)) 487 } 488 return nil 489 }, 490 } 491 492 ctx = cmd.Flags().StringP("context", "c", "", "") 493 ns = cmd.Flags().StringP("namespace", "n", "", "") 494 file = cmd.Flags().StringP("filename", "f", "", "") 495 timeout = cmd.Flags().DurationP("timeout", "t", 0*time.Second, "") 496 showEvents = cmd.Flags().BoolP("show-events", "", false, "") 497 ignoreNotFound = cmd.Flags().BoolP("ignore-not-found", "", false, "") 498 selector = cmd.Flags().StringP("selector", "l", "", "") 499 kubeconfig = cmd.Flags().StringP("kubeconfig", "", "", "") 500 verbose = cmd.Flags().StringP("v", "v", "0", "") 501 serverSide = cmd.Flags().BoolP("server-side", "", false, "") 502 forceConflicts = cmd.Flags().BoolP("force-conflicts", "", false, "") 503 cmd.SetArgs(args) 504 return &cmd 505 }