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

main,osbuildprogress: add --progress=text support (COMPOSER-2394) #525

Closed
wants to merge 5 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
12 changes: 10 additions & 2 deletions bib/cmd/bootc-image-builder/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"github.com/osbuild/bootc-image-builder/bib/internal/buildconfig"
podman_container "github.com/osbuild/bootc-image-builder/bib/internal/container"
"github.com/osbuild/bootc-image-builder/bib/internal/imagetypes"
"github.com/osbuild/bootc-image-builder/bib/internal/osbuildprogress"
"github.com/osbuild/bootc-image-builder/bib/internal/setup"
"github.com/osbuild/bootc-image-builder/bib/internal/source"
"github.com/osbuild/bootc-image-builder/bib/internal/util"
Expand Down Expand Up @@ -383,6 +384,7 @@ func cmdBuild(cmd *cobra.Command, args []string) error {
osbuildStore, _ := cmd.Flags().GetString("store")
outputDir, _ := cmd.Flags().GetString("output")
targetArch, _ := cmd.Flags().GetString("target-arch")
progress, _ := cmd.Flags().GetString("progress")

logrus.Debug("Validating environment")
if err := setup.Validate(targetArch); err != nil {
Expand Down Expand Up @@ -453,7 +455,12 @@ func cmdBuild(cmd *cobra.Command, args []string) error {
osbuildEnv = append(osbuildEnv, envVars...)
}

_, err = osbuild.RunOSBuild(mf, osbuildStore, outputDir, exports, nil, osbuildEnv, false, os.Stderr)
switch progress {
case "text":
err = osbuildprogress.RunOSBuild(mf, osbuildStore, outputDir, exports, osbuildEnv)
default:
_, err = osbuild.RunOSBuild(mf, osbuildStore, outputDir, exports, nil, osbuildEnv, false, os.Stderr)
}
if err != nil {
return fmt.Errorf("cannot run osbuild: %w", err)
}
Expand Down Expand Up @@ -607,7 +614,8 @@ func buildCobraCmdline() (*cobra.Command, error) {
buildCmd.Flags().String("aws-region", "", "target region for AWS uploads (only for type=ami)")
buildCmd.Flags().String("chown", "", "chown the ouput directory to match the specified UID:GID")
buildCmd.Flags().String("output", ".", "artifact output directory")
buildCmd.Flags().String("progress", "text", "type of progress bar to use")
// XXX: make this a proper type
buildCmd.Flags().String("progress", "none", "type of progress bar to use")
buildCmd.Flags().String("store", "/store", "osbuild store for intermediate pipeline trees")
// flag rules
for _, dname := range []string{"output", "store", "rpmmd"} {
Expand Down
203 changes: 203 additions & 0 deletions bib/internal/osbuildprogress/progress.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
package osbuildprogress

import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"io"
"os"
"os/exec"
"strings"
"sync"
"time"

"github.com/cheggaaa/pb/v3"
)

type OsbuildJsonProgress struct {
ID string `json:"id"`
Context struct {
Origin string `json:"origin"`
Pipeline struct {
Name string `json:"name"`
Stage struct {
Name string `json:"name"`
ID string `json:"id"`
} `json:"stage"`
ID string `json:"id"`
} `json:"pipeline"`
} `json:"context"`
Progress struct {
Copy link
Collaborator Author

@mvo5 mvo5 Aug 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to add "build_result"result here if/once osbuild/osbuild#1831 is merged

Name string `json:"name"`
Total int64 `json:"total"`
Done int64 `json:"done"`
// XXX: there are currently only two levels but it should be
// deeper nested in theory
SubProgress struct {
Name string `json:"name"`
Total int64 `json:"total"`
Done int64 `json:"done"`
// XXX: in theory this could be more nested but it's not

} `json:"progress"`
} `json:"progress"`

Message string `json:"message"`
}

func scanJsonSeq(wg *sync.WaitGroup, r io.Reader, ch chan OsbuildJsonProgress, errCh chan error) {
wg.Add(1)
defer wg.Done()

var progress OsbuildJsonProgress
scanner := bufio.NewScanner(r)
for scanner.Scan() {
// XXX: use a proper jsonseq reader?
line := scanner.Bytes()
line = bytes.Trim(line, "\x1e")
if err := json.Unmarshal(line, &progress); err != nil {
// XXX: provide an invalid lines chan here?
errCh <- err
continue
}
ch <- progress
}
if err := scanner.Err(); err != nil && err != io.EOF {
errCh <- err
}
errCh <- io.EOF
}

func AttachProgress(wg *sync.WaitGroup, r io.Reader, w io.Writer) {
wg.Add(1)
defer wg.Done()

var progress OsbuildJsonProgress
ch := make(chan OsbuildJsonProgress)
errCh := make(chan error)
go scanJsonSeq(wg, r, ch, errCh)

lastMessage := "-"

spinnerPb := pb.New(0)
spinnerPb.SetTemplate(`Building [{{ (cycle . "|" "/" "-" "\\") }}]`)
mainPb := pb.New(0)
progressBarTmplFmt := `[{{ counters . }}] %s: {{ string . "prefix" }} {{ bar .}} {{ percent . }}`
mainPb.SetTemplateString(fmt.Sprintf(progressBarTmplFmt, "step"))
subPb := pb.New(0)
subPb.SetTemplateString(fmt.Sprintf(progressBarTmplFmt, "module"))
msgPb := pb.New(0)
msgPb.SetTemplate(`last msg: {{ string . "msg" }}`)

pool, err := pb.StartPool(spinnerPb, mainPb, subPb, msgPb)
if err != nil {
fmt.Fprintf(os.Stderr, "progress failed: %v\n", err)
return
}
defer pool.Stop()

Check failure on line 98 in bib/internal/osbuildprogress/progress.go

View workflow job for this annotation

GitHub Actions / ⌨ Lint & unittests

Error return value of `pool.Stop` is not checked (errcheck)

contextMap := map[string]string{}

for {
select {
case err := <-errCh:
if err == io.EOF {
return
}
fmt.Fprintf(os.Stderr, "error: %v", err)
case progress = <-ch:
id := progress.Context.Pipeline.ID
pipelineName := contextMap[id]
if pipelineName == "" {
pipelineName = progress.Context.Pipeline.Name
contextMap[id] = pipelineName
}
// XXX: use differentmap?
id = "stage-" + progress.Context.Pipeline.Stage.ID
stageName := contextMap[id]
if stageName == "" {
stageName = progress.Context.Pipeline.Stage.Name
contextMap[id] = stageName
}

if progress.Progress.Total > 0 {
mainPb.SetTotal(progress.Progress.Total + 1)
mainPb.SetCurrent(progress.Progress.Done + 1)
mainPb.Set("prefix", pipelineName)
}
// XXX: use context instead of name here too
if progress.Progress.SubProgress.Total > 0 {
subPb.SetTotal(progress.Progress.SubProgress.Total + 1)
subPb.SetCurrent(progress.Progress.SubProgress.Done + 1)
subPb.Set("prefix", strings.TrimPrefix(stageName, "org.osbuild."))
}

// todo: make message more structured in osbuild?
// message from the stages themselfs are very noisy
// best not to show to the user (only for failures)
if progress.Context.Origin == "osbuild.monitor" {
lastMessage = progress.Message
}
/*
// todo: fix in osbuild?
lastMessage = strings.TrimSpace(strings.SplitN(progress.Message, "\n", 2)[0])
l := strings.SplitN(lastMessage, ":", 2)
if len(l) > 1 {
lastMessage = strings.TrimSpace(l[1])
}
*/
msgPb.Set("msg", lastMessage)

case <-time.After(200 * time.Millisecond):
// nothing
}
}
}

// XXX: merge back into images/pkg/osbuild/osbuild-exec.go(?)
func RunOSBuild(manifest []byte, store, outputDirectory string, exports, extraEnv []string) error {
wg := &sync.WaitGroup{}

rp, wp, err := os.Pipe()
if err != nil {
return fmt.Errorf("cannot create pipe for osbuild: %w", err)
}
defer rp.Close()
defer wp.Close()

cmd := exec.Command(
"osbuild",
"--store", store,
"--output-directory", outputDirectory,
"--monitor=JSONSeqMonitor",
"--monitor-fd=3",
"-",
)
for _, export := range exports {
cmd.Args = append(cmd.Args, "--export", export)
}

cmd.Env = append(os.Environ(), extraEnv...)
cmd.Stdin = bytes.NewBuffer(manifest)
cmd.Stderr = os.Stderr
// we could use "--json" here and would get the build-result
// exported here
cmd.Stdout = nil
cmd.ExtraFiles = []*os.File{wp}

go AttachProgress(wg, rp, os.Stdout)
if err := cmd.Start(); err != nil {
return fmt.Errorf("error starting osbuild: %v", err)
}
wp.Close()

if err := cmd.Wait(); err != nil {
return fmt.Errorf("error running osbuild: %w", err)
}

// wait until the goroutines are finished too or we get premature
// exit of the progress reading and half finished progress bars
wg.Wait()
return nil
}
Loading