slim/pkg/docker/dockerfile/reverse/reverse.go
guangwu 09058e2d78
chore: use fmt.Errorf(...) instead of errors.New(fmt.Sprintf(...)) (#542)
Signed-off-by: guoguangwu <guoguangwu@magic-shield.com>
2023-07-05 23:17:57 -07:00

927 lines
29 KiB
Go

package reverse
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"sort"
"strconv"
"strings"
"time"
"github.com/dustin/go-humanize"
docker "github.com/fsouza/go-dockerclient"
"github.com/google/shlex"
log "github.com/sirupsen/logrus"
)
var (
ErrBadInstPrefix = errors.New("bad instruction prefix")
)
// Dockerfile represents the reverse engineered Dockerfile info
type Dockerfile struct {
Lines []string `json:"lines,omitempty"`
Maintainers []string `json:"maintainers,omitempty"`
AllUsers []string `json:"all_users,omitempty"`
ExeUser string `json:"exe_user,omitempty"`
ExposedPorts []string `json:"exposed_ports,omitempty"`
ImageStack []*ImageInfo `json:"image_stack"`
AllInstructions []*InstructionInfo `json:"all_instructions"`
InstructionGroups [][]*InstructionInfo `json:"instruction_groups"`
InstructionGroupsReverse [][]*InstructionInfo `json:"instruction_groups_reverse"`
HasOnbuild bool `json:"has_onbuild"`
}
type ImageInfo struct {
IsTopImage bool `json:"is_top_image"`
ID string `json:"id"`
FullName string `json:"full_name"`
RepoName string `json:"repo_name"`
VersionTag string `json:"version_tag"`
RawTags []string `json:"raw_tags,omitempty"`
CreateTime string `json:"create_time"`
NewSize int64 `json:"new_size"`
NewSizeHuman string `json:"new_size_human"`
BaseImageID string `json:"base_image_id,omitempty"`
Instructions []*InstructionInfo `json:"instructions"`
}
type InstructionInfo struct {
Type string `json:"type"`
Time string `json:"time"`
//Time time.Time `json:"time"`
IsLastInstruction bool `json:"is_last_instruction,omitempty"`
IsNop bool `json:"is_nop"`
IsExecForm bool `json:"is_exec_form,omitempty"` //is exec/json format (a valid field for RUN, ENTRYPOINT, CMD)
LocalImageExists bool `json:"local_image_exists"`
IntermediateImageID string `json:"intermediate_image_id,omitempty"`
LayerIndex int `json:"layer_index"` //-1 for an empty layer
LayerID string `json:"layer_id,omitempty"`
LayerFSDiffID string `json:"layer_fsdiff_id,omitempty"`
Size int64 `json:"size"`
SizeHuman string `json:"size_human,omitempty"`
Params string `json:"params,omitempty"`
CommandSnippet string `json:"command_snippet"`
CommandAll string `json:"command_all"`
SystemCommands []string `json:"system_commands,omitempty"`
Comment string `json:"comment,omitempty"`
Author string `json:"author,omitempty"`
EmptyLayer bool `json:"empty_layer,omitempty"`
instPosition string
imageFullName string
RawTags []string `json:"raw_tags,omitempty"`
Target string `json:"target,omitempty"` //for ADD and COPY
SourceType string `json:"source_type,omitempty"` //for ADD and COPY
IsBuildKitInstruction bool `json:"is_buildkit_instruction,omitempty"`
BuildKitInfo string `json:"buildkit_info,omitempty"`
TimeValue time.Time `json:"-"`
InstSetTimeBucket time.Time `json:"inst_set_time_bucket,omitempty"`
InstSetTimeIndex int `json:"inst_set_time_index"`
InstSetTimeReverseIndex int `json:"inst_set_time_reverse_index"`
}
const (
buildkitCreatedBySuffix = "# buildkit"
buildkitPrefix = "buildkit."
buildkitDockerfilePrefix = "buildkit.dockerfile."
buildkitDockerfileV0 = "buildkit.dockerfile.v0"
)
//The 'History' API doesn't expose the 'author' in the records it returns
//The 'author' field is useful in detecting if it's a Dockerfile instruction
//or if it's created with something else.
//One option is to combine the 'History' API data with the history data
//from the image config JSON embedded in the image.
//Another option is to rely on '#(nop)'.
const (
defaultRunInstShell = "/bin/sh"
notRunInstPrefix = "/bin/sh -c #(nop) "
runInstShellPrefix = "/bin/sh -c " //without any ARG params
runInstArgsPrefix = "|"
)
const (
//MAINTAINER:
instTypeMaintainer = "MAINTAINER"
instPrefixMaintainer = "MAINTAINER "
//ENTRYPOINT:
instTypeEntrypoint = "ENTRYPOINT"
instPrefixEntrypoint = "ENTRYPOINT "
//CMD:
instTypeCmd = "CMD"
instPrefixCmd = "CMD "
//USER:
instTypeUser = "USER"
instPrefixUser = "USER "
//EXPOSE:
instTypeExpose = "EXPOSE"
instPrefixExpose = "EXPOSE "
//WORKDIR:
instTypeWorkdir = "WORKDIR"
instPrefixWorkdir = "WORKDIR "
//HEALTHCHECK:
instTypeHealthcheck = "HEALTHCHECK"
instPrefixHealthcheck = "HEALTHCHECK "
instPrefixBasicEncHealthcheck = "HEALTHCHECK --"
//ONBUILD:
instTypeOnbuild = "ONBUILD"
//RUN:
instTypeRun = "RUN"
instPrefixRun = "RUN "
//ADD:
instTypeAdd = "ADD"
//COPY:
instTypeCopy = "COPY"
instTypeVolume = "VOLUME"
instTypeEnv = "ENV"
instTypeLabel = "LABEL"
instTypeStopSignal = "STOPSIGNAL"
instTypeShell = "SHELL"
instTypeArg = "ARG" //shouldn't see it as an standalone instruction
)
var instructionTypes = map[string]struct{}{
instTypeRun: {},
instTypeEntrypoint: {},
instTypeCmd: {},
instTypeUser: {},
instTypeExpose: {},
instTypeWorkdir: {},
instTypeHealthcheck: {},
instTypeOnbuild: {},
instTypeAdd: {},
instTypeCopy: {},
instTypeMaintainer: {},
instTypeVolume: {},
instTypeEnv: {},
instTypeLabel: {},
instTypeStopSignal: {},
instTypeShell: {},
instTypeArg: {},
}
func isInstructionType(input string) bool {
_, found := instructionTypes[input]
return found
}
func hasInstructionPrefix(input string) bool {
if !strings.Contains(input, " ") {
return false
}
parts := strings.SplitN(input, " ", 2)
return isInstructionType(parts[0])
}
const (
mapPrefix = "map["
portMapKeySuffix = ":{}]"
)
type tbrecord struct {
index int
instruction *InstructionInfo
tb time.Time
}
const tbDuration = (15 * time.Minute)
// DockerfileFromHistoryData recreates Dockerfile information from container image history
func DockerfileFromHistoryData(data string) (*Dockerfile, error) {
var imageHistory []docker.ImageHistory
if err := json.NewDecoder(strings.NewReader(data)).Decode(&imageHistory); err != nil {
return nil, err
}
return DockerfileFromHistoryStruct(imageHistory)
}
// DockerfileFromHistory recreates Dockerfile information from container image history
func DockerfileFromHistory(apiClient *docker.Client, imageID string) (*Dockerfile, error) {
//TODO: make it possible to pass the history information as a param
//TODO: pass the other image metadata (including OCI and buildkit base image info)
imageHistory, err := apiClient.ImageHistory(imageID)
if err != nil {
return nil, err
}
return DockerfileFromHistoryStruct(imageHistory)
}
// DockerfileFromHistoryStruct recreates Dockerfile information from container image history
func DockerfileFromHistoryStruct(imageHistory []docker.ImageHistory) (*Dockerfile, error) {
var out Dockerfile
log.Tracef("\n\nreverse.DockerfileFromHistoryStruct - IMAGE HISTORY:\n%#v\n\n", imageHistory)
var timeBuckets = map[time.Time][]tbrecord{}
var reversedInstructions []*InstructionInfo
var currentImageInfo *ImageInfo
var prevImageID string
imageLayerCount := len(imageHistory)
imageLayerStart := imageLayerCount - 1
startNewImage := true
if imageLayerCount > 0 {
for idx := imageLayerStart; idx >= 0; idx-- {
rawLine := imageHistory[idx].CreatedBy
var isNop bool
var inst string
var isBuildKitInstruction bool
if strings.HasSuffix(rawLine, buildkitCreatedBySuffix) {
isBuildKitInstruction = true
rawLine = strings.TrimSuffix(rawLine, buildkitCreatedBySuffix)
}
var rawInst string
isRunInst := strings.HasPrefix(rawLine, instPrefixRun)
if isRunInst {
parts := strings.SplitN(rawLine, " ", 2)
rawInst = parts[1]
} else {
rawInst = rawLine
}
if strings.Contains(rawLine, "#(nop)") {
isNop = true
}
isExecForm := false
switch {
case len(rawInst) == 0:
inst = ""
//NOTE:
//still keeping a placeholder for the empty instructions in history
//because not all builders populate all history record fields (e.g., buildkits)
case strings.HasPrefix(rawInst, notRunInstPrefix):
//Instructions that are not RUN
inst = strings.TrimPrefix(rawInst, notRunInstPrefix)
case strings.HasPrefix(rawInst, runInstShellPrefix):
//RUN instruction in shell form
runData := strings.TrimPrefix(rawInst, runInstShellPrefix)
if strings.Contains(runData, "&&") {
parts := strings.Split(runData, "&&")
for i := range parts {
partPrefix := ""
if i != 0 {
partPrefix = "\t"
}
parts[i] = partPrefix + strings.TrimSpace(parts[i])
}
runDataFormatted := strings.Join(parts, " && \\\n")
inst = instPrefixRun + runDataFormatted
} else {
inst = instPrefixRun + runData
}
default:
//TODO: need to refactor
processed := false
//rawInst := rawLine
if strings.HasPrefix(rawInst, runInstArgsPrefix) {
var err error
inst, processed, isExecForm, err = stripRunInstArgs(rawInst) //should not be ':='
if err != nil {
log.Debugf("stripRunInstArgs: err -> %v\n", err)
}
}
if hasInstructionPrefix(rawInst) {
inst = rawInst
} else {
if !processed {
//default to RUN instruction in exec form
isExecForm = true
inst = instPrefixRun + rawInst
if outArray, err := shlex.Split(rawInst); err == nil {
var outJson bytes.Buffer
encoder := json.NewEncoder(&outJson)
encoder.SetEscapeHTML(false)
err := encoder.Encode(outArray)
if err == nil {
inst = fmt.Sprintf("RUN %s", outJson.String())
}
}
}
}
}
//NOTE: Dockerfile instructions can be any case, but the instructions from history are always uppercase
cleanInst := strings.TrimSpace(inst)
if strings.HasPrefix(cleanInst, instPrefixEntrypoint) {
cleanInst = strings.Replace(cleanInst, "&{[", "[", -1)
cleanInst = strings.Replace(cleanInst, "]}", "]", -1)
entrypointShellFormPrefix := `ENTRYPOINT ["/bin/sh" "-c" "`
if strings.HasPrefix(cleanInst, entrypointShellFormPrefix) {
instData := strings.TrimPrefix(cleanInst, entrypointShellFormPrefix)
instData = strings.TrimSuffix(instData, `"]`)
cleanInst = instPrefixEntrypoint + instData
} else {
isExecForm = true
instData := strings.TrimPrefix(cleanInst, instPrefixEntrypoint)
instData = fixJSONArray(instData)
cleanInst = instPrefixEntrypoint + instData
}
}
if strings.HasPrefix(cleanInst, instPrefixCmd) {
cmdShellFormPrefix := `CMD ["/bin/sh" "-c" "`
if strings.HasPrefix(cleanInst, cmdShellFormPrefix) {
instData := strings.TrimPrefix(cleanInst, cmdShellFormPrefix)
instData = strings.TrimSuffix(instData, `"]`)
cleanInst = instPrefixCmd + instData
} else {
isExecForm = true
instData := strings.TrimPrefix(cleanInst, instPrefixCmd)
instData = fixJSONArray(instData)
cleanInst = instPrefixCmd + instData
}
}
if strings.HasPrefix(cleanInst, instPrefixMaintainer) {
parts := strings.SplitN(cleanInst, " ", 2)
if len(parts) == 2 {
maintainer := strings.TrimSpace(parts[1])
out.Maintainers = append(out.Maintainers, maintainer)
} else {
log.Infof("ReverseDockerfileFromHistory - MAINTAINER - unexpected number of user parts - %v", len(parts))
}
}
if strings.HasPrefix(cleanInst, instPrefixUser) {
parts := strings.SplitN(cleanInst, " ", 2)
if len(parts) == 2 {
userName := strings.TrimSpace(parts[1])
out.AllUsers = append(out.AllUsers, userName)
out.ExeUser = userName
} else {
log.Infof("ReverseDockerfileFromHistory - unexpected number of user parts - %v", len(parts))
}
}
if strings.HasPrefix(cleanInst, instPrefixExpose) {
parts := strings.SplitN(cleanInst, " ", 2)
if len(parts) == 2 {
portInfo := strings.TrimSpace(parts[1])
if strings.HasPrefix(portInfo, mapPrefix) &&
strings.HasSuffix(portInfo, portMapKeySuffix) {
portInfo = strings.TrimPrefix(portInfo, mapPrefix)
portInfo = strings.TrimSuffix(portInfo, portMapKeySuffix)
cleanInst = fmt.Sprintf("EXPOSE %s", portInfo)
}
out.ExposedPorts = append(out.ExposedPorts, portInfo)
} else {
log.Infof("ReverseDockerfileFromHistory - unexpected number of expose parts - %v", len(parts))
}
}
instInfo := InstructionInfo{
IsNop: isNop,
IsExecForm: isExecForm,
CommandAll: cleanInst,
Time: time.Unix(imageHistory[idx].Created, 0).UTC().Format(time.RFC3339),
TimeValue: time.Unix(imageHistory[idx].Created, 0),
Comment: imageHistory[idx].Comment,
RawTags: imageHistory[idx].Tags,
Size: imageHistory[idx].Size,
IsBuildKitInstruction: isBuildKitInstruction,
InstSetTimeIndex: -1,
InstSetTimeReverseIndex: -1,
}
instInfo.InstSetTimeBucket = instInfo.TimeValue.Truncate(tbDuration)
if strings.HasPrefix(instInfo.Comment, buildkitPrefix) {
instInfo.IsBuildKitInstruction = true
}
instParts := strings.SplitN(cleanInst, " ", 2)
if len(instParts) == 2 {
instInfo.Type = instParts[0]
}
if instInfo.Type == instTypeOnbuild {
out.HasOnbuild = true
}
if instInfo.CommandAll == "" {
instInfo.Type = "NONE"
instInfo.CommandAll = "# no instruction info"
}
if instInfo.Type == instTypeRun {
var cmdParts []string
cmds := strings.Replace(instParts[1], "\\", "", -1)
if strings.Contains(cmds, "&&") {
cmdParts = strings.Split(cmds, "&&")
} else {
cmdParts = strings.Split(cmds, ";")
}
for _, cmd := range cmdParts {
cmd = strings.TrimSpace(cmd)
cmd = strings.Replace(cmd, "\t", "", -1)
cmd = strings.Replace(cmd, "\n", "", -1)
instInfo.SystemCommands = append(instInfo.SystemCommands, cmd)
}
} else {
if len(instParts) == 2 {
instInfo.Params = instParts[1]
}
}
if instInfo.Type == instTypeWorkdir {
instInfo.SystemCommands = append(instInfo.SystemCommands, fmt.Sprintf("mkdir -p %s", instParts[1]))
}
switch instInfo.Type {
case instTypeAdd, instTypeCopy:
if strings.Contains(instInfo.Params, ":") && strings.Contains(instInfo.Params, " in ") {
pparts := strings.SplitN(instInfo.Params, ":", 2)
if len(pparts) == 2 {
instInfo.SourceType = pparts[0]
tparts := strings.SplitN(pparts[1], " in ", 2)
if len(tparts) == 2 {
instInfo.Target = tparts[1]
instInfo.CommandAll = fmt.Sprintf("%s %s:%s %s",
instInfo.Type,
instInfo.SourceType,
tparts[0],
instInfo.Target)
}
}
}
}
if instInfo.Type == instTypeHealthcheck {
healthInst, _, err := deserialiseHealtheckInstruction(instInfo.CommandAll)
if err != nil {
log.Errorf("ReverseDockerfileFromHistory - HEALTHCHECK - deserialiseHealtheckInstruction - %v", err)
}
instInfo.CommandAll = healthInst
}
if len(instInfo.CommandAll) > 44 {
instInfo.CommandSnippet = fmt.Sprintf("%s...", instInfo.CommandAll[0:44])
} else {
instInfo.CommandSnippet = instInfo.CommandAll
}
if instInfo.Size > 0 {
instInfo.SizeHuman = humanize.Bytes(uint64(instInfo.Size))
}
if imageHistory[idx].ID != "<missing>" {
instInfo.LocalImageExists = true
instInfo.IntermediateImageID = imageHistory[idx].ID
}
if startNewImage {
startNewImage = false
currentImageInfo = &ImageInfo{
BaseImageID: prevImageID,
NewSize: 0,
}
}
currentImageInfo.NewSize += imageHistory[idx].Size
currentImageInfo.Instructions = append(currentImageInfo.Instructions, &instInfo)
out.AllInstructions = append(out.AllInstructions, &instInfo)
instPosition := "intermediate"
if idx == imageLayerStart {
instPosition = "first" //first instruction in the list
}
if idx == 0 || (len(imageHistory[idx].Tags) > 0) {
instPosition = "last" //last in an image
currentImageInfo.ID = imageHistory[idx].ID
prevImageID = currentImageInfo.ID
if instInfo.IntermediateImageID == currentImageInfo.ID {
instInfo.IntermediateImageID = ""
instInfo.IsLastInstruction = true
}
currentImageInfo.CreateTime = instInfo.Time
currentImageInfo.RawTags = imageHistory[idx].Tags
if len(imageHistory[idx].Tags) > 0 {
instInfo.imageFullName = imageHistory[idx].Tags[0]
currentImageInfo.FullName = imageHistory[idx].Tags[0]
if tagInfo := strings.Split(imageHistory[idx].Tags[0], ":"); len(tagInfo) > 1 {
currentImageInfo.RepoName = tagInfo[0]
currentImageInfo.VersionTag = tagInfo[1]
}
}
currentImageInfo.NewSizeHuman = humanize.Bytes(uint64(currentImageInfo.NewSize))
out.ImageStack = append(out.ImageStack, currentImageInfo)
startNewImage = true
}
instInfo.instPosition = instPosition
reversedInstructions = append(reversedInstructions, &instInfo)
tbr := tbrecord{
index: len(reversedInstructions) - 1,
instruction: &instInfo,
tb: instInfo.InstSetTimeBucket,
}
timeBuckets[instInfo.InstSetTimeBucket] = append(timeBuckets[instInfo.InstSetTimeBucket], tbr)
}
if currentImageInfo != nil {
currentImageInfo.IsTopImage = true
}
}
tkeys := make([]time.Time, 0, len(timeBuckets))
for k := range timeBuckets {
tkeys = append(tkeys, k)
}
sort.SliceStable(tkeys, func(i, j int) bool { return tkeys[i].Before(tkeys[j]) })
tkListLen := len(tkeys)
for i, k := range tkeys {
tbrList := timeBuckets[k]
for _, tbr := range tbrList {
tbr.instruction.InstSetTimeIndex = i
tbr.instruction.InstSetTimeReverseIndex = tkListLen - 1 - i
}
}
out.InstructionGroups = make([][]*InstructionInfo, tkListLen)
out.InstructionGroupsReverse = make([][]*InstructionInfo, tkListLen)
//Always adding "FROM scratch" as the first line
//GOAL: to have a reversed Dockerfile that can be used to build a new image
out.Lines = append(out.Lines, "FROM scratch")
prevInstSetTimeIndex := -1
for idx, instInfo := range reversedInstructions {
if instInfo.instPosition == "first" {
out.Lines = append(out.Lines, "# new image")
}
if instInfo.InstSetTimeIndex != prevInstSetTimeIndex {
out.Lines = append(out.Lines, fmt.Sprintf("\n# instruction set group %d\n", instInfo.InstSetTimeIndex+1))
prevInstSetTimeIndex = instInfo.InstSetTimeIndex
}
out.InstructionGroups[instInfo.InstSetTimeIndex] = append(out.InstructionGroups[instInfo.InstSetTimeIndex], instInfo)
out.InstructionGroupsReverse[instInfo.InstSetTimeReverseIndex] = append(out.InstructionGroupsReverse[instInfo.InstSetTimeReverseIndex], instInfo)
if instInfo.Comment != "" {
outComment := fmt.Sprintf("# %s", instInfo.Comment)
if instInfo.IsBuildKitInstruction {
outComment = fmt.Sprintf("%s (a buildkit instruction)", outComment)
}
out.Lines = append(out.Lines, outComment)
} else if instInfo.IsBuildKitInstruction {
out.Lines = append(out.Lines, "# a buildkit instruction")
}
out.Lines = append(out.Lines, instInfo.CommandAll)
if instInfo.instPosition == "last" {
commentText := fmt.Sprintf("# end of image: %s (id: %s tags: %s)",
instInfo.imageFullName, instInfo.IntermediateImageID, strings.Join(instInfo.RawTags, ","))
out.Lines = append(out.Lines, commentText)
out.Lines = append(out.Lines, "")
if idx < (len(reversedInstructions) - 1) {
out.Lines = append(out.Lines, "# new image")
}
}
}
log.Debugf("IMAGE INSTRUCTIONS:")
for _, iiLine := range out.Lines {
log.Debug(iiLine)
}
return &out, nil
//BASE LAYER IDENTIFICATION:
//* tags from the instruction history
//* instruction time-based clustering
//* instruction patterns (e.g., base images often have their own ENTRYPOINT/CMD instructions)
//* base image metadata from the image labels (e.g., "org.opencontainers.image.base.digest" OCI label)
//* database with pre-indexed common base image digests (will require a network lookup)
}
func stripRunInstArgs(rawInst string) (string, bool, bool, error) {
parts := strings.SplitN(rawInst, " ", 2)
if len(parts) == 2 {
withArgs := strings.TrimSpace(parts[1])
argNumStr := parts[0][1:]
argNum, err := strconv.Atoi(argNumStr)
if err == nil {
if withArgsArray, err := shlex.Split(withArgs); err == nil {
if len(withArgsArray) > argNum {
rawInstParts := withArgsArray[argNum:]
isExecForm := false
processed := true
inst := ""
if len(rawInstParts) > 2 &&
rawInstParts[0] == defaultRunInstShell &&
rawInstParts[1] == "-c" {
isExecForm = false
rawInstParts = rawInstParts[2:]
inst = fmt.Sprintf("RUN %s", strings.Join(rawInstParts, " "))
inst = strings.TrimSpace(inst)
} else {
isExecForm = true
var outJson bytes.Buffer
encoder := json.NewEncoder(&outJson)
encoder.SetEscapeHTML(false)
err = encoder.Encode(rawInstParts)
if err == nil {
inst = fmt.Sprintf("RUN %s", outJson.String())
}
}
return inst, processed, isExecForm, err
} else {
log.Infof("reverse.stripRunInstArgs - RUN with ARGs - malformed - %v (%v)", rawInst, err)
}
} else {
log.Infof("reverse.stripRunInstArgs - RUN with ARGs - malformed - %v (%v)", rawInst, err)
}
} else {
log.Infof("reverse.stripRunInstArgs - RUN with ARGs - malformed number of ARGs - %v (%v)", rawInst, err)
}
} else {
log.Infof("reverse.stripRunInstArgs - RUN with ARGs - unexpected number of parts - %v", rawInst)
}
return "", false, false, nil
}
// SaveDockerfileData saves the Dockerfile information to a file
func SaveDockerfileData(fatImageDockerfileLocation string, fatImageDockerfileLines []string) error {
var data bytes.Buffer
data.WriteString(strings.Join(fatImageDockerfileLines, "\n"))
return ioutil.WriteFile(fatImageDockerfileLocation, data.Bytes(), 0644)
}
func fixJSONArray(in string) string {
data := in
if data[0] == '[' {
data = data[1 : len(data)-1]
}
outArray, err := shlex.Split(data)
if err != nil {
return in
}
var out bytes.Buffer
encoder := json.NewEncoder(&out)
encoder.SetEscapeHTML(false)
err = encoder.Encode(outArray)
if err != nil {
return in
}
return out.String()
}
func deserialiseHealtheckInstruction(data string) (string, *docker.HealthConfig, error) {
//Example:
// HEALTHCHECK &{["CMD" "/healthcheck" "8080"] "5s" "10s" "0s" '\x03'}
// HEALTHCHECK --interval=5s --timeout=10s --retries=3 CMD [ "/healthcheck", "8080" ]
//Note: CMD can be specified with both formats (shell and json)
//Buildah example (raw/full):
// /bin/sh -c #(nop) HEALTHCHECK --interval=5m --timeout=3s CMD curl -f http://localhost/ || exit 1
cleanInst := strings.TrimSpace(data)
if !strings.HasPrefix(cleanInst, instPrefixHealthcheck) {
return "", nil, ErrBadInstPrefix
}
var config docker.HealthConfig
var strTest string
if strings.HasPrefix(cleanInst, instPrefixBasicEncHealthcheck) || !strings.Contains(cleanInst, "&{[") {
//handling the basic Buildah encoding
var err error
if strings.Contains(cleanInst, "--interval=") {
vparts := strings.SplitN(cleanInst, "--interval=", 2)
vparts = strings.SplitN(vparts[1], " ", 2)
config.Interval, err = time.ParseDuration(vparts[0])
if err != nil {
log.Errorf("[%s] config.Interval err = %v", vparts[0], err)
}
}
if strings.Contains(cleanInst, "--timeout=") {
vparts := strings.SplitN(cleanInst, "--timeout=", 2)
vparts = strings.SplitN(vparts[1], " ", 2)
config.Timeout, err = time.ParseDuration(vparts[0])
if err != nil {
log.Errorf("[%s] config.Timeout err = %v", vparts[0], err)
}
}
if strings.Contains(cleanInst, "--start-period=") {
vparts := strings.SplitN(cleanInst, "--start-period=", 2)
vparts = strings.SplitN(vparts[1], " ", 2)
config.StartPeriod, err = time.ParseDuration(vparts[0])
if err != nil {
log.Errorf("[%s] config.StartPeriod err = %v", vparts[0], err)
}
}
if strings.Contains(cleanInst, "--retries=") {
vparts := strings.SplitN(cleanInst, "--retries=", 2)
vparts = strings.SplitN(vparts[1], " ", 2)
retries, err := strconv.ParseInt(vparts[0], 16, 64)
if err != nil {
log.Errorf("[%s] config.Retries err = %v", vparts[0], err)
} else {
config.Retries = int(retries)
}
}
if strings.Contains(cleanInst, " CMD ") {
parts := strings.SplitN(cleanInst, " CMD ", 2)
strTest = fmt.Sprintf("CMD %s", parts[1])
config.Test = []string{"CMD", parts[1]}
}
} else {
cleanInst = strings.Replace(cleanInst, "&{[", "", -1)
//Splits the string into two parts - first part pointer to array of string and rest of the string with } in end.
instParts := strings.SplitN(cleanInst, "]", 2)
// Cleans HEALTHCHECK part and splits the first part further
parts := strings.SplitN(instParts[0], " ", 2)
// joins the first part of the string
instPart1 := strings.Join(parts[1:], " ")
// removes quotes from the first part of the string
instPart1 = strings.ReplaceAll(instPart1, "\"", "")
// cleans it to assign it to the pointer config.Test
config.Test = strings.Split(instPart1, " ")
// removes the } from the second part of the string
instPart2 := strings.Replace(instParts[1], "}", "", -1)
// removes extra spaces from string
instPart2 = strings.TrimSpace(instPart2)
paramParts := strings.SplitN(instPart2, " ", 4)
for i, param := range paramParts {
paramParts[i] = strings.Trim(param, "\"'")
}
var err error
config.Interval, err = time.ParseDuration(paramParts[0])
if err != nil {
log.Errorf("[%s] config.Interval err = %v", paramParts[0], err)
}
config.Timeout, err = time.ParseDuration(paramParts[1])
if err != nil {
log.Errorf("[%s] config.Timeout err = %v", paramParts[1], err)
}
config.StartPeriod, err = time.ParseDuration(paramParts[2])
if err != nil {
log.Errorf("[%s] config.StartPeriod err = %v", paramParts[2], err)
}
var retries int64
if strings.Index(paramParts[3], `\x`) != -1 {
// retries are hex encoded
retries, err = strconv.ParseInt(strings.TrimPrefix(paramParts[3], `\x`), 16, 64)
} else if strings.Index(paramParts[3], `\U`) != -1 {
// retries are a unicode string
retries, err = strconv.ParseInt(strings.TrimPrefix(paramParts[3], `\U`), 16, 64)
} else if strings.Index(paramParts[3], `\`) == 0 {
// retries is printed as a C-escape
if len(paramParts[3]) != 2 {
err = fmt.Errorf("expected retries (%s) to be an escape sequence", paramParts[3])
} else {
escapeCodes := map[byte]int64{
byte('a'): 7,
byte('b'): 8,
byte('t'): 9,
byte('n'): 10,
byte('v'): 11,
byte('f'): 12,
byte('r'): 13,
}
var ok bool
if retries, ok = escapeCodes[(paramParts[3])[1]]; !ok {
err = fmt.Errorf("got an invalid escape sequence: %s", paramParts[3])
}
}
} else {
retries = int64((paramParts[3])[0])
}
if err != nil {
log.Errorf("[%s] config.Retries err = %v", paramParts[3], err)
} else {
config.Retries = int(retries)
}
var testType string
if len(config.Test) > 0 {
testType = config.Test[0]
}
switch testType {
case "NONE":
strTest = "NONE"
case "CMD":
if len(config.Test) == 1 {
strTest = "CMD []"
} else {
strTest = fmt.Sprintf(`CMD ["%s"]`, strings.Join(config.Test[1:], `", "`))
}
case "CMD-SHELL":
cmdShell := strings.Join(config.Test[1:], " ")
strTest = fmt.Sprintf("CMD %s", cmdShell)
config.Test = []string{config.Test[0], cmdShell}
}
}
defaultTimeout := false
defaultInterval := false
defaultRetries := false
defaultStartPeriod := false
if config.Timeout == 0 {
defaultTimeout = true
config.Timeout = 30 * time.Second
}
if config.Interval == 0 {
defaultInterval = true
config.Interval = 30 * time.Second
}
if config.Retries == 0 {
defaultRetries = true
config.Retries = 3
}
if config.StartPeriod == 0 {
defaultStartPeriod = true
}
type HealthCheckFlag struct {
flagFmtStr string
isDefault bool
value interface{}
}
healthInst := "HEALTHCHECK"
for _, flag := range []HealthCheckFlag{
{flagFmtStr: "--interval=%v", isDefault: defaultInterval, value: config.Interval},
{flagFmtStr: "--timeout=%v", isDefault: defaultTimeout, value: config.Timeout},
{flagFmtStr: "--start-period=%v", isDefault: defaultStartPeriod, value: config.StartPeriod},
{flagFmtStr: "--retries=%d", isDefault: defaultRetries, value: config.Retries},
} {
if !flag.isDefault {
healthInst = healthInst + " " + fmt.Sprintf(flag.flagFmtStr, flag.value)
}
}
healthInst += " " + strTest
if strTest == "NONE" {
healthInst = "HEALTHCHECK NONE"
}
return healthInst, &config, nil
}
//
// https://docs.docker.com/engine/reference/builder/
//