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
10 changes: 8 additions & 2 deletions tools/ctl/ide/rebuilder.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"io"
"log"
"os/exec"
"strings"
"sync"
"time"

Expand Down Expand Up @@ -194,15 +195,20 @@ func (rb *Rebuilder) Restart(ctx context.Context) {
}

// RunLocal runs the rebuilder for the given example.
func (rb *Rebuilder) RunLocal(ctx context.Context, r firestore.Rebuild) {
// Each element of extraParams is a url parameter like "param=value".
func (rb *Rebuilder) RunLocal(ctx context.Context, r firestore.Rebuild, extraParams ...string) {
_, err := rb.runningInstance(ctx)
if err != nil {
log.Println(err)
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")
var extra string
if len(extraParams) > 0 {
extra = "&" + strings.Join(extraParams, "&")
}
cmd := exec.CommandContext(ctx, "curl", "--silent", "-d", fmt.Sprintf("ecosystem=%s&pkg=%s&versions=%s&id=%s%s", r.Ecosystem, r.Package, r.Version, id, extra), "-X", "POST", "127.0.0.1:8080/smoketest")
rllog := logWriter(log.New(log.Default().Writer(), logPrefix("runlocal"), 0))
cmd.Stdout = rllog
cmd.Stderr = rllog
Expand Down
114 changes: 100 additions & 14 deletions tools/ctl/ide/ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/url"
"os"
"os/exec"
"path/filepath"
Expand Down Expand Up @@ -87,26 +89,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")
}
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 nil, errors.Errorf("GCS bucket was not specified")
}
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 +121,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 +206,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,6 +227,78 @@ func (e *explorer) showLogs(ctx context.Context, example firestore.Rebuild) {
}
}

func (e *explorer) editAndRun(ctx context.Context, example firestore.Rebuild) {
localAssets, err := localAssetStore(ctx, example.Run)
if err != nil {
log.Println(errors.Wrap(err, "failed to create local asset store"))
return
}
buildDefAsset := rebuild.Asset{Type: rebuild.BuildDef, Target: example.Target()}
var currentStratYaml []byte
{
if r, _, err := localAssets.Reader(ctx, buildDefAsset); err == nil {
buf := new(bytes.Buffer)
_, err = io.Copy(buf, r)
if err != nil {
log.Println(errors.Wrap(err, "failed to read existing build definition"))
return
}
currentStratYaml = buf.Bytes()
} else {
var oneof schema.StrategyOneOf
if err := json.Unmarshal([]byte(example.Strategy), &oneof); err != nil {
log.Println(errors.Wrap(err, "failed to parse strategy"))
return
}
var err error
if currentStratYaml, err = yaml.Marshal(oneof); err != nil {
log.Println(errors.Wrap(err, "failed to marshal strategy"))
return
}
}
}
var newStratYaml []byte
{
w, uri, err := localAssets.Writer(ctx, buildDefAsset)
if err != nil {
log.Println(errors.Wrapf(err, "opening strategy file"))
return
}
w.Write(append([]byte("# Edit the strategy below, then save and exit the file to begin a rebuild.\n"), currentStratYaml...))
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 out, err := cmd.Output(); err != nil {
log.Println(errors.Wrap(err, "failed to edit strategy:"))
log.Println(out)
return
}
// Wait to receive the tmux signal.
if _, err := exec.Command("tmux", "wait", "editing").Output(); err != nil {
log.Println(errors.Wrap(err, "failed to wait for tmux signal"))
return
}
newStratYaml, err = os.ReadFile(uri)
if err != nil {
log.Println(errors.Wrap(err, "failed to read strategy"))
return
}
}
log.Println("New strategy: " + string(newStratYaml))
var oneof schema.StrategyOneOf
if err := yaml.Unmarshal(newStratYaml, &oneof); err != nil {
log.Println(errors.Wrap(err, "failed to parse new strategy"))
return
}
log.Printf("Decoded oneof: %+v", oneof)
newStratJsonBytes, err := json.Marshal(oneof)
if err != nil {
log.Println(errors.Wrap(err, "failed to convert new strategy to json"))
return
}
go e.rb.RunLocal(e.ctx, example, "strategy="+url.QueryEscape(string(newStratJsonBytes)))
}

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)
Expand All @@ -231,6 +314,9 @@ func (e *explorer) makeExampleNode(example firestore.Rebuild) *tview.TreeNode {
e.rb.RunLocal(e.ctx, example)
}()
}))
node.AddChild(makeCommandNode("edit and run local", func() {
go e.editAndRun(e.ctx, example)
}))
node.AddChild(makeCommandNode("details", func() {
go e.showDetails(e.ctx, example)
}))
Expand Down