Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

podman play kube support container startup probe #16794

Merged
merged 2 commits into from
Dec 21, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 70 additions & 28 deletions pkg/specgen/generate/kube/kube.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,10 @@ func ToSpecGen(ctx context.Context, opts *CtrSpecGenOptions) (*specgen.SpecGener
if err != nil {
return nil, fmt.Errorf("failed to configure livenessProbe: %w", err)
}
err = setupStartupProbe(s, opts.Container, opts.RestartPolicy)
if err != nil {
return nil, fmt.Errorf("failed to configure startupProbe: %w", err)
}

// Since we prefix the container name with pod name to work-around the uniqueness requirement,
// the seccomp profile should reference the actual container name from the YAML
Expand Down Expand Up @@ -511,45 +515,83 @@ func parseMountPath(mountPath string, readOnly bool, propagationMode *v1.MountPr
return dest, opts, nil
}

func probeToHealthConfig(probe *v1.Probe) (*manifest.Schema2HealthConfig, error) {
var commandString string
failureCmd := "exit 1"
probeHandler := probe.Handler

// configure healthcheck on the basis of Handler Actions.
switch {
case probeHandler.Exec != nil:
cmd, err := json.Marshal(probeHandler.Exec.Command)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's not immediately obvious to me why this works. do you have a go doc reference or code you based this off that could explain why/how it works?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

func makeHealthCheck(inCmd string, interval int32, retries int32, timeout int32, startPeriod int32) (*manifest.Schema2HealthConfig, error) {
// Every healthcheck requires a command
if len(inCmd) == 0 {
return nil, errors.New("must define a healthcheck command for all healthchecks")
}
// first try to parse option value as JSON array of strings...
cmd := []string{}
if inCmd == "none" {
cmd = []string{define.HealthConfigTestNone}
} else {
err := json.Unmarshal([]byte(inCmd), &cmd)
if err != nil {
// ...otherwise pass it to "/bin/sh -c" inside the container
cmd = []string{define.HealthConfigTestCmdShell}
cmd = append(cmd, strings.Split(inCmd, " ")...)
}
}

the function makeHealthCheck supports input string as a json array :D

if err != nil {
return nil, err
}
commandString = string(cmd)
case probeHandler.HTTPGet != nil:
haircommander marked this conversation as resolved.
Show resolved Hide resolved
// set defaults as in https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/#http-probes
uriScheme := v1.URISchemeHTTP
if probeHandler.HTTPGet.Scheme != "" {
uriScheme = probeHandler.HTTPGet.Scheme
}
host := "localhost" // Kubernetes default is host IP, but with Podman there is only one node
if probeHandler.HTTPGet.Host != "" {
host = probeHandler.HTTPGet.Host
}
path := "/"
if probeHandler.HTTPGet.Path != "" {
path = probeHandler.HTTPGet.Path
}
commandString = fmt.Sprintf("curl -f %s://%s:%d%s || %s", uriScheme, host, probeHandler.HTTPGet.Port.IntValue(), path, failureCmd)
case probeHandler.TCPSocket != nil:
commandString = fmt.Sprintf("nc -z -v %s %d || %s", probeHandler.TCPSocket.Host, probeHandler.TCPSocket.Port.IntValue(), failureCmd)
}
return makeHealthCheck(commandString, probe.PeriodSeconds, probe.FailureThreshold, probe.TimeoutSeconds, probe.InitialDelaySeconds)
}

func setupLivenessProbe(s *specgen.SpecGenerator, containerYAML v1.Container, restartPolicy string) error {
var err error
if containerYAML.LivenessProbe == nil {
return nil
}
emptyHandler := v1.Handler{}
if containerYAML.LivenessProbe.Handler != emptyHandler {
var commandString string
failureCmd := "exit 1"
probe := containerYAML.LivenessProbe
probeHandler := probe.Handler

// configure healthcheck on the basis of Handler Actions.
switch {
case probeHandler.Exec != nil:
execString := strings.Join(probeHandler.Exec.Command, " ")
commandString = fmt.Sprintf("%s || %s", execString, failureCmd)
case probeHandler.HTTPGet != nil:
// set defaults as in https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/#http-probes
uriScheme := v1.URISchemeHTTP
if probeHandler.HTTPGet.Scheme != "" {
uriScheme = probeHandler.HTTPGet.Scheme
}
host := "localhost" // Kubernetes default is host IP, but with Podman there is only one node
if probeHandler.HTTPGet.Host != "" {
host = probeHandler.HTTPGet.Host
}
path := "/"
if probeHandler.HTTPGet.Path != "" {
path = probeHandler.HTTPGet.Path
}
commandString = fmt.Sprintf("curl -f %s://%s:%d%s || %s", uriScheme, host, probeHandler.HTTPGet.Port.IntValue(), path, failureCmd)
case probeHandler.TCPSocket != nil:
commandString = fmt.Sprintf("nc -z -v %s %d || %s", probeHandler.TCPSocket.Host, probeHandler.TCPSocket.Port.IntValue(), failureCmd)
s.HealthConfig, err = probeToHealthConfig(containerYAML.LivenessProbe)
if err != nil {
return err
}
s.HealthConfig, err = makeHealthCheck(commandString, probe.PeriodSeconds, probe.FailureThreshold, probe.TimeoutSeconds, probe.InitialDelaySeconds)
// if restart policy is in place, ensure the health check enforces it
if restartPolicy == "always" || restartPolicy == "onfailure" {
s.HealthCheckOnFailureAction = define.HealthCheckOnFailureActionRestart
}
return nil
}
return nil
}

func setupStartupProbe(s *specgen.SpecGenerator, containerYAML v1.Container, restartPolicy string) error {
if containerYAML.StartupProbe == nil {
return nil
}
emptyHandler := v1.Handler{}
if containerYAML.StartupProbe.Handler != emptyHandler {
healthConfig, err := probeToHealthConfig(containerYAML.StartupProbe)
if err != nil {
return err
}

// currently, StartupProbe still an optional feature, and it requires HealthConfig.
if s.HealthConfig == nil {
probe := containerYAML.StartupProbe
s.HealthConfig, err = makeHealthCheck("exit 0", probe.PeriodSeconds, probe.FailureThreshold, probe.TimeoutSeconds, probe.InitialDelaySeconds)
if err != nil {
return err
}
}
s.StartupHealthConfig = &define.StartupHealthCheck{
Schema2HealthConfig: *healthConfig,
Successes: int(containerYAML.StartupProbe.SuccessThreshold),
}
// if restart policy is in place, ensure the health check enforces it
if restartPolicy == "always" || restartPolicy == "onfailure" {
s.HealthCheckOnFailureAction = define.HealthCheckOnFailureActionRestart
Expand Down
73 changes: 72 additions & 1 deletion test/e2e/play_kube_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,48 @@ spec:
periodSeconds: 1
`

var startupProbePodYaml = `
apiVersion: apps/v1
kind: Deployment
metadata:
name: startup-healthy-probe
labels:
app: alpine
spec:
replicas: 1
selector:
matchLabels:
app: alpine
template:
metadata:
labels:
app: alpine
spec:
restartPolicy: Never
containers:
- command:
- top
- -d
- "1.5"
name: alpine
image: quay.io/libpod/alpine:latest
startupProbe:
exec:
command:
- /bin/sh
- -c
- cat /testfile
initialDelaySeconds: 0
periodSeconds: 1
livenessProbe:
exec:
command:
- echo
- liveness probe
initialDelaySeconds: 0
periodSeconds: 1
`

var selinuxLabelPodYaml = `
apiVersion: v1
kind: Pod
Expand Down Expand Up @@ -1712,7 +1754,7 @@ var _ = Describe("Podman play kube", func() {
inspect.WaitWithDefaultTimeout()
healthcheckcmd := inspect.OutputToString()
// check if CMD-SHELL based equivalent health check is added to container
Expect(healthcheckcmd).To(ContainSubstring("CMD-SHELL"))
Expect(healthcheckcmd).To(ContainSubstring("[echo hello]"))
})

It("podman play kube liveness probe should fail", func() {
Expand All @@ -1730,6 +1772,35 @@ var _ = Describe("Podman play kube", func() {
Expect(hcoutput).To(ContainSubstring(define.HealthCheckUnhealthy))
})

It("podman play kube support container startup probe", func() {
ctrName := "startup-healthy-probe-pod-0-alpine"
err := writeYaml(startupProbePodYaml, kubeYaml)
Expect(err).ToNot(HaveOccurred())

kube := podmanTest.Podman([]string{"play", "kube", kubeYaml})
kube.WaitWithDefaultTimeout()
Expect(kube).Should(Exit(0))

time.Sleep(2 * time.Second)
inspect := podmanTest.InspectContainer(ctrName)
Expect(inspect[0].State.Health).To(HaveField("Status", "starting"))

hc := podmanTest.Podman([]string{"healthcheck", "run", ctrName})
hc.WaitWithDefaultTimeout()
Expect(hc).Should(Exit(1))

exec := podmanTest.Podman([]string{"exec", ctrName, "sh", "-c", "echo 'startup probe success' > /testfile"})
exec.WaitWithDefaultTimeout()
Expect(exec).Should(Exit(0))

hc = podmanTest.Podman([]string{"healthcheck", "run", ctrName})
hc.WaitWithDefaultTimeout()
Expect(hc).Should(Exit(0))

inspect = podmanTest.InspectContainer(ctrName)
Expect(inspect[0].State.Health).To(HaveField("Status", define.HealthCheckHealthy))
})

It("podman play kube fail with nonexistent authfile", func() {
err := generateKubeYaml("pod", getPod(), kubeYaml)
Expect(err).ToNot(HaveOccurred())
Expand Down