Add progress bar and improve cmd output for image build command

This patch provides the ability to show progress bar using
container logs (ubuntu/debian based only).

Change-Id: I86eebe4d368d81c4685fb27ca31b86cbb3dea08d
Signed-off-by: Ruslan Aliev <raliev@mirantis.com>
Relates-To: #278
This commit is contained in:
Ruslan Aliev 2020-07-26 17:37:28 -05:00
parent 484a4b1549
commit ec51a71181
12 changed files with 2042 additions and 142 deletions

View File

@ -23,13 +23,23 @@ import (
// NewImageBuildCommand creates a new command with the capability to build an ISO image.
func NewImageBuildCommand(cfgFactory config.Factory) *cobra.Command {
options := &isogen.Options{
CfgFactory: cfgFactory,
}
cmd := &cobra.Command{
Use: "build",
Short: "Build ISO image",
RunE: func(cmd *cobra.Command, args []string) error {
return isogen.GenerateBootstrapIso(cfgFactory)
return options.GenerateBootstrapIso()
},
}
flags := cmd.Flags()
flags.BoolVar(
&options.Progress,
"progress",
false,
"show progress")
return cmd
}

1
go.mod
View File

@ -5,6 +5,7 @@ go 1.13
require (
github.com/Masterminds/sprig v2.22.0+incompatible
github.com/chai2010/gettext-go v0.0.0-20170215093142-bf70f2a70fb1 // indirect
github.com/cheggaaa/pb/v3 v3.0.4
github.com/docker/docker v1.4.2-0.20200203170920-46ec8731fbce
github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c // indirect
github.com/elazarl/goproxy v0.0.0-20190421051319-9d40249d3c2f // indirect

12
go.sum
View File

@ -87,6 +87,8 @@ github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:H
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
github.com/VividCortex/ewma v1.1.1 h1:MnEK4VOv6n0RSY4vtRe3h11qjxL3+t0B8yOL8iMXdcM=
github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmxzcbUokwA=
github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs=
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
@ -157,6 +159,8 @@ github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5/go.mod h1:/iP1
github.com/chai2010/gettext-go v0.0.0-20170215093142-bf70f2a70fb1 h1:HD4PLRzjuCVW79mQ0/pdsalOLHJ+FaEoqJLxfltpb2U=
github.com/chai2010/gettext-go v0.0.0-20170215093142-bf70f2a70fb1/go.mod h1:/iP1qXHoty45bqomnu2LM+VVyAEdWN+vtSHGlQgyxbw=
github.com/cheekybits/genny v0.0.0-20170328200008-9127e812e1e9/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
github.com/cheggaaa/pb/v3 v3.0.4 h1:QZEPYOj2ix6d5oEg63fbHmpolrnNiwjUsk+h74Yt4bM=
github.com/cheggaaa/pb/v3 v3.0.4/go.mod h1:7rgWxLrAUcFMkvJuv09+DYi7mMUYi8nO9iOWcvGJPfw=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
@ -285,6 +289,7 @@ github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d h1:105gxyaGwC
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4=
github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb/go.mod h1:bH6Xx7IW64qjjJq8M2u4dxNaBiDfKK+z/3eGDpXEQhc=
github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc=
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/structtag v1.1.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
@ -684,6 +689,7 @@ github.com/martinlindhe/base36 v1.0.0/go.mod h1:+AtEs8xrBpCeYgSLoY/aJ6Wf37jtBuR0
github.com/matoous/godox v0.0.0-20190911065817-5d6d842e92eb/go.mod h1:1BELzlh859Sh1c6+90blK8lbYy0kwQf1bYlBhBysy1s=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-ieproxy v0.0.0-20190610004146-91bb50d98149/go.mod h1:31jz6HNzdxOmlERGGEc4v/dMssOfmp2p5bT/okiKFFc=
github.com/mattn/go-ieproxy v0.0.0-20190702010315-6dee0af9227d/go.mod h1:31jz6HNzdxOmlERGGEc4v/dMssOfmp2p5bT/okiKFFc=
@ -692,10 +698,14 @@ github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNx
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.6/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54=
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-shellwords v1.0.9/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
@ -1221,11 +1231,13 @@ golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191002063906-3421d5a6bb1c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191025021431-6c3a3bfe00ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191112214154-59a1497f0cea/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191113165036-4c7a9d0fe056/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

View File

@ -15,12 +15,18 @@
package isogen
import (
"bufio"
"context"
"fmt"
"io"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"github.com/cheggaaa/pb/v3"
"opendev.org/airship/airshipctl/pkg/api/v1alpha1"
"opendev.org/airship/airshipctl/pkg/bootstrap/cloudinit"
"opendev.org/airship/airshipctl/pkg/config"
@ -32,15 +38,31 @@ import (
const (
builderConfigFileName = "builder-conf.yaml"
// progressBarTemplate is a template string for progress bar
// looks like 'Prefix [-->______] 20%' where Prefix is trimmed log line from docker container
progressBarTemplate = `{{string . "prefix"}} {{bar . }} {{percent . }} `
// defaultTerminalWidth is a default width of terminal if it's impossible to determine the actual one
defaultTerminalWidth = 80
// multiplier is a number of log lines produces while installing 1 package
multiplier = 3
// reInstallActions is a regular expression to check whether the log line contains of this substrings
reInstallActions = `Extracting|Unpacking|Configuring|Preparing|Setting`
)
// Options is used for generate bootstrap ISO
type Options struct {
CfgFactory config.Factory
Progress bool
}
// GenerateBootstrapIso will generate data for cloud init and start ISO builder container
// TODO (vkuzmin): Remove this public function and move another functions
// to the executor module when the phases will be ready
func GenerateBootstrapIso(cfgFactory config.Factory) error {
func (s *Options) GenerateBootstrapIso() error {
ctx := context.Background()
globalConf, err := cfgFactory()
globalConf, err := s.CfgFactory()
if err != nil {
return err
}
@ -80,7 +102,7 @@ func GenerateBootstrapIso(cfgFactory config.Factory) error {
return err
}
err = createBootstrapIso(docBundle, builder, doc, imageConfiguration, log.DebugEnabled())
err = createBootstrapIso(docBundle, builder, doc, imageConfiguration, log.DebugEnabled(), s.Progress)
if err != nil {
return err
}
@ -141,6 +163,7 @@ func createBootstrapIso(
doc document.Document,
cfg *v1alpha1.ImageConfiguration,
debug bool,
progress bool,
) error {
cntVol := strings.Split(cfg.Container.Volume, ":")[1]
log.Print("Creating cloud-init for ephemeral K8s")
@ -162,20 +185,44 @@ func createBootstrapIso(
vols := []string{cfg.Container.Volume}
builderCfgLocation := filepath.Join(cntVol, builderConfigFileName)
log.Printf("Running default container command. Mounted dir: %s", vols)
if err := builder.RunCommand(
[]string{},
nil,
vols,
[]string{
envVars := []string{
fmt.Sprintf("BUILDER_CONFIG=%s", builderCfgLocation),
fmt.Sprintf("http_proxy=%s", os.Getenv("http_proxy")),
fmt.Sprintf("https_proxy=%s", os.Getenv("https_proxy")),
fmt.Sprintf("HTTP_PROXY=%s", os.Getenv("HTTP_PROXY")),
fmt.Sprintf("HTTPS_PROXY=%s", os.Getenv("HTTPS_PROXY")),
fmt.Sprintf("NO_PROXY=%s", os.Getenv("NO_PROXY")),
},
debug,
); err != nil {
}
err = builder.RunCommand([]string{}, nil, vols, envVars)
if err != nil {
return err
}
log.Print("ISO generation is in progress. The whole process could take up to several minutes, please wait...")
if debug || progress {
var cLogs io.ReadCloser
cLogs, err = builder.GetContainerLogs()
if err != nil {
log.Printf("failed to read container logs %s", err)
} else {
switch {
case progress:
showProgress(cLogs, log.Writer())
case debug:
log.Print("start reading container logs")
// either container log output or progress bar will be shown
if _, err = io.Copy(log.Writer(), cLogs); err != nil {
log.Debugf("failed to write container logs to log output %s", err)
}
log.Print("got EOF from container logs")
}
}
}
if err = builder.WaitUntilFinished(); err != nil {
return err
}
@ -188,3 +235,97 @@ func createBootstrapIso(
log.Debugf("Debug flag is set. Container %s stopped but not deleted.", builder.GetID())
return nil
}
func showProgress(reader io.ReadCloser, writer io.Writer) {
reFindActions := regexp.MustCompile(reInstallActions)
var bar *pb.ProgressBar
scanner := bufio.NewScanner(reader)
scanner.Split(bufio.ScanLines)
// Reading container log line by line
for scanner.Scan() {
curLine := scanner.Text()
// Trying to find entry points of package installation
switch {
case strings.Contains(curLine, "Retrieving Packages") ||
strings.Contains(curLine, "newly installed"):
finalizePb(bar, "Completed")
pkgCount := calculatePkgCount(scanner, writer, curLine)
if pkgCount > 0 {
bar = pb.ProgressBarTemplate(progressBarTemplate).Start(pkgCount * multiplier)
bar.SetWriter(writer)
setPbPrefix(bar, "Installing required packages")
}
case strings.Contains(curLine, "Base system installed successfully") ||
strings.Contains(curLine, "mksquashfs"):
finalizePb(bar, "Completed")
case bar != nil && bar.IsStarted():
if reFindActions.MatchString(curLine) {
if bar.Current() < bar.Total() {
setPbPrefix(bar, curLine)
bar.Increment()
}
}
case strings.Contains(curLine, "filesystem.squashfs"):
fmt.Fprintln(writer, curLine)
}
}
finalizePb(bar, "An unexpected error occurred while log parsing")
}
func finalizePb(bar *pb.ProgressBar, msg string) {
if bar != nil && bar.IsStarted() {
bar.SetCurrent(bar.Total())
setPbPrefix(bar, msg)
bar.Finish()
}
}
func setPbPrefix(bar *pb.ProgressBar, msg string) {
terminalWidth := defaultTerminalWidth
halfWidth := terminalWidth / 2
bar.SetWidth(terminalWidth)
if len(msg) > halfWidth {
msg = fmt.Sprintf("%v...", msg[0:halfWidth-3])
} else {
msg = fmt.Sprintf("%-*v", halfWidth, msg)
}
bar.Set("prefix", msg)
}
func calculatePkgCount(scanner *bufio.Scanner, writer io.Writer, curLine string) int {
reFindNumbers := regexp.MustCompile("[0-9]+")
// Trying to count how many packages is going to be installed
pkgCount := 0
matches := reFindNumbers.FindAllString(curLine, -1)
if matches == nil {
// There is no numbers is line about base packages, counting them manually to get estimates
fmt.Fprint(writer, "Retrieving base packages ")
for scanner.Scan() {
curLine = scanner.Text()
if strings.Contains(curLine, "Retrieving") {
pkgCount++
fmt.Fprint(writer, ".")
}
if strings.Contains(curLine, "Chosen extractor") {
fmt.Fprintln(writer, " Done")
return pkgCount
}
}
}
if len(matches) >= 2 {
for _, v := range matches[0:2] {
j, err := strconv.Atoi(v)
if err != nil {
continue
}
pkgCount += j
}
}
return pkgCount
}

View File

@ -15,9 +15,12 @@
package isogen
import (
"bufio"
"bytes"
"fmt"
"io"
"io/ioutil"
"os"
"strings"
"testing"
@ -34,21 +37,22 @@ import (
type mockContainer struct {
imagePull func() error
runCommand func() error
runCommandOutput func() (io.ReadCloser, error)
getContainerLogs func() (io.ReadCloser, error)
rmContainer func() error
getID func() string
waitUntilFinished func() error
}
func (mc *mockContainer) ImagePull() error {
return mc.imagePull()
}
func (mc *mockContainer) RunCommand([]string, io.Reader, []string, []string, bool) error {
func (mc *mockContainer) RunCommand([]string, io.Reader, []string, []string) error {
return mc.runCommand()
}
func (mc *mockContainer) RunCommandOutput([]string, io.Reader, []string, []string) (io.ReadCloser, error) {
return mc.runCommandOutput()
func (mc *mockContainer) GetContainerLogs() (io.ReadCloser, error) {
return mc.getContainerLogs()
}
func (mc *mockContainer) RmContainer() error {
@ -59,6 +63,12 @@ func (mc *mockContainer) GetID() string {
return mc.getID()
}
func (mc *mockContainer) WaitUntilFinished() error {
return mc.waitUntilFinished()
}
const testID = "TESTID"
func TestBootstrapIso(t *testing.T) {
bundle, err := document.NewBundleByPath("testdata/primary/site/test-site/ephemeral/bootstrap")
require.NoError(t, err, "Building Bundle Failed")
@ -83,7 +93,7 @@ func TestBootstrapIso(t *testing.T) {
}
testBuilder := &mockContainer{
runCommand: func() error { return nil },
getID: func() string { return "TESTID" },
getID: func() string { return testID },
rmContainer: func() error { return nil },
}
@ -106,6 +116,8 @@ func TestBootstrapIso(t *testing.T) {
{
builder: &mockContainer{
runCommand: func() error { return testErr },
waitUntilFinished: func() error { return nil },
rmContainer: func() error { return nil },
},
cfg: testCfg,
doc: testDoc,
@ -114,7 +126,13 @@ func TestBootstrapIso(t *testing.T) {
expectedErr: testErr,
},
{
builder: testBuilder,
builder: &mockContainer{
runCommand: func() error { return nil },
getID: func() string { return "TESTID" },
waitUntilFinished: func() error { return nil },
rmContainer: func() error { return nil },
getContainerLogs: func() (io.ReadCloser, error) { return ioutil.NopCloser(strings.NewReader("")), nil },
},
cfg: testCfg,
doc: testDoc,
debug: true,
@ -126,6 +144,7 @@ func TestBootstrapIso(t *testing.T) {
runCommand: func() error { return nil },
getID: func() string { return "TESTID" },
rmContainer: func() error { return testErr },
waitUntilFinished: func() error { return nil },
},
cfg: testCfg,
doc: testDoc,
@ -148,7 +167,7 @@ func TestBootstrapIso(t *testing.T) {
for _, tt := range tests {
outBuf := &bytes.Buffer{}
log.Init(tt.debug, outBuf)
actualErr := createBootstrapIso(bundle, tt.builder, tt.doc, tt.cfg, tt.debug)
actualErr := createBootstrapIso(bundle, tt.builder, tt.doc, tt.cfg, tt.debug, false)
actualOut := outBuf.String()
for _, line := range tt.expectedOut {
@ -233,70 +252,92 @@ func TestGenerateBootstrapIso(t *testing.T) {
airshipConfigPath := "testdata/config/config"
kubeConfigPath := "testdata/config/kubeconfig"
t.Run("EnsureCompleteError", func(t *testing.T) {
settings, err := config.CreateFactory(&airshipConfigPath, &kubeConfigPath)()
require.NoError(t, err)
expectedErr := config.ErrMissingConfig{What: "Context with name ''"}
settings.CurrentContext = ""
actualErr := GenerateBootstrapIso(func() (*config.Config, error) {
return settings, nil
})
assert.Equal(t, expectedErr, actualErr)
})
t.Run("ContextEntryPointError", func(t *testing.T) {
settings, err := config.CreateFactory(&airshipConfigPath, &kubeConfigPath)()
cfg, err := config.CreateFactory(&airshipConfigPath, &kubeConfigPath)()
require.NoError(t, err)
cfg.Manifests["default"].Repositories = make(map[string]*config.Repository)
settings := &Options{CfgFactory: func() (*config.Config, error) {
return cfg, nil
}}
expectedErr := config.ErrMissingPrimaryRepo{}
settings.Manifests["default"].Repositories = make(map[string]*config.Repository)
actualErr := GenerateBootstrapIso(func() (*config.Config, error) {
return settings, nil
})
actualErr := settings.GenerateBootstrapIso()
assert.Equal(t, expectedErr, actualErr)
})
t.Run("NewBundleByPathError", func(t *testing.T) {
settings, err := config.CreateFactory(&airshipConfigPath, &kubeConfigPath)()
cfg, err := config.CreateFactory(&airshipConfigPath, &kubeConfigPath)()
require.NoError(t, err)
cfg.Manifests["default"].TargetPath = "/nonexistent"
settings := &Options{CfgFactory: func() (*config.Config, error) {
return cfg, nil
}}
expectedErr := config.ErrMissingPhaseDocument{PhaseName: "bootstrap"}
settings.Manifests["default"].TargetPath = "/nonexistent"
actualErr := GenerateBootstrapIso(func() (*config.Config, error) {
return settings, nil
})
actualErr := settings.GenerateBootstrapIso()
assert.Equal(t, expectedErr, actualErr)
})
t.Run("SelectOneError", func(t *testing.T) {
settings, err := config.CreateFactory(&airshipConfigPath, &kubeConfigPath)()
cfg, err := config.CreateFactory(&airshipConfigPath, &kubeConfigPath)()
require.NoError(t, err)
cfg.Manifests["default"].SubPath = "missingkinddoc/site/test-site"
settings := &Options{CfgFactory: func() (*config.Config, error) {
return cfg, nil
}}
expectedErr := document.ErrDocNotFound{
Selector: document.NewSelector().ByGvk("airshipit.org", "v1alpha1", "ImageConfiguration")}
settings.Manifests["default"].SubPath = "missingkinddoc/site/test-site"
actualErr := GenerateBootstrapIso(func() (*config.Config, error) {
return settings, nil
})
actualErr := settings.GenerateBootstrapIso()
assert.Equal(t, expectedErr, actualErr)
})
t.Run("ToObjectError", func(t *testing.T) {
settings, err := config.CreateFactory(&airshipConfigPath, &kubeConfigPath)()
cfg, err := config.CreateFactory(&airshipConfigPath, &kubeConfigPath)()
require.NoError(t, err)
cfg.Manifests["default"].SubPath = "missingmetadoc/site/test-site"
settings := &Options{CfgFactory: func() (*config.Config, error) {
return cfg, nil
}}
expectedErrMessage := "missing metadata.name in object"
settings.Manifests["default"].SubPath = "missingmetadoc/site/test-site"
actualErr := GenerateBootstrapIso(func() (*config.Config, error) {
return settings, nil
})
actualErr := settings.GenerateBootstrapIso()
assert.Contains(t, actualErr.Error(), expectedErrMessage)
})
t.Run("verifyInputsError", func(t *testing.T) {
settings, err := config.CreateFactory(&airshipConfigPath, &kubeConfigPath)()
cfg, err := config.CreateFactory(&airshipConfigPath, &kubeConfigPath)()
require.NoError(t, err)
cfg.Manifests["default"].SubPath = "missingvoldoc/site/test-site"
settings := &Options{CfgFactory: func() (*config.Config, error) {
return cfg, nil
}}
expectedErr := config.ErrMissingConfig{What: "Must specify volume bind for ISO builder container"}
settings.Manifests["default"].SubPath = "missingvoldoc/site/test-site"
actualErr := GenerateBootstrapIso(func() (*config.Config, error) {
return settings, nil
})
actualErr := settings.GenerateBootstrapIso()
assert.Equal(t, expectedErr, actualErr)
})
}
func TestShowProgress(t *testing.T) {
tests := []struct {
name string
input string
output string
}{
{
name: "Process-debian-based-logs",
input: "testdata/debian-container-logs",
output: "testdata/pb-output-debian",
},
}
for _, tt := range tests {
tt := tt
file, err := os.Open(tt.input)
require.NoError(t, err)
reader := ioutil.NopCloser(bufio.NewReader(file))
writer := &bytes.Buffer{}
showProgress(reader, writer)
err = file.Close()
require.NoError(t, err)
expected, err := ioutil.ReadFile(tt.output)
require.NoError(t, err)
assert.Equal(t, expected, writer.Bytes())
}
}

View File

@ -110,7 +110,7 @@ func (c *Executor) Run(evtCh chan events.Event, opts ifc.RunOptions) {
}
}
err := createBootstrapIso(c.ExecutorBundle, c.builder, c.ExecutorDocument, c.imgConf, log.DebugEnabled())
err := createBootstrapIso(c.ExecutorBundle, c.builder, c.ExecutorDocument, c.imgConf, log.DebugEnabled(), false)
if err != nil {
handleError(evtCh, err)
return

View File

@ -107,6 +107,7 @@ func TestExecutorRun(t *testing.T) {
runCommand: func() error { return nil },
getID: func() string { return "TESTID" },
rmContainer: func() error { return nil },
waitUntilFinished: func() error { return nil },
},
expectedEvt: []events.Event{
{

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,2 @@
Retrieving base packages ........................................................................................... Done
Completed [----------------------------] 100.00% Completed [----------------------------] 100.00% Completed [----------------------------] 100.00% Creating 4.0 filesystem on /root/LIVE_BOOT/image/live/filesystem.squashfs, block size 131072.

View File

@ -24,10 +24,11 @@ import (
// defines methods that must be implemented for CRE (e.g. docker, containerd or CRI-O)
type Container interface {
ImagePull() error
RunCommand([]string, io.Reader, []string, []string, bool) error
RunCommandOutput([]string, io.Reader, []string, []string) (io.ReadCloser, error)
RunCommand([]string, io.Reader, []string, []string) error
GetContainerLogs() (io.ReadCloser, error)
RmContainer() error
GetID() string
WaitUntilFinished() error
}
// NewContainer returns instance of Container interface implemented by particular driver

View File

@ -241,7 +241,6 @@ func (c *DockerContainer) RunCommand(
containerInput io.Reader,
volumeMounts []string,
envVars []string,
debug bool,
) error {
realCmd, err := c.getCmd(cmd)
if err != nil {
@ -279,53 +278,13 @@ func (c *DockerContainer) RunCommand(
return err
}
if debug {
log.Debug("start reading container logs")
var reader io.ReadCloser
reader, err = c.dockerClient.ContainerLogs(*c.ctx, c.id, types.ContainerLogsOptions{ShowStdout: true, Follow: true})
if err != nil {
log.Debugf("failed to read container logs %s", err)
reader = ioutil.NopCloser(strings.NewReader(""))
}
_, err = io.Copy(log.Writer(), reader)
if err != nil {
log.Debugf("failed to write container logs to log output %s", err)
}
log.Debug("got EOF from container logs")
}
statusCh, errCh := c.dockerClient.ContainerWait(*c.ctx, c.id, container.WaitConditionNotRunning)
log.Debugf("waiting until command '%s' is finished...", realCmd)
select {
case err = <-errCh:
if err != nil {
return err
}
case retCode := <-statusCh:
if retCode.StatusCode != 0 {
logsCmd := fmt.Sprintf("docker logs %s", c.id)
return ErrRunContainerCommand{Cmd: logsCmd}
}
}
log.Debug("docker container is started")
return nil
}
// RunCommandOutput executes specified command in Docker container and
// returns command output as ReadCloser object. RunCommand debug option is
// set to false explicitly
func (c *DockerContainer) RunCommandOutput(
cmd []string,
containerInput io.Reader,
volumeMounts []string,
envVars []string,
) (io.ReadCloser, error) {
if err := c.RunCommand(cmd, containerInput, volumeMounts, envVars, false); err != nil {
return nil, err
}
return c.dockerClient.ContainerLogs(*c.ctx, c.id, types.ContainerLogsOptions{ShowStdout: true})
// GetContainerLogs returns logs from the container as io.ReadCloser
func (c *DockerContainer) GetContainerLogs() (io.ReadCloser, error) {
return c.dockerClient.ContainerLogs(*c.ctx, c.id, types.ContainerLogsOptions{ShowStdout: true, Follow: true})
}
// RmContainer kills and removes a container from the docker host.
@ -338,3 +297,21 @@ func (c *DockerContainer) RmContainer() error {
},
)
}
// WaitUntilFinished waits unit container command is finished, return an error if failed
func (c *DockerContainer) WaitUntilFinished() error {
statusCh, errCh := c.dockerClient.ContainerWait(*c.ctx, c.id, container.WaitConditionNotRunning)
log.Debugf("waiting until command is finished...")
select {
case err := <-errCh:
if err != nil {
return err
}
case retCode := <-statusCh:
if retCode.StatusCode != 0 {
logsCmd := fmt.Sprintf("docker logs %s", c.id)
return ErrRunContainerCommand{Cmd: logsCmd}
}
}
return nil
}

View File

@ -260,7 +260,7 @@ func TestImagePull(t *testing.T) {
func TestGetId(t *testing.T) {
cnt := getDockerContainerMock(mockDockerClient{})
err := cnt.RunCommand([]string{"testCmd"}, nil, nil, []string{}, false)
err := cnt.RunCommand([]string{"testCmd"}, nil, nil, []string{})
require.NoError(t, err)
actualID := cnt.GetID()
@ -278,7 +278,8 @@ func TestRunCommand(t *testing.T) {
volumeMounts []string
debug bool
mockDockerClient mockDockerClient
expectedErr error
expectedRunErr error
expectedWaitErr error
assertF func(*testing.T)
}{
{
@ -287,7 +288,8 @@ func TestRunCommand(t *testing.T) {
volumeMounts: nil,
debug: false,
mockDockerClient: mockDockerClient{},
expectedErr: nil,
expectedRunErr: nil,
expectedWaitErr: nil,
assertF: func(t *testing.T) {},
},
{
@ -300,7 +302,8 @@ func TestRunCommand(t *testing.T) {
return nil, imageListError
},
},
expectedErr: imageListError,
expectedRunErr: imageListError,
expectedWaitErr: nil,
assertF: func(t *testing.T) {},
},
{
@ -316,7 +319,8 @@ func TestRunCommand(t *testing.T) {
return conn, nil
},
},
expectedErr: nil,
expectedRunErr: nil,
expectedWaitErr: nil,
assertF: func(t *testing.T) {},
},
{
@ -329,7 +333,8 @@ func TestRunCommand(t *testing.T) {
return types.HijackedResponse{}, attachError
},
},
expectedErr: attachError,
expectedRunErr: attachError,
expectedWaitErr: nil,
assertF: func(t *testing.T) {},
},
{
@ -342,7 +347,8 @@ func TestRunCommand(t *testing.T) {
return containerStartError
},
},
expectedErr: containerStartError,
expectedRunErr: containerStartError,
expectedWaitErr: nil,
assertF: func(t *testing.T) {},
},
{
@ -359,7 +365,8 @@ func TestRunCommand(t *testing.T) {
return nil, errC
},
},
expectedErr: containerWaitError,
expectedRunErr: nil,
expectedWaitErr: containerWaitError,
assertF: func(t *testing.T) {},
},
{
@ -376,7 +383,8 @@ func TestRunCommand(t *testing.T) {
return resC, nil
},
},
expectedErr: ErrRunContainerCommand{Cmd: "docker logs testID"},
expectedRunErr: nil,
expectedWaitErr: ErrRunContainerCommand{Cmd: "docker logs testID"},
assertF: func(t *testing.T) {},
},
{
@ -396,15 +404,17 @@ func TestRunCommand(t *testing.T) {
return nil, fmt.Errorf("logs error")
},
},
expectedErr: ErrRunContainerCommand{Cmd: "docker logs testID"},
expectedRunErr: nil,
expectedWaitErr: ErrRunContainerCommand{Cmd: "docker logs testID"},
assertF: func(t *testing.T) {},
},
}
for _, tt := range tests {
cnt := getDockerContainerMock(tt.mockDockerClient)
actualErr := cnt.RunCommand(tt.cmd, tt.containerInput, tt.volumeMounts, []string{}, tt.debug)
assert.Equal(t, tt.expectedErr, actualErr)
actualErr := cnt.RunCommand(tt.cmd, tt.containerInput, tt.volumeMounts, []string{})
assert.Equal(t, tt.expectedRunErr, actualErr)
actualErr = cnt.WaitUntilFinished()
assert.Equal(t, tt.expectedWaitErr, actualErr)
tt.assertF(t)
}
@ -443,9 +453,10 @@ func TestRunCommandOutput(t *testing.T) {
}
for _, tt := range tests {
cnt := getDockerContainerMock(tt.mockDockerClient)
actualRes, actualErr := cnt.RunCommandOutput(tt.cmd, tt.containerInput, tt.volumeMounts, []string{})
actualErr := cnt.RunCommand(tt.cmd, tt.containerInput, tt.volumeMounts, []string{})
assert.Equal(t, tt.expectedErr, actualErr)
actualRes, actualErr := cnt.GetContainerLogs()
require.NoError(t, actualErr)
var actualResBytes []byte
if actualRes != nil {