Skip to content

Commit 9981c37

Browse files
authored
Merge pull request #2359 from tejal29/add_healthcheck
Add deployment health check implementation
2 parents 2c9ed37 + 9bce2ca commit 9981c37

File tree

14 files changed

+637
-7
lines changed

14 files changed

+637
-7
lines changed

cmd/skaffold/app/cmd/flags.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ var FlagRegistry = []Flag{
226226
Value: &opts.StatusCheck,
227227
DefValue: true,
228228
FlagAddMethod: "BoolVar",
229-
DefinedOn: []string{"dev", "debug", "deploy"},
229+
DefinedOn: []string{"dev", "debug", "deploy", "run"},
230230
},
231231
}
232232

integration/deploy_test.go

+37-1
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@ func TestBuildDeploy(t *testing.T) {
6767
dir.Write("build.out", string(outputBytes))
6868

6969
// Run Deploy using the build output
70-
skaffold.Deploy("--build-artifacts", buildOutputFile).InDir("examples/microservices").InNs(ns.Name).RunOrFail(t)
70+
// See https://github.com/GoogleContainerTools/skaffold/issues/2372 on why status-check=false
71+
skaffold.Deploy("--build-artifacts", buildOutputFile, "--status-check=false").InDir("examples/microservices").InNs(ns.Name).RunOrFail(t)
7172

7273
depApp := client.GetDeployment("leeroy-app")
7374
testutil.CheckDeepEqual(t, appTag, depApp.Spec.Template.Spec.Containers[0].Image)
@@ -96,3 +97,38 @@ func TestDeploy(t *testing.T) {
9697

9798
skaffold.Delete().InDir("examples/kustomize").InNs(ns.Name).RunOrFail(t)
9899
}
100+
101+
func TestDeployWithInCorrectConfig(t *testing.T) {
102+
if testing.Short() {
103+
t.Skip("skipping integration test")
104+
}
105+
if ShouldRunGCPOnlyTests() {
106+
t.Skip("skipping test that is not gcp only")
107+
}
108+
109+
ns, _, deleteNs := SetupNamespace(t)
110+
defer deleteNs()
111+
112+
err := skaffold.Deploy().InDir("testdata/unstable-deployment").InNs(ns.Name).Run(t)
113+
if err == nil {
114+
t.Error("expected an error to see since the deployment is not stable. However deploy returned success")
115+
}
116+
117+
skaffold.Delete().InDir("testdata/unstable-deployment").InNs(ns.Name).RunOrFail(t)
118+
}
119+
120+
func TestDeployWithInCorrectConfigWithNoStatusCheck(t *testing.T) {
121+
if testing.Short() {
122+
t.Skip("skipping integration test")
123+
}
124+
if ShouldRunGCPOnlyTests() {
125+
t.Skip("skipping test that is not gcp only")
126+
}
127+
128+
ns, _, deleteNs := SetupNamespace(t)
129+
defer deleteNs()
130+
131+
skaffold.Deploy("--status-check=false").InDir("testdata/unstable-deployment").InNs(ns.Name).RunOrFailOutput(t)
132+
133+
skaffold.Delete().InDir("testdata/unstable-deployment").InNs(ns.Name).RunOrFail(t)
134+
}

integration/run_test.go

+2
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ func TestRun(t *testing.T) {
5252
}, {
5353
description: "microservices",
5454
dir: "examples/microservices",
55+
// See https://github.com/GoogleContainerTools/skaffold/issues/2372
56+
args: []string{"--status-check=false"},
5557
deployments: []string{"leeroy-app", "leeroy-web"},
5658
}, {
5759
description: "envTagger",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
FROM SCRATCH
2+
COPY hello /
3+
CMD ["/hello"]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
=== Example: This example is used in integration tests.
2+
3+
This is an invalid deployment. Please do not use it.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/bin/bash
2+
3+
echo "Hello World"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
apiVersion: apps/v1
2+
kind: Deployment
3+
metadata:
4+
name: unstable-deployment
5+
spec:
6+
progressDeadlineSeconds: 10
7+
replicas: 1
8+
selector:
9+
matchLabels:
10+
app: unstable
11+
template:
12+
metadata:
13+
labels:
14+
app: unstable
15+
spec:
16+
containers:
17+
- name: incorrect-example
18+
image: gcr.io/k8s-skaffold/incorrect-example
19+
readinessProbe:
20+
exec:
21+
command:
22+
- cat
23+
- /does-not-exist
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
apiVersion: skaffold/v1beta12
2+
kind: Config
3+
build:
4+
artifacts:
5+
- image: gcr.io/k8s-skaffold/incorrect-example
6+
deploy:
7+
kubectl:
8+
manifests:
9+
- incorrect-deployment.yaml

pkg/skaffold/deploy/kubectl/cli.go

+10
Original file line numberDiff line numberDiff line change
@@ -118,3 +118,13 @@ func (c *CLI) args(command string, commandFlags []string, arg ...string) []strin
118118

119119
return args
120120
}
121+
122+
// Run shells out kubectl CLI.
123+
func (c *CLI) RunOut(ctx context.Context, in io.Reader, command string, commandFlags []string, arg ...string) ([]byte, error) {
124+
args := c.args(command, commandFlags, arg...)
125+
126+
cmd := exec.CommandContext(ctx, "kubectl", args...)
127+
cmd.Stdin = in
128+
129+
return util.RunCmdOut(cmd)
130+
}

pkg/skaffold/deploy/status_check.go

+149
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
/*
2+
Copyright 2019 The Skaffold Authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package deploy
18+
19+
import (
20+
"context"
21+
"fmt"
22+
"strings"
23+
"sync"
24+
"time"
25+
26+
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/deploy/kubectl"
27+
kubernetesutil "github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubernetes"
28+
runcontext "github.com/GoogleContainerTools/skaffold/pkg/skaffold/runner/context"
29+
"github.com/pkg/errors"
30+
"github.com/sirupsen/logrus"
31+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
32+
"k8s.io/client-go/kubernetes"
33+
)
34+
35+
var (
36+
// TODO: Move this to a flag or global config.
37+
// Default deadline set to 10 minutes. This is default value for progressDeadlineInSeconds
38+
// See: https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/api/apps/v1/types.go#L305
39+
defaultStatusCheckDeadlineInSeconds int32 = 600
40+
// Poll period for checking set to 100 milliseconds
41+
defaultPollPeriodInMilliseconds = 100
42+
43+
// For testing
44+
executeRolloutStatus = getRollOutStatus
45+
)
46+
47+
func StatusCheck(ctx context.Context, defaultLabeller *DefaultLabeller, runCtx *runcontext.RunContext) error {
48+
49+
client, err := kubernetesutil.GetClientset()
50+
if err != nil {
51+
return err
52+
}
53+
dMap, err := getDeployments(client, runCtx.Opts.Namespace, defaultLabeller)
54+
if err != nil {
55+
return errors.Wrap(err, "could not fetch deployments")
56+
}
57+
58+
wg := sync.WaitGroup{}
59+
// Its safe to use sync.Map without locks here as each subroutine adds a different key to the map.
60+
syncMap := &sync.Map{}
61+
kubeCtl := &kubectl.CLI{
62+
Namespace: runCtx.Opts.Namespace,
63+
KubeContext: runCtx.KubeContext,
64+
}
65+
66+
for dName, deadline := range dMap {
67+
deadlineDuration := time.Duration(deadline) * time.Second
68+
wg.Add(1)
69+
go func(dName string, deadlineDuration time.Duration) {
70+
defer wg.Done()
71+
pollDeploymentRolloutStatus(ctx, kubeCtl, dName, deadlineDuration, syncMap)
72+
}(dName, deadlineDuration)
73+
}
74+
75+
// Wait for all deployment status to be fetched
76+
wg.Wait()
77+
return getSkaffoldDeployStatus(syncMap)
78+
}
79+
80+
func getDeployments(client kubernetes.Interface, ns string, l *DefaultLabeller) (map[string]int32, error) {
81+
82+
deps, err := client.AppsV1().Deployments(ns).List(metav1.ListOptions{
83+
LabelSelector: l.K8sManagedByLabelKeyValueString(),
84+
})
85+
if err != nil {
86+
return nil, errors.Wrap(err, "could not fetch deployments")
87+
}
88+
89+
depMap := map[string]int32{}
90+
91+
for _, d := range deps.Items {
92+
var deadline int32
93+
if d.Spec.ProgressDeadlineSeconds == nil {
94+
logrus.Debugf("no progressDeadlineSeconds config found for deployment %s. Setting deadline to %d seconds", d.Name, defaultStatusCheckDeadlineInSeconds)
95+
deadline = defaultStatusCheckDeadlineInSeconds
96+
} else {
97+
deadline = *d.Spec.ProgressDeadlineSeconds
98+
}
99+
depMap[d.Name] = deadline
100+
}
101+
102+
return depMap, nil
103+
}
104+
105+
func pollDeploymentRolloutStatus(ctx context.Context, k *kubectl.CLI, dName string, deadline time.Duration, syncMap *sync.Map) {
106+
pollDuration := time.Duration(defaultPollPeriodInMilliseconds) * time.Millisecond
107+
// Add poll duration to account for one last attempt after progressDeadlineSeconds.
108+
timeoutContext, cancel := context.WithTimeout(ctx, deadline+pollDuration)
109+
logrus.Debugf("checking rollout status %s", dName)
110+
defer cancel()
111+
for {
112+
select {
113+
case <-timeoutContext.Done():
114+
syncMap.Store(dName, errors.Wrap(timeoutContext.Err(), fmt.Sprintf("deployment rollout status could not be fetched within %v", deadline)))
115+
return
116+
case <-time.After(pollDuration):
117+
status, err := executeRolloutStatus(timeoutContext, k, dName)
118+
if err != nil {
119+
syncMap.Store(dName, err)
120+
return
121+
}
122+
if strings.Contains(status, "successfully rolled out") {
123+
syncMap.Store(dName, nil)
124+
return
125+
}
126+
}
127+
}
128+
}
129+
130+
func getSkaffoldDeployStatus(m *sync.Map) error {
131+
errorStrings := []string{}
132+
m.Range(func(k, v interface{}) bool {
133+
if t, ok := v.(error); ok {
134+
errorStrings = append(errorStrings, fmt.Sprintf("deployment %s failed due to %s", k, t.Error()))
135+
}
136+
return true
137+
})
138+
139+
if len(errorStrings) == 0 {
140+
return nil
141+
}
142+
return fmt.Errorf("following deployments are not stable:\n%s", strings.Join(errorStrings, "\n"))
143+
}
144+
145+
func getRollOutStatus(ctx context.Context, k *kubectl.CLI, dName string) (string, error) {
146+
b, err := k.RunOut(ctx, nil, "rollout", []string{"status", "deployment", dName},
147+
"--watch=false")
148+
return string(b), err
149+
}

0 commit comments

Comments
 (0)