Skip to content

Commit 0480641

Browse files
authored
Set a request timeout on kubectl commands (#360)
* feat: add kubectl timeout value to configuration Signed-off-by: Scott Leggett <[email protected]> * fix: set a request timeout on kubectl commands Simple kubectl commands such as get, describe, and logs should return quickly, but kubectl does not set a timeout on requests by default. This can mean kubectl hangs forever if the kubernetes API stops responding during a request, which is quite possible in CI environments where jobs may cancelled and kubernetes clusters torn down while ct is running. This change sets a configurable timeout on such kubectl commands, with a default value of 30s. Signed-off-by: Scott Leggett <[email protected]> * chore: update integration test for new kubectl API Signed-off-by: Scott Leggett <[email protected]>
1 parent 882d4be commit 0480641

File tree

7 files changed

+94
-47
lines changed

7 files changed

+94
-47
lines changed

pkg/chart/chart.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ func NewTesting(config config.Configuration, extraSetArgs string) (Testing, erro
263263
config: config,
264264
helm: tool.NewHelm(procExec, extraArgs, strings.Fields(extraSetArgs)),
265265
git: tool.NewGit(procExec),
266-
kubectl: tool.NewKubectl(procExec),
266+
kubectl: tool.NewKubectl(procExec, config.KubectlTimeout),
267267
linter: tool.NewLinter(procExec),
268268
cmdExecutor: tool.NewCmdTemplateExecutor(procExec),
269269
accountValidator: tool.AccountValidator{},

pkg/chart/integration_test.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"fmt"
2222
"strings"
2323
"testing"
24+
"time"
2425

2526
"github.com/helm/chart-testing/v3/pkg/config"
2627
"github.com/helm/chart-testing/v3/pkg/exec"
@@ -41,7 +42,7 @@ func newTestingHelmIntegration(cfg config.Configuration, extraSetArgs string) Te
4142
accountValidator: fakeAccountValidator{},
4243
linter: fakeMockLinter,
4344
helm: tool.NewHelm(procExec, extraArgs, strings.Fields(extraSetArgs)),
44-
kubectl: tool.NewKubectl(procExec),
45+
kubectl: tool.NewKubectl(procExec, 30*time.Second),
4546
}
4647
}
4748

pkg/config/config.go

+29-25
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"path/filepath"
2121
"reflect"
2222
"strings"
23+
"time"
2324

2425
"github.com/mitchellh/go-homedir"
2526

@@ -41,36 +42,39 @@ var (
4142
)
4243

4344
type Configuration struct {
44-
Remote string `mapstructure:"remote"`
45-
TargetBranch string `mapstructure:"target-branch"`
46-
Since string `mapstructure:"since"`
47-
BuildId string `mapstructure:"build-id"`
48-
LintConf string `mapstructure:"lint-conf"`
49-
ChartYamlSchema string `mapstructure:"chart-yaml-schema"`
50-
ValidateMaintainers bool `mapstructure:"validate-maintainers"`
51-
ValidateChartSchema bool `mapstructure:"validate-chart-schema"`
52-
ValidateYaml bool `mapstructure:"validate-yaml"`
53-
AdditionalCommands []string `mapstructure:"additional-commands"`
54-
CheckVersionIncrement bool `mapstructure:"check-version-increment"`
55-
ProcessAllCharts bool `mapstructure:"all"`
56-
Charts []string `mapstructure:"charts"`
57-
ChartRepos []string `mapstructure:"chart-repos"`
58-
ChartDirs []string `mapstructure:"chart-dirs"`
59-
ExcludedCharts []string `mapstructure:"excluded-charts"`
60-
HelmExtraArgs string `mapstructure:"helm-extra-args"`
61-
HelmRepoExtraArgs []string `mapstructure:"helm-repo-extra-args"`
62-
HelmDependencyExtraArgs []string `mapstructure:"helm-dependency-extra-args"`
63-
Debug bool `mapstructure:"debug"`
64-
Upgrade bool `mapstructure:"upgrade"`
65-
SkipMissingValues bool `mapstructure:"skip-missing-values"`
66-
Namespace string `mapstructure:"namespace"`
67-
ReleaseLabel string `mapstructure:"release-label"`
68-
ExcludeDeprecated bool `mapstructure:"exclude-deprecated"`
45+
Remote string `mapstructure:"remote"`
46+
TargetBranch string `mapstructure:"target-branch"`
47+
Since string `mapstructure:"since"`
48+
BuildId string `mapstructure:"build-id"`
49+
LintConf string `mapstructure:"lint-conf"`
50+
ChartYamlSchema string `mapstructure:"chart-yaml-schema"`
51+
ValidateMaintainers bool `mapstructure:"validate-maintainers"`
52+
ValidateChartSchema bool `mapstructure:"validate-chart-schema"`
53+
ValidateYaml bool `mapstructure:"validate-yaml"`
54+
AdditionalCommands []string `mapstructure:"additional-commands"`
55+
CheckVersionIncrement bool `mapstructure:"check-version-increment"`
56+
ProcessAllCharts bool `mapstructure:"all"`
57+
Charts []string `mapstructure:"charts"`
58+
ChartRepos []string `mapstructure:"chart-repos"`
59+
ChartDirs []string `mapstructure:"chart-dirs"`
60+
ExcludedCharts []string `mapstructure:"excluded-charts"`
61+
HelmExtraArgs string `mapstructure:"helm-extra-args"`
62+
HelmRepoExtraArgs []string `mapstructure:"helm-repo-extra-args"`
63+
HelmDependencyExtraArgs []string `mapstructure:"helm-dependency-extra-args"`
64+
Debug bool `mapstructure:"debug"`
65+
Upgrade bool `mapstructure:"upgrade"`
66+
SkipMissingValues bool `mapstructure:"skip-missing-values"`
67+
Namespace string `mapstructure:"namespace"`
68+
ReleaseLabel string `mapstructure:"release-label"`
69+
ExcludeDeprecated bool `mapstructure:"exclude-deprecated"`
70+
KubectlTimeout time.Duration `mapstructure:"kubectl-timeout"`
6971
}
7072

7173
func LoadConfiguration(cfgFile string, cmd *cobra.Command, printConfig bool) (*Configuration, error) {
7274
v := viper.New()
7375

76+
v.SetDefault("kubectl-timeout", time.Duration(30*time.Second))
77+
7478
cmd.Flags().VisitAll(func(flag *flag.Flag) {
7579
flagName := flag.Name
7680
if flagName != "config" && flagName != "help" {

pkg/config/config_test.go

+2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"os"
1919
"path/filepath"
2020
"testing"
21+
"time"
2122

2223
"github.com/spf13/cobra"
2324
"github.com/stretchr/testify/assert"
@@ -57,6 +58,7 @@ func loadAndAssertConfigFromFile(t *testing.T, configFile string) {
5758
require.Equal(t, "default", cfg.Namespace)
5859
require.Equal(t, "release", cfg.ReleaseLabel)
5960
require.Equal(t, true, cfg.ExcludeDeprecated)
61+
require.Equal(t, 120*time.Second, cfg.KubectlTimeout)
6062
}
6163

6264
func Test_findConfigFile(t *testing.T) {

pkg/config/test_config.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,6 @@
2929
"skip-missing-values": true,
3030
"namespace": "default",
3131
"release-label": "release",
32-
"exclude-deprecated": true
32+
"exclude-deprecated": true,
33+
"kubectl-timeout": "120s"
3334
}

pkg/config/test_config.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,4 @@ skip-missing-values: true
2525
namespace: default
2626
release-label: release
2727
exclude-deprecated: true
28+
kubectl-timeout: 120s

pkg/tool/kubectl.go

+57-19
Original file line numberDiff line numberDiff line change
@@ -14,35 +14,46 @@ import (
1414
)
1515

1616
type Kubectl struct {
17-
exec exec.ProcessExecutor
17+
exec exec.ProcessExecutor
18+
timeout time.Duration
1819
}
1920

20-
func NewKubectl(exec exec.ProcessExecutor) Kubectl {
21+
func NewKubectl(exec exec.ProcessExecutor, timeout time.Duration) Kubectl {
2122
return Kubectl{
22-
exec: exec,
23+
exec: exec,
24+
timeout: timeout,
2325
}
2426
}
2527

2628
// CreateNamespace creates a new namespace with the given name.
2729
func (k Kubectl) CreateNamespace(namespace string) error {
2830
fmt.Printf("Creating namespace '%s'...\n", namespace)
29-
return k.exec.RunProcess("kubectl", "create", "namespace", namespace)
31+
return k.exec.RunProcess("kubectl",
32+
fmt.Sprintf("--request-timeout=%s", k.timeout),
33+
"create", "namespace", namespace)
3034
}
3135

3236
// DeleteNamespace deletes the specified namespace. If the namespace does not terminate within 120s, pods running in the
3337
// namespace and, eventually, the namespace itself are force-deleted.
3438
func (k Kubectl) DeleteNamespace(namespace string) {
3539
fmt.Printf("Deleting namespace '%s'...\n", namespace)
3640
timeoutSec := "180s"
37-
if err := k.exec.RunProcess("kubectl", "delete", "namespace", namespace, "--timeout", timeoutSec); err != nil {
41+
err := k.exec.RunProcess("kubectl",
42+
fmt.Sprintf("--request-timeout=%s", k.timeout),
43+
"delete", "namespace", namespace, "--timeout", timeoutSec)
44+
if err != nil {
3845
fmt.Printf("Namespace '%s' did not terminate after %s.\n", namespace, timeoutSec)
3946
}
4047

4148
if k.getNamespace(namespace) {
4249
fmt.Printf("Namespace '%s' did not terminate after %s.\n", namespace, timeoutSec)
4350

4451
fmt.Println("Force-deleting everything...")
45-
if err := k.exec.RunProcess("kubectl", "delete", "all", "--namespace", namespace, "--all", "--force", "--grace-period=0"); err != nil {
52+
err = k.exec.RunProcess("kubectl",
53+
fmt.Sprintf("--request-timeout=%s", k.timeout),
54+
"delete", "all", "--namespace", namespace, "--all", "--force",
55+
"--grace-period=0")
56+
if err != nil {
4657
fmt.Printf("Error deleting everything in the namespace %v: %v", namespace, err)
4758
}
4859

@@ -59,7 +70,9 @@ func (k Kubectl) DeleteNamespace(namespace string) {
5970

6071
func (k Kubectl) forceNamespaceDeletion(namespace string) error {
6172
// Getting the namespace json to remove the finalizer
62-
cmdOutput, err := k.exec.RunProcessAndCaptureOutput("kubectl", "get", "namespace", namespace, "--output=json")
73+
cmdOutput, err := k.exec.RunProcessAndCaptureOutput("kubectl",
74+
fmt.Sprintf("--request-timeout=%s", k.timeout),
75+
"get", "namespace", namespace, "--output=json")
6376
if err != nil {
6477
fmt.Println("Error getting namespace json:", err)
6578
return err
@@ -111,13 +124,20 @@ func (k Kubectl) forceNamespaceDeletion(namespace string) error {
111124
time.Sleep(5 * time.Second)
112125

113126
// Check again
114-
if _, err := k.exec.RunProcessAndCaptureOutput("kubectl", "get", "namespace", namespace); err != nil {
127+
_, err = k.exec.RunProcessAndCaptureOutput("kubectl",
128+
fmt.Sprintf("--request-timeout=%s", k.timeout),
129+
"get", "namespace", namespace)
130+
if err != nil {
115131
fmt.Printf("Namespace '%s' terminated.\n", namespace)
116132
return nil
117133
}
118134

119135
fmt.Printf("Force-deleting namespace '%s'...\n", namespace)
120-
if err := k.exec.RunProcess("kubectl", "delete", "namespace", namespace, "--force", "--grace-period=0", "--ignore-not-found=true"); err != nil {
136+
err = k.exec.RunProcess("kubectl",
137+
fmt.Sprintf("--request-timeout=%s", k.timeout),
138+
"delete", "namespace", namespace, "--force", "--grace-period=0",
139+
"--ignore-not-found=true")
140+
if err != nil {
121141
fmt.Println("Error deleting namespace:", err)
122142
return err
123143
}
@@ -126,16 +146,20 @@ func (k Kubectl) forceNamespaceDeletion(namespace string) error {
126146
}
127147

128148
func (k Kubectl) WaitForDeployments(namespace string, selector string) error {
129-
output, err := k.exec.RunProcessAndCaptureOutput(
130-
"kubectl", "get", "deployments", "--namespace", namespace, "--selector", selector, "--output", "jsonpath={.items[*].metadata.name}")
149+
output, err := k.exec.RunProcessAndCaptureOutput("kubectl",
150+
fmt.Sprintf("--request-timeout=%s", k.timeout),
151+
"get", "deployments", "--namespace", namespace, "--selector", selector,
152+
"--output", "jsonpath={.items[*].metadata.name}")
131153
if err != nil {
132154
return err
133155
}
134156

135157
deployments := strings.Fields(output)
136158
for _, deployment := range deployments {
137159
deployment = strings.Trim(deployment, "'")
138-
err := k.exec.RunProcess("kubectl", "rollout", "status", "deployment", deployment, "--namespace", namespace)
160+
err = k.exec.RunProcess("kubectl",
161+
fmt.Sprintf("--request-timeout=%s", k.timeout),
162+
"rollout", "status", "deployment", deployment, "--namespace", namespace)
139163
if err != nil {
140164
return err
141165
}
@@ -145,7 +169,9 @@ func (k Kubectl) WaitForDeployments(namespace string, selector string) error {
145169
//
146170
// Just after rollout, pods from the previous deployment revision may still be in a
147171
// terminating state.
148-
unavailable, err := k.exec.RunProcessAndCaptureOutput("kubectl", "get", "deployment", deployment, "--namespace", namespace, "--output",
172+
unavailable, err := k.exec.RunProcessAndCaptureOutput("kubectl",
173+
fmt.Sprintf("--request-timeout=%s", k.timeout),
174+
"get", "deployment", deployment, "--namespace", namespace, "--output",
149175
`jsonpath={.status.unavailableReplicas}`)
150176
if err != nil {
151177
return err
@@ -159,7 +185,9 @@ func (k Kubectl) WaitForDeployments(namespace string, selector string) error {
159185
}
160186

161187
func (k Kubectl) GetPodsforDeployment(namespace string, deployment string) ([]string, error) {
162-
jsonString, _ := k.exec.RunProcessAndCaptureOutput("kubectl", "get", "deployment", deployment, "--namespace", namespace, "--output=json")
188+
jsonString, _ := k.exec.RunProcessAndCaptureOutput("kubectl",
189+
fmt.Sprintf("--request-timeout=%s", k.timeout),
190+
"get", "deployment", deployment, "--namespace", namespace, "--output=json")
163191
var deploymentMap map[string]interface{}
164192
err := json.Unmarshal([]byte(jsonString), &deploymentMap)
165193
if err != nil {
@@ -183,23 +211,30 @@ func (k Kubectl) GetPodsforDeployment(namespace string, deployment string) ([]st
183211
func (k Kubectl) GetPods(args ...string) ([]string, error) {
184212
kubectlArgs := []string{"get", "pods"}
185213
kubectlArgs = append(kubectlArgs, args...)
186-
pods, err := k.exec.RunProcessAndCaptureOutput("kubectl", kubectlArgs)
214+
pods, err := k.exec.RunProcessAndCaptureOutput("kubectl",
215+
fmt.Sprintf("--request-timeout=%s", k.timeout), kubectlArgs)
187216
if err != nil {
188217
return nil, err
189218
}
190219
return strings.Fields(pods), nil
191220
}
192221

193222
func (k Kubectl) GetEvents(namespace string) error {
194-
return k.exec.RunProcess("kubectl", "get", "events", "--output", "wide", "--namespace", namespace)
223+
return k.exec.RunProcess("kubectl",
224+
fmt.Sprintf("--request-timeout=%s", k.timeout),
225+
"get", "events", "--output", "wide", "--namespace", namespace)
195226
}
196227

197228
func (k Kubectl) DescribePod(namespace string, pod string) error {
198-
return k.exec.RunProcess("kubectl", "describe", "pod", pod, "--namespace", namespace)
229+
return k.exec.RunProcess("kubectl",
230+
fmt.Sprintf("--request-timeout=%s", k.timeout),
231+
"describe", "pod", pod, "--namespace", namespace)
199232
}
200233

201234
func (k Kubectl) Logs(namespace string, pod string, container string) error {
202-
return k.exec.RunProcess("kubectl", "logs", pod, "--namespace", namespace, "--container", container)
235+
return k.exec.RunProcess("kubectl",
236+
fmt.Sprintf("--request-timeout=%s", k.timeout),
237+
"logs", pod, "--namespace", namespace, "--container", container)
203238
}
204239

205240
func (k Kubectl) GetInitContainers(namespace string, pod string) ([]string, error) {
@@ -211,7 +246,10 @@ func (k Kubectl) GetContainers(namespace string, pod string) ([]string, error) {
211246
}
212247

213248
func (k Kubectl) getNamespace(namespace string) bool {
214-
if _, err := k.exec.RunProcessAndCaptureOutput("kubectl", "get", "namespace", namespace); err != nil {
249+
_, err := k.exec.RunProcessAndCaptureOutput("kubectl",
250+
fmt.Sprintf("--request-timeout=%s", k.timeout),
251+
"get", "namespace", namespace)
252+
if err != nil {
215253
fmt.Printf("Namespace '%s' terminated.\n", namespace)
216254
return false
217255
}

0 commit comments

Comments
 (0)