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

Support "edit and run local" command. #22

Merged
merged 13 commits into from
Jun 4, 2024
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Locally built binaries
rebuilder

# jj
.jj/

Expand Down
2 changes: 2 additions & 0 deletions build/binary/binary.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@ package binary
import (
"context"
"log"
"os"
"os/exec"
"path/filepath"
)

// Build constructs a binary for one of the project's microservices.
func Build(ctx context.Context, name string) (path string, err error) {
cmd := exec.CommandContext(ctx, "go", "build", "-o", name, "./cmd/"+name)
cmd.Env = append(os.Environ(), "CGO_ENABLED=0")
cmd.Stdout = log.Writer()
cmd.Stderr = log.Writer()
log.Print(cmd.String())
Expand Down
3 changes: 3 additions & 0 deletions pkg/rebuild/rebuild/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ const (

// AttestationBundleAsset is the signed attestation bundle generated for a rebuild.
AttestationBundleAsset AssetType = "rebuild.intoto.jsonl"

// BuildDef is the build definition, including strategy.
BuildDef AssetType = "build.yaml"
)

var (
Expand Down
9 changes: 9 additions & 0 deletions tools/ctl/firestore/firestore.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,15 @@ func NewRebuildFromFirestore(doc *firestore.DocumentSnapshot) Rebuild {
return rb
}

func (r Rebuild) Target() rebuild.Target {
return rebuild.Target{
Ecosystem: rebuild.Ecosystem(r.Ecosystem),
Package: r.Package,
Version: r.Version,
Artifact: r.Artifact,
}
}

type BenchmarkMode string

const (
Expand Down
50 changes: 41 additions & 9 deletions tools/ctl/ide/rebuilder.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,21 @@ package ide

import (
"bufio"
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"net/url"
"os/exec"
"sync"
"time"

"github.com/google/oss-rebuild/build/binary"
"github.com/google/oss-rebuild/build/container"
"github.com/google/oss-rebuild/pkg/rebuild/schema"
"github.com/google/oss-rebuild/tools/ctl/firestore"
"github.com/google/oss-rebuild/tools/docker"
"github.com/pkg/errors"
Expand Down Expand Up @@ -193,23 +198,50 @@ func (rb *Rebuilder) Restart(ctx context.Context) {
}
}

type RunLocalOpts struct {
Strategy *schema.StrategyOneOf
}

// RunLocal runs the rebuilder for the given example.
func (rb *Rebuilder) RunLocal(ctx context.Context, r firestore.Rebuild) {
func (rb *Rebuilder) RunLocal(ctx context.Context, r firestore.Rebuild, opts RunLocalOpts) {
_, err := rb.runningInstance(ctx)
if err != nil {
log.Println(err)
log.Println(err.Error())
return
}
log.Printf("Calling the rebuilder for %s\n", r.ID())
id := time.Now().UTC().Format(time.RFC3339)
cmd := exec.CommandContext(ctx, "curl", "--silent", "-d", fmt.Sprintf("ecosystem=%s&pkg=%s&versions=%s&id=%s", r.Ecosystem, r.Package, r.Version, id), "-X", "POST", "127.0.0.1:8080/smoketest")
rllog := logWriter(log.New(log.Default().Writer(), logPrefix("runlocal"), 0))
cmd.Stdout = rllog
cmd.Stderr = rllog
log.Println(cmd.String())
if err := cmd.Start(); err != nil {
log.Println(err)
vals := url.Values{}
vals.Add("ecosystem", r.Ecosystem)
vals.Add("pkg", r.Package)
vals.Add("versions", r.Version)
vals.Add("id", id)
if opts.Strategy != nil {
byts, err := json.Marshal(opts.Strategy)
if err != nil {
log.Println(err)
return
}
vals.Add("strategy", string(byts))
}
client := http.DefaultClient
url := "http://127.0.0.1:8080/smoketest"
log.Println("Requesting a smoketest from: " + url)
resp, err := client.PostForm(url, vals)
if err != nil {
log.Println(err.Error())
return
}
var smkRespBytes bytes.Buffer
var smkResp schema.SmoketestResponse
if err := json.NewDecoder(io.TeeReader(resp.Body, &smkRespBytes)).Decode(&smkResp); err != nil {
log.Println(errors.Wrap(err, "failed to decode smoketest response").Error())
}
msg := "FAILED"
if json.Unmarshal(smkRespBytes.Bytes(), &smkResp) == nil && len(smkResp.Verdicts) == 1 && smkResp.Verdicts[0].Message == "" {
msg = "SUCCESS"
}
log.Printf("Smoketest %s:\n%s", msg, smkRespBytes.String())
}

// Attach opens a new tmux window that's attached to the rebuilder container.
Expand Down
103 changes: 87 additions & 16 deletions tools/ctl/ide/ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,26 +87,25 @@ func sanitize(name string) string {
return strings.ReplaceAll(strings.ReplaceAll(name, "@", ""), "/", "-")
}

func newAssetStores(ctx context.Context, runID string) (localAssets, gcsAssets rebuild.AssetStore, err error) {
func localAssetStore(ctx context.Context, runID string) (rebuild.AssetStore, error) {
// TODO: Maybe this should be a different ctx variable?
dir := filepath.Join("/tmp/oss-rebuild", runID)
if err := os.MkdirAll(dir, 0755); err != nil {
return nil, nil, errors.Wrapf(err, "failed to create directory %s", dir)
return nil, errors.Wrapf(err, "failed to create directory %s", dir)
}
assetsFS, err := osfs.New("/").Chroot(dir)
if err != nil {
return nil, nil, errors.Wrapf(err, "failed to chroot into directory %s", dir)
return nil, errors.Wrapf(err, "failed to chroot into directory %s", dir)
}
localAssets = rebuild.NewFilesystemAssetStore(assetsFS)
return rebuild.NewFilesystemAssetStore(assetsFS), nil
}

func gcsAssetStore(ctx context.Context, runID string) (rebuild.AssetStore, error) {
bucket, ok := ctx.Value(rebuild.UploadArtifactsPathID).(string)
if !ok {
return nil, nil, errors.Errorf("GCS bucket was not specified")
return nil, errors.Errorf("GCS bucket was not specified")
}
gcsAssets, err = rebuild.NewGCSStore(context.WithValue(ctx, rebuild.RunID, runID), bucket)
if err != nil {
return nil, nil, errors.Wrap(err, "failed to create GCS store")
}
return localAssets, gcsAssets, nil
return rebuild.NewGCSStore(context.WithValue(ctx, rebuild.RunID, runID), bucket)
}

func diffArtifacts(ctx context.Context, example firestore.Rebuild) {
Expand All @@ -120,9 +119,14 @@ func diffArtifacts(ctx context.Context, example firestore.Rebuild) {
Version: example.Version,
Artifact: example.Artifact,
}
localAssets, gcsAssets, err := newAssetStores(ctx, example.Run)
localAssets, err := localAssetStore(ctx, example.Run)
if err != nil {
log.Println(errors.Wrap(err, "failed to create asset stores"))
log.Println(errors.Wrap(err, "failed to create local asset store"))
return
}
gcsAssets, err := gcsAssetStore(ctx, example.Run)
if err != nil {
log.Println(errors.Wrap(err, "failed to create gcs asset store"))
return
}
// TODO: Clean up these artifacts.
Expand Down Expand Up @@ -200,9 +204,14 @@ func (e *explorer) showLogs(ctx context.Context, example firestore.Rebuild) {
Version: example.Version,
Artifact: example.Artifact,
}
localAssets, gcsAssets, err := newAssetStores(ctx, example.Run)
localAssets, err := localAssetStore(ctx, example.Run)
if err != nil {
log.Println(errors.Wrap(err, "failed to create asset stores"))
log.Println(errors.Wrap(err, "failed to create local asset store"))
return
}
gcsAssets, err := gcsAssetStore(ctx, example.Run)
if err != nil {
log.Println(errors.Wrap(err, "failed to create gcs asset store"))
return
}
logs, err := rebuild.AssetCopy(ctx, localAssets, gcsAssets, rebuild.Asset{Target: t, Type: rebuild.DebugLogsAsset})
Expand All @@ -216,19 +225,81 @@ func (e *explorer) showLogs(ctx context.Context, example firestore.Rebuild) {
}
}

func (e *explorer) editAndRun(ctx context.Context, example firestore.Rebuild) error {
localAssets, err := localAssetStore(ctx, example.Run)
if err != nil {
return errors.Wrap(err, "failed to create local asset store")
}
buildDefAsset := rebuild.Asset{Type: rebuild.BuildDef, Target: example.Target()}
var currentStrat schema.StrategyOneOf
{
if r, _, err := localAssets.Reader(ctx, buildDefAsset); err == nil {
d := yaml.NewDecoder(r)
if d.Decode(&currentStrat) != nil {
return errors.Wrap(err, "failed to read existing build definition")
}
} else {
if err := json.Unmarshal([]byte(example.Strategy), &currentStrat); err != nil {
return errors.Wrap(err, "failed to parse strategy")
}
}
}
var newStrat schema.StrategyOneOf
{
w, uri, err := localAssets.Writer(ctx, buildDefAsset)
if err != nil {
return errors.Wrapf(err, "opening build definition")
}
if _, err = w.Write([]byte("# Edit the build definition below, then save and exit the file to begin a rebuild.\n")); err != nil {
return errors.Wrapf(err, "writing comment to build definition file")
}
e := yaml.NewEncoder(w)
if e.Encode(&currentStrat) != nil {
return errors.Wrapf(err, "populating build definition")
}
w.Close()
// Send a "tmux wait -S" signal once the edit is complete.
cmd := exec.Command("tmux", "new-window", fmt.Sprintf("$EDITOR %s; tmux wait -S editing", uri))
if _, err := cmd.Output(); err != nil {
return errors.Wrap(err, "failed to edit build definition")
}
// Wait to receive the tmux signal.
if _, err := exec.Command("tmux", "wait", "editing").Output(); err != nil {
return errors.Wrap(err, "failed to wait for tmux signal")
}
r, _, err := localAssets.Reader(ctx, buildDefAsset)
if err != nil {
return errors.Wrap(err, "failed to open build definition after edits")
}
d := yaml.NewDecoder(r)
if err := d.Decode(&newStrat); err != nil {
return errors.Wrap(err, "manual strategy oneof failed to parse")
}
}
e.rb.RunLocal(e.ctx, example, RunLocalOpts{Strategy: &newStrat})
return nil
}

func (e *explorer) makeExampleNode(example firestore.Rebuild) *tview.TreeNode {
name := fmt.Sprintf("%s [%ds]", example.ID(), int(example.Timings.EstimateCleanBuild().Seconds()))
node := tview.NewTreeNode(name).SetColor(tcell.ColorYellow)
node.SetSelectedFunc(func() {
children := node.GetChildren()
if len(children) == 0 {
node.AddChild(makeCommandNode("run local", func() {
go e.rb.RunLocal(e.ctx, example)
go e.rb.RunLocal(e.ctx, example, RunLocalOpts{})
}))
node.AddChild(makeCommandNode("restart && run local", func() {
go func() {
e.rb.Restart(e.ctx)
e.rb.RunLocal(e.ctx, example)
e.rb.RunLocal(e.ctx, example, RunLocalOpts{})
}()
}))
node.AddChild(makeCommandNode("edit and run local", func() {
go func() {
if err := e.editAndRun(e.ctx, example); err != nil {
log.Println(err.Error())
}
}()
}))
node.AddChild(makeCommandNode("details", func() {
Expand Down