Consolidate exec package with go-execute

This patch consolidates the exec package for docker build so that
it uses the the go-execute package used in other OpenFaaS projects.

The aim is to allow for conditional printing of stdio whilst also
being able to capture the output.

In a future PR a CLI animation can replace the Docker build, which
will be default, but optional. If an error is found then the
result of the build will be buffered and available to print to the
user.

This change stops Docker from printing progress bars when
downloading layers. Instead a line is printed when pulling and
when a layer is complete.

* Tested for faas-cli build with multiple functions using the
sample stack.yml and --parallel=1/4.

* Adds StreamStdio option and updates Docker build version to use
Go 1.12.

* Add complete build time to output

* Add duration of each build to output

* Add --quiet flag for faas-cli build

* The --quiet flag hides output from Docker during the execution
of the docker build.

Signed-off-by: Alex Ellis (OpenFaaS Ltd) <alexellis2@gmail.com>
This commit is contained in:
Alex Ellis (OpenFaaS Ltd)
2019-12-06 14:08:37 +00:00
committed by Alex Ellis
parent 83fd873d45
commit 38ecd73a60
14 changed files with 159 additions and 51 deletions

View File

@@ -1,14 +1,11 @@
sudo: required sudo: required
language: generic language: generic
services:
- docker
addons: services:
apt: - docker
packages:
- docker-ce before_script:
- curl -sSLf https://get.docker.com | sed s/sleep\ 20/sleep\ 0/g | sudo -E sh
script: script:
- make build - make build
@@ -55,3 +52,5 @@ deploy:
on: on:
tags: true tags: true
env:
- GO111MODULE=off

View File

@@ -1,5 +1,8 @@
# Build stage # Build stage
FROM golang:1.11 as builder FROM golang:1.12 as builder
ENV GO111MODULE=off
ENV CGO_ENABLED=0
WORKDIR /usr/bin/ WORKDIR /usr/bin/
RUN curl -sLSf https://raw.githubusercontent.com/teamserverless/license-check/master/get.sh | sh RUN curl -sLSf https://raw.githubusercontent.com/teamserverless/license-check/master/get.sh | sh
@@ -13,8 +16,10 @@ RUN test -z "$(gofmt -l $(find . -type f -name '*.go' -not -path "./vendor/*"))"
# ldflags "-s -w" strips binary # ldflags "-s -w" strips binary
# ldflags -X injects commit version into binary # ldflags -X injects commit version into binary
RUN /usr/bin/license-check -path ./ --verbose=false "Alex Ellis" "OpenFaaS Author(s)" RUN /usr/bin/license-check -path ./ --verbose=false "Alex Ellis" "OpenFaaS Author(s)"
RUN go test $(go list ./... | grep -v /vendor/ | grep -v /template/|grep -v /build/) -cover \
&& VERSION=$(git describe --all --exact-match `git rev-parse HEAD` | grep tags | sed 's/tags\///') \ RUN go test $(go list ./... | grep -v /vendor/ | grep -v /template/|grep -v /build/|grep -v /sample/) -cover
RUN VERSION=$(git describe --all --exact-match `git rev-parse HEAD` | grep tags | sed 's/tags\///') \
&& GIT_COMMIT=$(git rev-list -1 HEAD) \ && GIT_COMMIT=$(git rev-list -1 HEAD) \
&& CGO_ENABLED=0 GOOS=linux go build --ldflags "-s -w \ && CGO_ENABLED=0 GOOS=linux go build --ldflags "-s -w \
-X github.com/openfaas/faas-cli/version.GitCommit=${GIT_COMMIT} \ -X github.com/openfaas/faas-cli/version.GitCommit=${GIT_COMMIT} \
@@ -23,7 +28,7 @@ RUN go test $(go list ./... | grep -v /vendor/ | grep -v /template/|grep -v /bui
-a -installsuffix cgo -o faas-cli -a -installsuffix cgo -o faas-cli
# Release stage # Release stage
FROM alpine:3.9 FROM alpine:3.10
RUN apk --no-cache add ca-certificates git RUN apk --no-cache add ca-certificates git

View File

@@ -1,5 +1,8 @@
# Build stage # Build stage
FROM golang:1.11 as builder FROM golang:1.12 as builder
ENV GO111MODULE=off
ENV CGO_ENABLED=0
WORKDIR /usr/bin/ WORKDIR /usr/bin/
RUN curl -sLSf https://raw.githubusercontent.com/teamserverless/license-check/master/get.sh | sh RUN curl -sLSf https://raw.githubusercontent.com/teamserverless/license-check/master/get.sh | sh
@@ -44,7 +47,7 @@ RUN /usr/bin/license-check -path ./ --verbose=false "Alex Ellis" "OpenFaaS Autho
-a -installsuffix cgo -o faas-cli-arm64 -a -installsuffix cgo -o faas-cli-arm64
# Release stage # Release stage
FROM alpine:3.9 FROM alpine:3.10
RUN apk --no-cache add ca-certificates git RUN apk --no-cache add ca-certificates git

9
Gopkg.lock generated
View File

@@ -20,6 +20,14 @@
revision = "839c75faf7f98a33d445d181f3018b5c3409a45e" revision = "839c75faf7f98a33d445d181f3018b5c3409a45e"
version = "v1.4.2" version = "v1.4.2"
[[projects]]
digest = "1:74860eb071d52337d67e9ffd6893b29affebd026505aa917ec23131576a91a77"
name = "github.com/alexellis/go-execute"
packages = ["pkg/v1"]
pruneopts = "UT"
revision = "961405ea754427780f2151adff607fa740d377f7"
version = "0.3.0"
[[projects]] [[projects]]
digest = "1:871b7cfa5fe18bfdbd4bf117c166c3cff8d3b61c8afe4e998b5b8ac0c160ca24" digest = "1:871b7cfa5fe18bfdbd4bf117c166c3cff8d3b61c8afe4e998b5b8ac0c160ca24"
name = "github.com/alexellis/hmac" name = "github.com/alexellis/hmac"
@@ -166,6 +174,7 @@
analyzer-name = "dep" analyzer-name = "dep"
analyzer-version = 1 analyzer-version = 1
input-imports = [ input-imports = [
"github.com/alexellis/go-execute/pkg/v1",
"github.com/alexellis/hmac", "github.com/alexellis/hmac",
"github.com/docker/docker-credential-helpers/client", "github.com/docker/docker-credential-helpers/client",
"github.com/docker/docker/pkg/term", "github.com/docker/docker/pkg/term",

View File

@@ -49,3 +49,9 @@
[[constraint]] [[constraint]]
name = "github.com/pkg/errors" name = "github.com/pkg/errors"
version = "v0.8.1" version = "v0.8.1"
[[constraint]]
name = "github.com/alexellis/go-execute"
version = "0.3.0"

View File

@@ -1,4 +1,4 @@
#!/bin/sh #!/bin/bash
export eTAG="latest-dev" export eTAG="latest-dev"
echo $1 echo $1
@@ -8,8 +8,14 @@ fi
echo Building openfaas/faas-cli:$eTAG echo Building openfaas/faas-cli:$eTAG
docker build --build-arg http_proxy=$http_proxy --build-arg https_proxy=$https_proxy -t openfaas/faas-cli:$eTAG . && \ docker build --build-arg http_proxy=$http_proxy --build-arg https_proxy=$https_proxy -t openfaas/faas-cli:$eTAG .
docker create --name faas-cli openfaas/faas-cli:$eTAG && \
docker cp faas-cli:/usr/bin/faas-cli . && \
docker rm -f faas-cli
if [ $? == 0 ] ; then
docker create --name faas-cli openfaas/faas-cli:$eTAG && \
docker cp faas-cli:/usr/bin/faas-cli . && \
docker rm -f faas-cli
else
exit 1
fi

View File

@@ -11,7 +11,7 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/openfaas/faas-cli/exec" v1execute "github.com/alexellis/go-execute/pkg/v1"
"github.com/openfaas/faas-cli/schema" "github.com/openfaas/faas-cli/schema"
"github.com/openfaas/faas-cli/stack" "github.com/openfaas/faas-cli/stack"
vcs "github.com/openfaas/faas-cli/versioncontrol" vcs "github.com/openfaas/faas-cli/versioncontrol"
@@ -22,7 +22,7 @@ import (
const AdditionalPackageBuildArg = "ADDITIONAL_PACKAGE" const AdditionalPackageBuildArg = "ADDITIONAL_PACKAGE"
// BuildImage construct Docker image from function parameters // BuildImage construct Docker image from function parameters
func BuildImage(image string, handler string, functionName string, language string, nocache bool, squash bool, shrinkwrap bool, buildArgMap map[string]string, buildOptions []string, tagMode schema.BuildFormat, buildLabelMap map[string]string) error { func BuildImage(image string, handler string, functionName string, language string, nocache bool, squash bool, shrinkwrap bool, buildArgMap map[string]string, buildOptions []string, tagMode schema.BuildFormat, buildLabelMap map[string]string, quietBuild bool) error {
if stack.IsValidTemplate(language) { if stack.IsValidTemplate(language) {
branch, version, err := GetImageTagValues(tagMode) branch, version, err := GetImageTagValues(tagMode)
@@ -67,9 +67,25 @@ func BuildImage(image string, handler string, functionName string, language stri
BuildLabelMap: buildLabelMap, BuildLabelMap: buildLabelMap,
} }
spaceSafeCmdLine := getDockerBuildCommand(dockerBuildVal) command, args := getDockerBuildCommand(dockerBuildVal)
task := v1execute.ExecTask{
Cwd: tempPath,
Command: command,
Args: args,
StreamStdio: !quietBuild,
}
res, err := task.Execute()
if err != nil {
return err
}
if res.ExitCode != 0 {
return fmt.Errorf("[%s] received non-zero exit code from build, error: %s", functionName, res.Stderr)
}
exec.Command(tempPath, spaceSafeCmdLine)
fmt.Printf("Image: %s built.\n", imageName) fmt.Printf("Image: %s built.\n", imageName)
} else { } else {
@@ -113,13 +129,15 @@ func GetImageTagValues(tagType schema.BuildFormat) (branch, version string, err
return branch, version, nil return branch, version, nil
} }
func getDockerBuildCommand(build dockerBuild) []string { func getDockerBuildCommand(build dockerBuild) (string, []string) {
flagSlice := buildFlagSlice(build.NoCache, build.Squash, build.HTTPProxy, build.HTTPSProxy, build.BuildArgMap, build.BuildOptPackages, build.BuildLabelMap) flagSlice := buildFlagSlice(build.NoCache, build.Squash, build.HTTPProxy, build.HTTPSProxy, build.BuildArgMap, build.BuildOptPackages, build.BuildLabelMap)
command := []string{"docker", "build"} args := []string{"build"}
command = append(command, flagSlice...) args = append(args, flagSlice...)
command = append(command, "-t", build.Image, ".") args = append(args, "-t", build.Image, ".")
return command command := "docker"
return command, args
} }
type dockerBuild struct { type dockerBuild struct {

View File

@@ -41,14 +41,20 @@ func Test_getDockerBuildCommand_NoOpts(t *testing.T) {
BuildOptPackages: []string{}, BuildOptPackages: []string{},
} }
values := getDockerBuildCommand(dockerBuildVal) want := "build -t imagename:latest ."
wantCommand := "docker"
joined := strings.Join(values, " ") command, args := getDockerBuildCommand(dockerBuildVal)
want := "docker build -t imagename:latest ."
joined := strings.Join(args, " ")
if joined != want { if joined != want {
t.Errorf("getDockerBuildCommand want: \"%s\", got: \"%s\"", want, joined) t.Errorf("getDockerBuildCommand want: \"%s\", got: \"%s\"", want, joined)
} }
if command != wantCommand {
t.Errorf("getDockerBuildCommand want command: \"%s\", got: \"%s\"", wantCommand, command)
}
} }
func Test_getDockerBuildCommand_WithNoCache(t *testing.T) { func Test_getDockerBuildCommand_WithNoCache(t *testing.T) {
@@ -62,14 +68,21 @@ func Test_getDockerBuildCommand_WithNoCache(t *testing.T) {
BuildOptPackages: []string{}, BuildOptPackages: []string{},
} }
values := getDockerBuildCommand(dockerBuildVal) want := "build --no-cache -t imagename:latest ."
joined := strings.Join(values, " ") wantCommand := "docker"
want := "docker build --no-cache -t imagename:latest ."
command, args := getDockerBuildCommand(dockerBuildVal)
joined := strings.Join(args, " ")
if joined != want { if joined != want {
t.Errorf("getDockerBuildCommand want: \"%s\", got: \"%s\"", want, joined) t.Errorf("getDockerBuildCommand want: \"%s\", got: \"%s\"", want, joined)
} }
if command != wantCommand {
t.Errorf("getDockerBuildCommand want command: \"%s\", got: \"%s\"", wantCommand, command)
}
} }
func Test_getDockerBuildCommand_WithProxies(t *testing.T) { func Test_getDockerBuildCommand_WithProxies(t *testing.T) {
@@ -83,14 +96,21 @@ func Test_getDockerBuildCommand_WithProxies(t *testing.T) {
BuildOptPackages: []string{}, BuildOptPackages: []string{},
} }
values := getDockerBuildCommand(dockerBuildVal) want := "build --build-arg http_proxy=http://127.0.0.1:3128 --build-arg https_proxy=https://127.0.0.1:3128 -t imagename:latest ."
joined := strings.Join(values, " ") wantCommand := "docker"
want := "docker build --build-arg http_proxy=http://127.0.0.1:3128 --build-arg https_proxy=https://127.0.0.1:3128 -t imagename:latest ."
command, args := getDockerBuildCommand(dockerBuildVal)
joined := strings.Join(args, " ")
if joined != want { if joined != want {
t.Errorf("getDockerBuildCommand want: \"%s\", got: \"%s\"", want, joined) t.Errorf("getDockerBuildCommand want: \"%s\", got: \"%s\"", want, joined)
} }
if command != wantCommand {
t.Errorf("getDockerBuildCommand want command: \"%s\", got: \"%s\"", wantCommand, command)
}
} }
func Test_getDockerBuildCommand_WithBuildArg(t *testing.T) { func Test_getDockerBuildCommand_WithBuildArg(t *testing.T) {
@@ -105,7 +125,7 @@ func Test_getDockerBuildCommand_WithBuildArg(t *testing.T) {
BuildOptPackages: []string{}, BuildOptPackages: []string{},
} }
values := getDockerBuildCommand(dockerBuildVal) _, values := getDockerBuildCommand(dockerBuildVal)
joined := strings.Join(values, " ") joined := strings.Join(values, " ")
wantArg1 := "--build-arg USERNAME=admin" wantArg1 := "--build-arg USERNAME=admin"

View File

@@ -9,6 +9,7 @@ import (
"os" "os"
"strings" "strings"
"sync" "sync"
"time"
"github.com/morikuni/aec" "github.com/morikuni/aec"
"github.com/openfaas/faas-cli/builder" "github.com/openfaas/faas-cli/builder"
@@ -31,6 +32,7 @@ var (
buildLabels []string buildLabels []string
buildLabelMap map[string]string buildLabelMap map[string]string
envsubst bool envsubst bool
quietBuild bool
) )
func init() { func init() {
@@ -52,6 +54,8 @@ func init() {
buildCmd.Flags().BoolVar(&envsubst, "envsubst", true, "Substitute environment variables in stack.yml file") buildCmd.Flags().BoolVar(&envsubst, "envsubst", true, "Substitute environment variables in stack.yml file")
buildCmd.Flags().BoolVar(&quietBuild, "quiet", false, "Perform a quiet build, without showing output from Docker")
// Set bash-completion. // Set bash-completion.
_ = buildCmd.Flags().SetAnnotation("handler", cobra.BashCompSubdirsInDir, []string{}) _ = buildCmd.Flags().SetAnnotation("handler", cobra.BashCompSubdirsInDir, []string{})
@@ -103,6 +107,10 @@ func preRunBuild(cmd *cobra.Command, args []string) error {
buildLabelMap, err = parseMap(buildLabels, "build-label") buildLabelMap, err = parseMap(buildLabels, "build-label")
if parallel < 1 {
return fmt.Errorf("the --parallel flag must be great than 0")
}
return err return err
} }
@@ -156,11 +164,7 @@ func runBuild(cmd *cobra.Command, args []string) error {
return fmt.Errorf("could not pull templates for OpenFaaS: %v", pullErr) return fmt.Errorf("could not pull templates for OpenFaaS: %v", pullErr)
} }
if len(services.Functions) > 0 { if len(services.Functions) == 0 {
build(&services, parallel, shrinkwrap)
} else {
if len(image) == 0 { if len(image) == 0 {
return fmt.Errorf("please provide a valid --image name for your Docker image") return fmt.Errorf("please provide a valid --image name for your Docker image")
} }
@@ -170,16 +174,29 @@ func runBuild(cmd *cobra.Command, args []string) error {
if len(functionName) == 0 { if len(functionName) == 0 {
return fmt.Errorf("please provide the deployed --name of your function") return fmt.Errorf("please provide the deployed --name of your function")
} }
err := builder.BuildImage(image, handler, functionName, language, nocache, squash, shrinkwrap, buildArgMap, buildOptions, tagFormat, buildLabelMap) err := builder.BuildImage(image, handler, functionName, language, nocache, squash, shrinkwrap, buildArgMap, buildOptions, tagFormat, buildLabelMap, quietBuild)
if err != nil { if err != nil {
return err return err
} }
return nil
} }
errors := build(&services, parallel, shrinkwrap, quietBuild)
if len(errors) > 0 {
errorSummary := "Errors received during build:\n"
for _, err := range errors {
errorSummary = errorSummary + "- " + err.Error() + "\n"
}
return fmt.Errorf("%s", aec.Apply(errorSummary, aec.RedF))
}
return nil return nil
} }
func build(services *stack.Services, queueDepth int, shrinkwrap bool) { func build(services *stack.Services, queueDepth int, shrinkwrap, quietBuild bool) []error {
startOuter := time.Now()
errors := []error{}
wg := sync.WaitGroup{} wg := sync.WaitGroup{}
workChannel := make(chan stack.Function) workChannel := make(chan stack.Function)
@@ -188,23 +205,28 @@ func build(services *stack.Services, queueDepth int, shrinkwrap bool) {
for i := 0; i < queueDepth; i++ { for i := 0; i < queueDepth; i++ {
go func(index int) { go func(index int) {
for function := range workChannel { for function := range workChannel {
start := time.Now()
fmt.Printf(aec.YellowF.Apply("[%d] > Building %s.\n"), index, function.Name) fmt.Printf(aec.YellowF.Apply("[%d] > Building %s.\n"), index, function.Name)
if len(function.Language) == 0 { if len(function.Language) == 0 {
fmt.Println("Please provide a valid language for your function.") fmt.Println("Please provide a valid language for your function.")
} else { } else {
combinedBuildOptions := combineBuildOpts(function.BuildOptions, buildOptions) combinedBuildOptions := combineBuildOpts(function.BuildOptions, buildOptions)
err := builder.BuildImage(function.Image, function.Handler, function.Name, function.Language, nocache, squash, shrinkwrap, buildArgMap, combinedBuildOptions, tagFormat, buildLabelMap) err := builder.BuildImage(function.Image, function.Handler, function.Name, function.Language, nocache, squash, shrinkwrap, buildArgMap, combinedBuildOptions, tagFormat, buildLabelMap, quietBuild)
if err != nil { if err != nil {
log.Println(err) errors = append(errors, err)
} }
} }
fmt.Printf(aec.YellowF.Apply("[%d] < Building %s done.\n"), index, function.Name)
duration := time.Since(start)
fmt.Printf(aec.YellowF.Apply("[%d] < Building %s done in %1.2fs.\n"), index, function.Name, duration.Seconds())
} }
fmt.Printf(aec.YellowF.Apply("[%d] worker done.\n"), index) fmt.Printf(aec.YellowF.Apply("[%d] Worker done.\n"), index)
wg.Done() wg.Done()
}(i) }(i)
} }
for k, function := range services.Functions { for k, function := range services.Functions {
@@ -220,6 +242,9 @@ func build(services *stack.Services, queueDepth int, shrinkwrap bool) {
wg.Wait() wg.Wait()
duration := time.Since(startOuter)
fmt.Printf("\n%s\n", aec.Apply(fmt.Sprintf("Total build time: %1.2f", duration.Seconds()), aec.YellowF))
return errors
} }
// PullTemplates pulls templates from specified git remote. templateURL may be a pinned repository. // PullTemplates pulls templates from specified git remote. templateURL may be a pinned repository.

View File

@@ -26,6 +26,21 @@ func Test_build(t *testing.T) {
} }
} }
func Test_preRunBuild_ParallelOverZero(t *testing.T) {
buildCmd.ParseFlags([]string{"--parallel=0"})
got := buildCmd.PreRunE(buildCmd, nil)
if got == nil {
t.Error("Parallel should have errored about being over zero")
t.Fail()
return
}
want := "the --parallel flag must be great than 0"
if got.Error() != want {
t.Errorf("parsing error, want %s, got %s", want, got.Error())
}
}
func Test_parseBuildArgs_ValidParts(t *testing.T) { func Test_parseBuildArgs_ValidParts(t *testing.T) {
mapped, err := parseBuildArgs([]string{"k=v"}) mapped, err := parseBuildArgs([]string{"k=v"})

View File

@@ -109,7 +109,7 @@ func pushStack(services *stack.Services, queueDepth int, tagMode schema.BuildFor
} }
} }
fmt.Printf(aec.YellowF.Apply("[%d] worker done.\n"), index) fmt.Printf(aec.YellowF.Apply("[%d] Worker done.\n"), index)
wg.Done() wg.Done()
}(i) }(i)
} }

View File

@@ -18,6 +18,7 @@ func Command(tempPath string, builder []string) {
targetCmd.Dir = tempPath targetCmd.Dir = tempPath
targetCmd.Stdout = os.Stdout targetCmd.Stdout = os.Stdout
targetCmd.Stderr = os.Stderr targetCmd.Stderr = os.Stderr
targetCmd.Start() targetCmd.Start()
err := targetCmd.Wait() err := targetCmd.Wait()
if err != nil { if err != nil {

View File

@@ -1,4 +1,4 @@
FROM alpine:3.9 FROM alpine:3.10
# Alternatively use ADD https:// (which will not be cached by Docker builder) # Alternatively use ADD https:// (which will not be cached by Docker builder)
RUN apk --no-cache add curl ca-certificates imagemagick \ RUN apk --no-cache add curl ca-certificates imagemagick \

1
vendor/github.com/alexellis/go-execute generated vendored Submodule

Submodule vendor/github.com/alexellis/go-execute added at d17947259f