Skip to content

Commit

Permalink
main,osbuildprogress: add --progress=text support
Browse files Browse the repository at this point in the history
This adds a new `bootc-image-builder` mode that makes use of the
osbuild json-seq progress information. It will display some
user friendly progress information.
  • Loading branch information
mvo5 committed Jul 4, 2024
1 parent 04471d4 commit 1cb509f
Show file tree
Hide file tree
Showing 2 changed files with 186 additions and 2 deletions.
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 @@ -26,6 +26,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/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 @@ -381,6 +382,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 @@ -463,7 +465,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 @@ -613,7 +620,8 @@ func run() 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
Expand Down
176 changes: 176 additions & 0 deletions bib/internal/osbuildprogress/progress.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
package osbuildprogress

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

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 {
Name string `json:"name"`
Total int `json:"total"`
Done int `json:"done"`
// XXX: there are currently only two levels but it should be
// deeper nested in theory
SubProgress struct {
Name string `json:"name"`
Total int `json:"total"`
Done int `json:"done"`
// XXX: in theory this could be more nested but it's not

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

Message string `json:"message"`
}

func scanJsonSeq(r io.Reader, ch chan OsbuildJsonProgress) {
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
fmt.Fprintf(os.Stderr, "json decode err for line %q: %v\n", line, err)
continue
}
ch <- progress
}
if err := scanner.Err(); err != nil && err != io.EOF {
// log error here
}
}

func AttachProgress(r io.Reader, w io.Writer) {
var progress OsbuildJsonProgress
spinner := []string{"|", "/", "-", "\\"}
i := 0

ch := make(chan OsbuildJsonProgress)
go scanJsonSeq(r, ch)

mainProgress := "unknown"
subProgress := ""
message := "-"

contextMap := map[string]string{}

fmt.Fprintf(w, "\n")
for {
select {
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 {
mainProgress = fmt.Sprintf("step: %v [%v/%v]", pipelineName, progress.Progress.Done+1, progress.Progress.Total+1)
}
// XXX: use context instead of name here too
if progress.Progress.SubProgress.Total > 0 {
subProgress = fmt.Sprintf("%v [%v/%v]", stageName, progress.Progress.SubProgress.Done+1, progress.Progress.SubProgress.Total+1)
}

// 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" {
message = progress.Message
}
//message = strings.TrimSpace(strings.SplitN(progress.Message, "\n", 2)[0])
// todo: fix in osbuild?
/*
l := strings.SplitN(message, ":", 2)
if len(l) > 1 {
message = strings.TrimSpace(l[1])
}
*/
if len(message) > 60 {
message = message[:60] + "..."
}
case <-time.After(200 * time.Millisecond):
// nothing
}

// XXX: use real progressbar *or* use helper to get terminal
// size for proper length checks etc
//
// poor man progress, we need multiple progress bars and
// a message that keeps getting updated (or maybe not the
// message)
fmt.Fprintf(w, "\x1b[2KBuilding [%s]\n", spinner[i])
fmt.Fprintf(w, "\x1b[2Kstep : %s\n", mainProgress)
if subProgress != "" {
fmt.Fprintf(w, "\x1b[2Kmodule : %s\n", strings.TrimPrefix(subProgress, "org.osbuild."))
}
fmt.Fprintf(w, "\x1b[3Kmessage: %s\n", message)
if subProgress != "" {
fmt.Fprintf(w, "\x1b[%dA", 4)
} else {
fmt.Fprintf(w, "\x1b[%dA", 3)
}
// spin
i = (i + 1) % len(spinner)
}
}

// XXX: merge back into images/pkg/osbuild/osbuild-exec.go(?)
func RunOSBuild(manifest []byte, store, outputDirectory string, exports, extraEnv []string) error {
cmd := exec.Command(
"osbuild",
"--store", store,
"--output-directory", outputDirectory,
"--monitor=JSONSeqMonitor",
"-",
)
cmd.Env = append(os.Environ(), extraEnv...)
cmd.Stdin = bytes.NewBuffer(manifest)
cmd.Stderr = os.Stderr

for _, export := range exports {
cmd.Args = append(cmd.Args, "--export", export)
}

stdout, err := cmd.StdoutPipe()
if err != nil {
return err
}
go AttachProgress(stdout, os.Stdout)

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

0 comments on commit 1cb509f

Please sign in to comment.