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

Add the ability to add healthcheck definition for containers #1742

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
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
79 changes: 79 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2195,6 +2195,85 @@ container_image(name, base, data_path, directory, files, legacy_repository_namin
</p>
</td>
</tr>
<tr>
<td><code>healthcheck_test</code></td>
<td>
<code>List of strings, optional</code>
<p><a href="https://docs.docker.com/engine/reference/builder/#healthcheck">
The test to perform to check that the container is healthy.</a></p>
<p>
An empty list means to inherit the default. If <code>healthcheck_test</code> wasn't defined
<code>HEALTHCHECK</code> will be inherited from an upper layers.
</p>
<p>Possible definition options are:</p>
<p><code>None</code>/<code>[]</code> : inherit healthcheck</p>
<p><code>["NONE"]</code> : disable healthcheck</p>
<p><code>["CMD", "test", "-f", "file"]</code> : exec arguments directly</p>
<p><code>["CMD-SHELL", "curl -q localhost:8080"]</code> : run command with system's default shell</p>
<p>
<code>
healthcheck_test = ["CMD", "test", "-f", "file"],
</code>
</p>
</td>
</tr>
<tr>
<td><code>healthcheck_interval</code></td>
<td>
<code>strings, optional</code>
<p>Interval is the time to wait between health checks.</p>
<p>
Should be in the duration format. Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h"
</p>
<p>
<code>
healthcheck_interval = "30s",
</code>
</p>
</td>
</tr>
<tr>
<td><code>healthcheck_timeout</code></td>
<td>
<code>strings, optional</code>
<p>Health check timeout is the time to wait before considering the check to have hung.</p>
<p>
Should be in the duration format. Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h"
</p>
<p>
<code>
healthcheck_timeout = "30s",
</code>
</p>
</td>
</tr>
<tr>
<td><code>healthcheck_start_period</code></td>
<td>
<code>strings, optional</code>
<p>The health check start period for the container to initialize before the retries starts to count down.</p>
<p>
Should be in the duration format. Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h"
</p>
<p>
<code>
healthcheck_start_period = "10s",
</code>
</p>
</td>
</tr>
<tr>
<td><code>healthcheck_retries</code></td>
<td>
<code>integer, optional</code>
<p>Health check retries is the number of consecutive failures needed to consider a container as unhealthy.</p>
<p>
<code>
healthcheck_retries = 2,
</code>
</p>
</td>
</tr>
</tbody>
</table>

Expand Down
11 changes: 11 additions & 0 deletions container/go/cmd/create_image_config/create_image_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ var (
architecture = flag.String("architecture", "amd64", "The architecture of the docker image.")
operatingSystem = flag.String("operatingSystem", "linux", "Operating system to create docker image for, eg. linux.")
osVersion = flag.String("osVersion", "", "Operating system version to create docker image for (primarily for windows).")
healthcheckInterval = flag.String("healthcheckInterval", "", "Health check interval is the time to wait between checks.")
healthcheckTimeout = flag.String("healthcheckTimeout", "", "Health check timeout is the time to wait before considering the check to have hung.")
healthcheckStartPeriod = flag.String("healthcheckStartPeriod", "", "Health check start period for the container to initialize before the retries starts to count down.")
healthcheckRetries = flag.Int("healthcheckRetries", 0, "Health check retries is the number of consecutive failures needed to consider a container as unhealthy. Zero means inherit.")
labelsArray utils.ArrayStringFlags
ports utils.ArrayStringFlags
volumes utils.ArrayStringFlags
Expand All @@ -51,6 +55,7 @@ var (
entrypoint utils.ArrayStringFlags
layerDigestFile utils.ArrayStringFlags
stampInfoFile utils.ArrayStringFlags
healthcheckTest utils.ArrayStringFlags
)

func main() {
Expand All @@ -63,6 +68,7 @@ func main() {
flag.Var(&entrypoint, "entrypoint", "Override the Entrypoint of the previous layer.")
flag.Var(&layerDigestFile, "layerDigestFile", "Layer sha256 hashes that make up this image. The order that these layers are specified matters.")
flag.Var(&stampInfoFile, "stampInfoFile", "A list of files from which to read substitutions to make in the provided fields.")
flag.Var(&healthcheckTest, "healthcheckTest", "Override the HealthCheck command of the previous layer.")

flag.Parse()

Expand Down Expand Up @@ -109,6 +115,11 @@ func main() {
Command: command,
Entrypoint: entrypoint,
Layer: layerDigestFile,
HealthcheckInterval: *healthcheckInterval,
HealthcheckTimeout: *healthcheckTimeout,
HealthcheckStartPeriod: *healthcheckStartPeriod,
HealthcheckRetries: *healthcheckRetries,
HealthcheckTest: healthcheckTest,
Stamper: stamper,
}

Expand Down
97 changes: 97 additions & 0 deletions container/go/pkg/compat/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,29 @@ const (
defaultTimestamp = "1970-01-01T00:00:00Z"
)

var (
validHealthcheckTypes = map[string]func([]string) error{
"NONE": func(args []string) error {
if len(args) != 1 {
return errors.New("NONE doesn't accept any extra args")
}
return nil
},
"CMD": func(args []string) error {
if len(args) < 2 {
return errors.New("CMD accepts at least 1 arg")
}
return nil
},
"CMD-SHELL": func(args []string) error {
if len(args) != 2 {
return errors.New("CMD-SHELL accepts only 1 arg")
}
return nil
},
}
)

// OverrideConfigOpts holds all configuration settings for the newly outputted config file.
type OverrideConfigOpts struct {
// ConfigFile is the base config.json file.
Expand Down Expand Up @@ -88,6 +111,21 @@ type OverrideConfigOpts struct {
Entrypoint []string
// Layer is the list of layer sha256 hashes that compose the image for which the config is written.
Layer []string
// HealthcheckInterval is the time to wait between checks.
HealthcheckInterval string
// HealthcheckTimeout is the time to wait before considering the check to have hung.
HealthcheckTimeout string
// HealthcheckStartPeriod for the container to initialize before the retries starts to count down.
HealthcheckStartPeriod string
// HealthCheckRetries is the number of consecutive failures needed to consider a container as unhealthy. Zero means inherit.
HealthcheckRetries int
// HealthcheckTest is the command to override the health check command of the previous layer.
// Possible variations:
// {} : inherit healthcheck
// {"NONE"} : disable healthcheck
// {"CMD", args...} : exec arguments directly
// {"CMD-SHELL", command} : run command with system's default shell
HealthcheckTest []string
// Stamper will be used to stamp values in the image config.
Stamper *Stamper
}
Expand Down Expand Up @@ -365,6 +403,61 @@ func updateConfigLabels(overrideInfo *OverrideConfigOpts, labels map[string]stri
return labelsMap
}

// healthCheckConfigIsChanged checks if any parameters for healthcheck were changed
func healthCheckConfigIsChanged(overrideInfo *OverrideConfigOpts) bool {
return overrideInfo.HealthcheckInterval != "" ||
overrideInfo.HealthcheckTimeout != "" ||
overrideInfo.HealthcheckStartPeriod != "" ||
overrideInfo.HealthcheckRetries > 0 ||
len(overrideInfo.HealthcheckTest) > 0
}

// updateHealthCheck modifies the config's health check definition based on the provided override options.
func updateHealthCheck(overrideInfo *OverrideConfigOpts) error {
if overrideInfo.ConfigFile.Config.Healthcheck == nil && healthCheckConfigIsChanged(overrideInfo) {
overrideInfo.ConfigFile.Config.Healthcheck = &v1.HealthConfig{}
}

if overrideInfo.HealthcheckInterval != "" {
interval, err := time.ParseDuration(overrideInfo.HealthcheckInterval)
if err != nil {
return errors.Wrapf(err, "failed to parse healthcheckInterval (%s)", overrideInfo.HealthcheckInterval)
}
overrideInfo.ConfigFile.Config.Healthcheck.Interval = interval
}

if overrideInfo.HealthcheckTimeout != "" {
timeout, err := time.ParseDuration(overrideInfo.HealthcheckTimeout)
if err != nil {
return errors.Wrapf(err, "failed to parse healthcheckTimeout (%s)", overrideInfo.HealthcheckTimeout)
}
overrideInfo.ConfigFile.Config.Healthcheck.Timeout = timeout
}

if overrideInfo.HealthcheckStartPeriod != "" {
startPeriod, err := time.ParseDuration(overrideInfo.HealthcheckStartPeriod)
if err != nil {
return errors.Wrapf(err, "failed to parse healthcheckStartPeriod (%s)", overrideInfo.HealthcheckStartPeriod)
}
overrideInfo.ConfigFile.Config.Healthcheck.StartPeriod = startPeriod
}

if overrideInfo.HealthcheckRetries > 0 {
overrideInfo.ConfigFile.Config.Healthcheck.Retries = overrideInfo.HealthcheckRetries
}

if len(overrideInfo.HealthcheckTest) > 0 {
checkType := overrideInfo.HealthcheckTest[0]
if check, ok := validHealthcheckTypes[checkType]; !ok {
return fmt.Errorf("HealthcheckTest first argument should be one of: 'NONE', 'CMD', 'CMD-SHELL' was '%s'", checkType)
} else if err := check(overrideInfo.HealthcheckTest); err != nil {
return errors.Wrap(err, "failed to validate check check command")
}
overrideInfo.ConfigFile.Config.Healthcheck.Test = overrideInfo.HealthcheckTest
}
return nil
}

// updateExposedPorts modifies the config's exposed ports based on the input ports.
func updateExposedPorts(overrideInfo *OverrideConfigOpts) error {
for _, port := range overrideInfo.Ports {
Expand Down Expand Up @@ -505,6 +598,10 @@ func updateConfig(overrideInfo *OverrideConfigOpts) error {
}
}

if err := updateHealthCheck(overrideInfo); err != nil {
return errors.Wrap(err, "failed to update health check from config")
}

if len(overrideInfo.Volumes) > 0 {
if len(overrideInfo.ConfigFile.Config.Volumes) == 0 {
overrideInfo.ConfigFile.Config.Volumes = make(map[string]struct{})
Expand Down
61 changes: 61 additions & 0 deletions container/go/pkg/compat/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ package compat

import (
"bytes"
"reflect"
"testing"
"time"

v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/kylelemons/godebug/pretty"
Expand Down Expand Up @@ -191,6 +193,65 @@ func TestWorkdirOverride(t *testing.T) {
}
}

// TestHealthCheckOverride ensures updateConfig correctly overrides base image config
func TestHealthCheckOverride(t *testing.T) {
testCases := []struct {
name string
expected v1.HealthConfig
opts *OverrideConfigOpts
}{
{
name: "HealthcheckInterval override",
opts: &OverrideConfigOpts{
HealthcheckInterval: "10s",
ConfigFile: &v1.ConfigFile{
Config: v1.Config{
Healthcheck: &v1.HealthConfig{
Test: []string{"NONE"},
Interval: 1,
Timeout: 2,
},
},
},
},
expected: v1.HealthConfig{
Test: []string{"NONE"},
Interval: time.Duration(10 * time.Second),
Timeout: 2,
},
},
{
name: "Healthcheck Test override",
opts: &OverrideConfigOpts{
HealthcheckTest: []string{"CMD", "echo"},
ConfigFile: &v1.ConfigFile{
Config: v1.Config{
Healthcheck: &v1.HealthConfig{
Interval: 1,
Timeout: 2,
},
},
},
},
expected: v1.HealthConfig{
Test: []string{"CMD", "echo"},
Timeout: 2,
Interval: 1,
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if err := updateConfig(tc.opts); err != nil {
t.Fatalf("Failed to update config: %v", err)
}
if !reflect.DeepEqual(*tc.opts.ConfigFile.Config.Healthcheck, tc.expected) {
t.Errorf("Healthcheck field in config was updated to invalid value, got %q, want %q.", *tc.opts.ConfigFile.Config.Healthcheck, tc.expected)
}
})
}
}

func TestEntrypointPrefix(t *testing.T) {
want := []string{"prefix1", "prefix2", "entrypoint1", "entrypoint2"}
opts := &OverrideConfigOpts{
Expand Down
Loading