mirror of
https://github.com/open-gitagent/langship.sh.git
synced 2026-08-03 07:21:04 +02:00
- Added multi-execution view to display multiple execution statuses. - Improved error handling in agent triggering to surface failures. - Updated execution view to support live updates via SSE and added a log panel for node logs. - Enhanced pipeline canvas to display node statuses during execution. - Introduced new fields in the build node form for Docker configurations. - Updated API to support execution streaming and enhanced node catalog descriptions.
221 lines
5.9 KiB
Go
221 lines
5.9 KiB
Go
package executors
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
cliconfig "github.com/docker/cli/cli/config/configfile"
|
|
cliconfigtypes "github.com/docker/cli/cli/config/types"
|
|
"github.com/moby/buildkit/client"
|
|
"github.com/moby/buildkit/frontend/dockerui"
|
|
"github.com/moby/buildkit/session"
|
|
"github.com/moby/buildkit/session/auth/authprovider"
|
|
"github.com/moby/buildkit/util/progress/progressui"
|
|
"golang.org/x/sync/errgroup"
|
|
|
|
"github.com/lyzrai/flow/pkg/engine"
|
|
)
|
|
|
|
// dockerBuildOpts is the input to runDockerBuild.
|
|
type dockerBuildOpts struct {
|
|
BuildKitAddr string // tcp://buildkitd:1234 or unix:// path
|
|
ContextDir string // local path to the cloned repo (build context)
|
|
Dockerfile string // path to Dockerfile relative to ContextDir (default "Dockerfile")
|
|
ImageRef string // full image ref to push, e.g. ghcr.io/org/agent:abc123
|
|
Platform string // e.g. linux/amd64 (empty = buildkit default)
|
|
BuildArgs map[string]string // build-args
|
|
Insecure bool // allow http registry / skip TLS verify on push
|
|
// RegistryAuth maps registry hostname -> {username, password}. Empty
|
|
// for anonymous (e.g. localhost:5000). For ghcr.io use the agent owner
|
|
// + PAT.
|
|
RegistryAuth map[string]registryCreds
|
|
}
|
|
|
|
type registryCreds struct {
|
|
Username string
|
|
Password string
|
|
}
|
|
|
|
// runDockerBuild solves a Dockerfile via BuildKit and pushes the image.
|
|
// Returns the tail of the BuildKit progress log on success or failure.
|
|
func runDockerBuild(ctx context.Context, opts dockerBuildOpts) (string, error) {
|
|
if opts.BuildKitAddr == "" {
|
|
return "", errors.New("BUILDKIT_HOST is not configured")
|
|
}
|
|
if opts.ImageRef == "" {
|
|
return "", errors.New("imageRef is required")
|
|
}
|
|
if opts.ContextDir == "" {
|
|
return "", errors.New("contextDir is required")
|
|
}
|
|
if opts.Dockerfile == "" {
|
|
opts.Dockerfile = "Dockerfile"
|
|
}
|
|
|
|
bk, err := client.New(ctx, opts.BuildKitAddr)
|
|
if err != nil {
|
|
return "", fmt.Errorf("buildkit dial %s: %w", opts.BuildKitAddr, err)
|
|
}
|
|
defer bk.Close()
|
|
|
|
dfDir, dfName := splitDockerfile(opts.Dockerfile)
|
|
localDirs := map[string]string{
|
|
dockerui.DefaultLocalNameContext: opts.ContextDir,
|
|
dockerui.DefaultLocalNameDockerfile: joinIfRel(opts.ContextDir, dfDir),
|
|
}
|
|
|
|
frontendAttrs := map[string]string{
|
|
"filename": dfName,
|
|
}
|
|
for k, v := range opts.BuildArgs {
|
|
frontendAttrs["build-arg:"+k] = v
|
|
}
|
|
if opts.Platform != "" {
|
|
frontendAttrs["platform"] = opts.Platform
|
|
}
|
|
|
|
exporter := client.ExportEntry{
|
|
Type: client.ExporterImage,
|
|
Attrs: map[string]string{
|
|
"name": opts.ImageRef,
|
|
"push": "true",
|
|
"registry.insecure": boolStr(opts.Insecure),
|
|
},
|
|
}
|
|
|
|
// session.Attachable for registry auth
|
|
authAttachable := newAuthAttachable(opts.RegistryAuth)
|
|
|
|
// Capture BuildKit's progress UI line-by-line. Each line is forwarded
|
|
// to the per-node logger (live SSE) AND appended to a tail buffer for
|
|
// the final summary embedded in __build.log_tail.
|
|
logger := engine.NodeLoggerFromContext(ctx)
|
|
var statusBuf strings.Builder
|
|
streamer := &streamingLineWriter{logger: logger, tail: &statusBuf}
|
|
|
|
statusCh := make(chan *client.SolveStatus, 16)
|
|
|
|
eg, gctx := errgroup.WithContext(ctx)
|
|
eg.Go(func() error {
|
|
_, err := bk.Solve(gctx, nil, client.SolveOpt{
|
|
Frontend: "dockerfile.v0",
|
|
FrontendAttrs: frontendAttrs,
|
|
LocalDirs: localDirs,
|
|
Exports: []client.ExportEntry{exporter},
|
|
Session: []session.Attachable{authAttachable},
|
|
}, statusCh)
|
|
return err
|
|
})
|
|
eg.Go(func() error {
|
|
display, derr := progressui.NewDisplay(streamer, progressui.PlainMode)
|
|
if derr != nil {
|
|
for range statusCh {
|
|
}
|
|
return derr
|
|
}
|
|
_, uerr := display.UpdateFrom(gctx, statusCh)
|
|
return uerr
|
|
})
|
|
|
|
err = eg.Wait()
|
|
streamer.flush() // emit any trailing partial line
|
|
tail := lastLines(statusBuf.String(), 80)
|
|
return tail, err
|
|
}
|
|
|
|
// newAuthAttachable returns a buildkit auth attachable populated with the
|
|
// given registry creds. Empty creds map → anonymous (still works for
|
|
// localhost:5000-style local registries).
|
|
func newAuthAttachable(creds map[string]registryCreds) session.Attachable {
|
|
cfg := &cliconfig.ConfigFile{
|
|
AuthConfigs: map[string]cliconfigtypes.AuthConfig{},
|
|
}
|
|
for host, c := range creds {
|
|
cfg.AuthConfigs[host] = cliconfigtypes.AuthConfig{
|
|
ServerAddress: host,
|
|
Username: c.Username,
|
|
Password: c.Password,
|
|
}
|
|
}
|
|
return authprovider.NewDockerAuthProvider(cfg, nil)
|
|
}
|
|
|
|
// streamingLineWriter is an io.Writer that splits BuildKit's plain-mode
|
|
// progress output on newlines and forwards each complete line to a
|
|
// NodeLogger so the UI can tail them live. Bytes also accumulate into
|
|
// `tail` for the final log_tail summary embedded in __build.
|
|
type streamingLineWriter struct {
|
|
logger engine.NodeLogger
|
|
tail *strings.Builder
|
|
buf []byte
|
|
}
|
|
|
|
func (w *streamingLineWriter) Write(p []byte) (int, error) {
|
|
if w.tail != nil {
|
|
w.tail.Write(p)
|
|
}
|
|
w.buf = append(w.buf, p...)
|
|
for {
|
|
i := indexByte(w.buf, '\n')
|
|
if i < 0 {
|
|
return len(p), nil
|
|
}
|
|
line := strings.TrimRight(string(w.buf[:i]), "\r")
|
|
if w.logger != nil && line != "" {
|
|
w.logger.Log(line)
|
|
}
|
|
w.buf = w.buf[i+1:]
|
|
}
|
|
}
|
|
|
|
// flush emits the trailing partial line if any.
|
|
func (w *streamingLineWriter) flush() {
|
|
if len(w.buf) == 0 {
|
|
return
|
|
}
|
|
line := strings.TrimRight(string(w.buf), "\r\n")
|
|
if w.logger != nil && line != "" {
|
|
w.logger.Log(line)
|
|
}
|
|
w.buf = nil
|
|
}
|
|
|
|
func indexByte(b []byte, c byte) int {
|
|
for i, x := range b {
|
|
if x == c {
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
func splitDockerfile(p string) (dir, name string) {
|
|
p = strings.TrimPrefix(strings.TrimSpace(p), "./")
|
|
if p == "" {
|
|
return "", "Dockerfile"
|
|
}
|
|
if i := strings.LastIndex(p, "/"); i >= 0 {
|
|
return p[:i], p[i+1:]
|
|
}
|
|
return "", p
|
|
}
|
|
|
|
func joinIfRel(root, sub string) string {
|
|
if sub == "" {
|
|
return root
|
|
}
|
|
if strings.HasPrefix(sub, "/") {
|
|
return sub
|
|
}
|
|
return strings.TrimRight(root, "/") + "/" + sub
|
|
}
|
|
|
|
func boolStr(b bool) string {
|
|
if b {
|
|
return "true"
|
|
}
|
|
return "false"
|
|
}
|