Skip to content
This repository has been archived by the owner on May 4, 2021. It is now read-only.

Commit

Permalink
Add support for HEALTHCHECK directive (#100)
Browse files Browse the repository at this point in the history
* Add support for healthcheck

* Add test
  • Loading branch information
yiranwang52 authored Dec 5, 2018
1 parent d10c012 commit 74e9b38
Show file tree
Hide file tree
Showing 8 changed files with 388 additions and 31 deletions.
69 changes: 69 additions & 0 deletions lib/builder/step/healthcheck.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Copyright (c) 2018 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package step

import (
"fmt"
"time"

"github.com/uber/makisu/lib/context"
"github.com/uber/makisu/lib/docker/image"
)

// HealthcheckStep implements BuildStep and execute HEALTHCHECK directive
type HealthcheckStep struct {
*baseStep

Interval time.Duration
Timeout time.Duration
StartPeriod time.Duration
Retries int

Test []string
}

// NewHealthcheckStep returns a BuildStep from given arguments.
func NewHealthcheckStep(
args string, interval, timeout, startPeriod time.Duration, retries int,
test []string, commit bool) (BuildStep, error) {

return &HealthcheckStep{
baseStep: newBaseStep(Healthcheck, args, commit),
Interval: interval,
Timeout: timeout,
StartPeriod: startPeriod,
Retries: retries,
Test: test,
}, nil
}

// UpdateCtxAndConfig updates mutable states in build context, and generates a
// new image config base on config from previous step.
func (s *HealthcheckStep) UpdateCtxAndConfig(
ctx *context.BuildContext, imageConfig *image.Config) (*image.Config, error) {

config, err := image.NewImageConfigFromCopy(imageConfig)
if err != nil {
return nil, fmt.Errorf("copy image config: %s", err)
}
config.Config.Healthcheck = &image.HealthConfig{
Interval: s.Interval,
Timeout: s.Timeout,
StartPeriod: s.StartPeriod,
Retries: s.Retries,
Test: s.Test,
}
return config, nil
}
49 changes: 49 additions & 0 deletions lib/builder/step/healthcheck_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright (c) 2018 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package step

import (
"testing"
"time"

"github.com/uber/makisu/lib/context"
"github.com/uber/makisu/lib/docker/image"

"github.com/stretchr/testify/require"
)

func TestHealthcheckStepUpdateCtxAndConfig(t *testing.T) {
require := require.New(t)

ctx, cleanup := context.BuildContextFixture()
defer cleanup()

cmd := []string{"CMD", "ls", "/"}
d5, _ := time.ParseDuration("5s")
d0, _ := time.ParseDuration("0s")
step, err := NewHealthcheckStep("", d0, d5, d0, 0, cmd, false)
require.NoError(err)

c := image.NewDefaultImageConfig()
result, err := step.UpdateCtxAndConfig(ctx, &c)
require.NoError(err)
require.Equal(result.Config.Healthcheck, &image.HealthConfig{
Interval: d0,
Timeout: d5,
StartPeriod: d0,
Retries: 0,
Test: cmd,
})
}
32 changes: 18 additions & 14 deletions lib/builder/step/step.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,21 @@ type Directive string

// Set of all valid directives.
const (
Add = Directive("ADD")
Cmd = Directive("CMD")
Copy = Directive("COPY")
Entrypoint = Directive("ENTRYPOINT")
Env = Directive("ENV")
Expose = Directive("EXPOSE")
From = Directive("FROM")
Label = Directive("LABEL")
Maintainer = Directive("MAINTAINER")
Run = Directive("RUN")
Stopsignal = Directive("STOPSIGNAL")
User = Directive("USER")
Volume = Directive("VOLUME")
Workdir = Directive("WORKDIR")
Add = Directive("ADD")
Cmd = Directive("CMD")
Copy = Directive("COPY")
Entrypoint = Directive("ENTRYPOINT")
Env = Directive("ENV")
Expose = Directive("EXPOSE")
From = Directive("FROM")
Healthcheck = Directive("HEALTHCHECK")
Label = Directive("LABEL")
Maintainer = Directive("MAINTAINER")
Run = Directive("RUN")
Stopsignal = Directive("STOPSIGNAL")
User = Directive("USER")
Volume = Directive("VOLUME")
Workdir = Directive("WORKDIR")
)

// BuildStep performs build for one build step.
Expand Down Expand Up @@ -127,6 +128,9 @@ func NewDockerfileStep(
case *dockerfile.FromDirective:
s, _ := d.(*dockerfile.FromDirective)
step, err = NewFromStep(s.Args, s.Image, s.Alias)
case *dockerfile.HealthcheckDirective:
s, _ := d.(*dockerfile.HealthcheckDirective)
step, err = NewHealthcheckStep(s.Args, s.Interval, s.Timeout, s.StartPeriod, s.Retries, s.Test, s.Commit)
case *dockerfile.LabelDirective:
s, _ := d.(*dockerfile.LabelDirective)
step = NewLabelStep(s.Args, s.Labels, s.Commit)
Expand Down
5 changes: 3 additions & 2 deletions lib/docker/image/container_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ type HealthConfig struct {
Test []string `json:",omitempty"`

// Zero means to inherit. Durations are expressed as integer nanoseconds.
Interval time.Duration `json:",omitempty"` // Interval is the time to wait between checks.
Timeout time.Duration `json:",omitempty"` // Timeout is the time to wait before considering the check to have hung.
Interval time.Duration `json:",omitempty"` // Interval is the time to wait between checks.
Timeout time.Duration `json:",omitempty"` // Timeout is the time to wait before considering the check to have hung.
StartPeriod time.Duration `json:",omitempty"` // The start period for the container to initialize before the retries starts to count down.

// Retries is the number of consecutive failures needed to consider a container as unhealthy.
// Zero means inherit.
Expand Down
31 changes: 16 additions & 15 deletions lib/parser/dockerfile/directive.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,22 @@ type Directive interface {
type directiveConstructor func(*baseDirective, *parsingState) (Directive, error)

var directiveConstructors = map[string]directiveConstructor{
"add": newAddDirective,
"arg": newArgDirective,
"cmd": newCmdDirective,
"copy": newCopyDirective,
"entrypoint": newEntrypointDirective,
"env": newEnvDirective,
"expose": newExposeDirective,
"from": newFromDirective,
"label": newLabelDirective,
"maintainer": newMaintainerDirective,
"run": newRunDirective,
"stopsignal": newStopsignalDirective,
"user": newUserDirective,
"volume": newVolumeDirective,
"workdir": newWorkdirDirective,
"add": newAddDirective,
"arg": newArgDirective,
"cmd": newCmdDirective,
"copy": newCopyDirective,
"entrypoint": newEntrypointDirective,
"env": newEnvDirective,
"expose": newExposeDirective,
"from": newFromDirective,
"healthcheck": newHealthcheckDirective,
"label": newLabelDirective,
"maintainer": newMaintainerDirective,
"run": newRunDirective,
"stopsignal": newStopsignalDirective,
"user": newUserDirective,
"volume": newVolumeDirective,
"workdir": newWorkdirDirective,
}

// newDirective initializes a directive from a line of a Dockerfile and
Expand Down
157 changes: 157 additions & 0 deletions lib/parser/dockerfile/healthcheck.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
// Copyright (c) 2018 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package dockerfile

import (
"fmt"
"regexp"
"strconv"
"time"
)

// HeathcheckDirective represents the "LABEL" dockerfile command.
type HealthcheckDirective struct {
*baseDirective

Interval time.Duration
Timeout time.Duration
StartPeriod time.Duration
Retries int

Test []string
}

// Variables:
// Replaced from ARGs and ENVs from within our stage.
// Formats:
// HEALTHCHECK NONE
// HEALTHCHECK [--interval=<t>] [--timeout=<t>] [--start-period=<t>] [--retries=<n>] \
// CMD ["<param>"...]
// HEALTHCHECK [--interval=<t>] [--timeout=<t>] [--start-period=<t>] [--retries=<n>] \
// CMD <command> <param>...
func newHealthcheckDirective(base *baseDirective, state *parsingState) (Directive, error) {
// TODO: regexp is not the ideal solution.
if isNone := regexp.MustCompile(`(?i)^[\s|\\]*none[\s|\\]*$`).MatchString(base.Args); isNone {
return &HealthcheckDirective{
baseDirective: base,
Test: []string{"None"},
}, nil
}
cmdIndices := regexp.MustCompile(`(?i)[\s|\\]*cmd[\s|\\]*`).FindStringIndex(base.Args)
if len(cmdIndices) < 2 {
return nil, base.err(fmt.Errorf("CMD not defined"))
}

flags, err := splitArgs(base.Args[:cmdIndices[0]])
if err != nil {
return nil, fmt.Errorf("failed to parse interval")
}

var interval, timeout, startPeriod time.Duration
var retries int
for _, flag := range flags {
if val, ok, err := parseFlag(flag, "interval"); err != nil {
return nil, base.err(err)
} else if ok {
interval, err = time.ParseDuration(val)
if err != nil {
return nil, fmt.Errorf("failed to parse interval")
}
continue
}

if val, ok, err := parseFlag(flag, "timeout"); err != nil {
return nil, base.err(err)
} else if ok {
timeout, err = time.ParseDuration(val)
if err != nil {
return nil, fmt.Errorf("failed to parse timeout")
}
continue
}

if val, ok, err := parseFlag(flag, "start-period"); err != nil {
return nil, base.err(err)
} else if ok {
startPeriod, err = time.ParseDuration(val)
if err != nil {
return nil, fmt.Errorf("failed to parse start-period")
}
continue
}

if val, ok, err := parseFlag(flag, "retries"); err != nil {
return nil, base.err(err)
} else if ok {
retries, err = strconv.Atoi(val)
if err != nil {
return nil, fmt.Errorf("failed to parse retries")
}
continue
}

return nil, base.err(fmt.Errorf("Unsupported flag %s", flag))
}

// Replace variables.
if state.stageVars == nil {
return nil, base.err(errBeforeFirstFrom)
}
remaining := base.Args[cmdIndices[1]:]
replaced, err := replaceVariables(remaining, state.stageVars)
if err != nil {
return nil, base.err(fmt.Errorf("Failed to replace variables in input: %s", err))
}
remaining = replaced

// Parse CMD.
if cmd, ok := parseJSONArray(remaining); ok {
if len(cmd) == 0 {
return nil, base.err(fmt.Errorf("missing CMD arguments: %s", err))
}

return &HealthcheckDirective{
baseDirective: base,
Interval: interval,
Timeout: timeout,
StartPeriod: startPeriod,
Retries: retries,
Test: append([]string{"CMD"}, cmd...),
}, nil
}

// Verify cmd arg is a valid array, but return the whole arg as one string.
args, err := splitArgs(remaining)
if err != nil {
return nil, base.err(err)
}
if len(args) == 0 {
return nil, base.err(fmt.Errorf("missing CMD arguments: %s", err))
}

return &HealthcheckDirective{
baseDirective: base,
Interval: interval,
Timeout: timeout,
StartPeriod: startPeriod,
Retries: retries,
Test: append([]string{"CMD-SHELL"}, remaining),
}, nil
}

// Add this command to the build stage.
func (d *HealthcheckDirective) update(state *parsingState) error {
return state.addToCurrStage(d)
}
Loading

0 comments on commit 74e9b38

Please sign in to comment.