eclipse/cmd/eclipse-run/main.go

186 lines
4.2 KiB
Go

// Copyright (C) 2023-2024 Umorpha Systems
// SPDX-License-Identifier: AGPL-3.0-or-later
package main
import (
"context"
"encoding/json"
"fmt"
"io"
"io/fs"
"log"
"mime"
"mime/multipart"
"net/http"
"net/textproto"
"os"
"os/exec"
"strings"
"github.com/datawire/ocibuild/pkg/cliutil"
"github.com/spf13/cobra"
source "git.mothstuff.lol/lukeshu/eclipse"
"git.mothstuff.lol/lukeshu/eclipse/lib/eclipse"
)
func main() {
argparser := &cobra.Command{
Use: os.Args[0] + " [flags] JOB_URL",
Short: "Grab a job from the central cordinator server",
Args: cliutil.WrapPositionalArgs(cobra.ExactArgs(1)),
SilenceErrors: true, // we'll handle this ourselves after .ExecuteContext()
SilenceUsage: true, // FlagErrorFunc will handle this
CompletionOptions: cobra.CompletionOptions{
DisableDefaultCmd: true,
},
}
argparser.SetFlagErrorFunc(cliutil.FlagErrorFunc)
argparser.SetHelpTemplate(cliutil.HelpTemplate)
var apikeyFile string
argparser.Flags().StringVar(&apikeyFile, "apikey-file", source.DefaultAPIKeyFile,
"file containing the API key to use")
argparser.MarkFlagFilename("apikey-file", "yml", "yaml")
argparser.RunE = func(cmd *cobra.Command, args []string) error {
return Run(apikeyFile, args[0])
}
ctx := context.Background()
if err := argparser.ExecuteContext(ctx); err != nil {
fmt.Fprintf(os.Stderr, "%v: error: %v\n", argparser.CommandPath(), err)
os.Exit(1)
}
}
func Run(apikeyFile, jobURL string) (err error) {
maybeSetErr := func(_err error) {
if err == nil && _err != nil {
err = _err
}
}
_apikey, err := os.ReadFile(apikeyFile)
if err != nil {
return err
}
apikey := strings.TrimSpace(string(_apikey))
attachmentsDir, err := os.MkdirTemp("", "eclipse-run.*")
if err != nil {
return err
}
defer func() { maybeSetErr(os.RemoveAll(attachmentsDir)) }()
pipeR, pipeW := io.Pipe()
defer func() {
if err != nil {
pipeW.CloseWithError(err)
}
}()
writer := multipart.NewWriter(pipeW)
req, err := http.NewRequest(http.MethodPost, jobURL, pipeR)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+apikey)
req.Header.Set("Content-Type", mime.FormatMediaType("multipart/mixed", map[string]string{
"boundary": writer.Boundary(),
}))
log.Printf("dialing %q...", jobURL)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
log.Printf("... dialed")
////////////////////////////////////////////////////////////////////////
if resp.StatusCode != http.StatusOK {
err := fmt.Errorf("HTTP %s", resp.Status)
body, _ := io.ReadAll(resp.Body)
if len(body) > 0 {
err = fmt.Errorf("%w\n%s", err, body)
}
return err
}
dec := json.NewDecoder(resp.Body)
log.Printf("reading job description...")
var job eclipse.Job
if err := dec.Decode(&job); err != nil {
return err
}
log.Printf("... read")
hdr := make(textproto.MIMEHeader)
hdr.Set("Content-Disposition", `attachment; filename="log.txt"`)
part, err := writer.CreatePart(hdr)
if err != nil {
return err
}
cmd := exec.Command("sh", "-c", job.Command)
cmd.Stdout = part
cmd.Stderr = part
cmd.Env = append(os.Environ(),
"ECLIPSE_ATTACHMENTSDIR="+attachmentsDir)
status := "0"
if err := cmd.Run(); err != nil {
status = err.Error()
}
attachmentsFS := os.DirFS(attachmentsDir)
if err := fs.WalkDir(attachmentsFS, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.Type() != 0 {
return nil
}
hdr.Set("Content-Disposition", mime.FormatMediaType("attachment", map[string]string{
"filename": "attachments/" + path,
}))
part, err := writer.CreatePart(hdr)
if err != nil {
return err
}
fh, err := attachmentsFS.Open(path)
if err != nil {
return err
}
if _, err := io.Copy(part, fh); err != nil {
_ = fh.Close()
return err
}
if err := fh.Close(); err != nil {
return err
}
return nil
}); err != nil {
return err
}
hdr.Set("Content-Disposition", mime.FormatMediaType("form-data", map[string]string{
"name": "status",
}))
part, err = writer.CreatePart(hdr)
if err != nil {
return err
}
if _, err := io.WriteString(part, status); err != nil {
return err
}
if err := writer.Close(); err != nil {
return err
}
return nil
}