cmd/osbuild-worker-executor: import verbatim from mvo5/oaas
This commit imports the repository https://github.com/mvo5/oaas without keeping the history. The history is largely unimportant and can be looked up in https://github.com/mvo5/oaas/commits/main if needed.
This commit is contained in:
parent
fa416e4545
commit
372d9f07dd
12 changed files with 1024 additions and 0 deletions
38
cmd/osbuild-worker-executor/build_result.go
Normal file
38
cmd/osbuild-worker-executor/build_result.go
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
type buildResult struct {
|
||||||
|
resultGood string
|
||||||
|
resultBad string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newBuildResult(config *Config) *buildResult {
|
||||||
|
return &buildResult{
|
||||||
|
resultGood: filepath.Join(config.BuildDirBase, "result.good"),
|
||||||
|
resultBad: filepath.Join(config.BuildDirBase, "result.bad"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *buildResult) Mark(err error) error {
|
||||||
|
if err == nil {
|
||||||
|
return ioutil.WriteFile(br.resultGood, nil, 0600)
|
||||||
|
} else {
|
||||||
|
return ioutil.WriteFile(br.resultBad, nil, 0600)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo: switch to (Good, Bad, Unknown)
|
||||||
|
func (br *buildResult) Good() bool {
|
||||||
|
_, err := os.Stat(br.resultGood)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *buildResult) Bad() bool {
|
||||||
|
_, err := os.Stat(br.resultBad)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
25
cmd/osbuild-worker-executor/config.go
Normal file
25
cmd/osbuild-worker-executor/config.go
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Host string
|
||||||
|
Port string
|
||||||
|
|
||||||
|
BuildDirBase string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newConfigFromCmdline(args []string) (*Config, error) {
|
||||||
|
var config Config
|
||||||
|
|
||||||
|
fs := flag.NewFlagSet("oaas", flag.ContinueOnError)
|
||||||
|
fs.StringVar(&config.Host, "host", "localhost", "host to listen on")
|
||||||
|
fs.StringVar(&config.Port, "port", "8001", "port to listen on")
|
||||||
|
fs.StringVar(&config.BuildDirBase, "build-path", "/var/tmp/oaas", "base dir to run the builds in")
|
||||||
|
if err := fs.Parse(args); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &config, nil
|
||||||
|
}
|
||||||
45
cmd/osbuild-worker-executor/export_test.go
Normal file
45
cmd/osbuild-worker-executor/export_test.go
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
logrusTest "github.com/sirupsen/logrus/hooks/test"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
Run = run
|
||||||
|
|
||||||
|
HandleIncludedSources = handleIncludedSources
|
||||||
|
)
|
||||||
|
|
||||||
|
func MockLogger() (hook *logrusTest.Hook, restore func()) {
|
||||||
|
saved := logrusNew
|
||||||
|
logger, hook := logrusTest.NewNullLogger()
|
||||||
|
logrusNew = func() *logrus.Logger {
|
||||||
|
return logger
|
||||||
|
}
|
||||||
|
logger.SetLevel(logrus.DebugLevel)
|
||||||
|
|
||||||
|
return hook, func() {
|
||||||
|
logrusNew = saved
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func MockOsbuildBinary(t *testing.T, new string) (restore func()) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
saved := osbuildBinary
|
||||||
|
|
||||||
|
tmpdir := t.TempDir()
|
||||||
|
osbuildBinary = filepath.Join(tmpdir, "fake-osbuild")
|
||||||
|
if err := ioutil.WriteFile(osbuildBinary, []byte(new), 0755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return func() {
|
||||||
|
osbuildBinary = saved
|
||||||
|
}
|
||||||
|
}
|
||||||
286
cmd/osbuild-worker-executor/handler_build.go
Normal file
286
cmd/osbuild-worker-executor/handler_build.go
Normal file
|
|
@ -0,0 +1,286 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
supportedBuildContentTypes = []string{"application/x-tar"}
|
||||||
|
osbuildBinary = "osbuild"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrAlreadyBuilding = errors.New("build already starte")
|
||||||
|
)
|
||||||
|
|
||||||
|
type writeFlusher struct {
|
||||||
|
w io.Writer
|
||||||
|
flusher http.Flusher
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wf *writeFlusher) Write(p []byte) (n int, err error) {
|
||||||
|
n, err = wf.w.Write(p)
|
||||||
|
if wf.flusher != nil {
|
||||||
|
wf.flusher.Flush()
|
||||||
|
}
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func runOsbuild(buildDir string, control *controlJSON, output io.Writer) (string, error) {
|
||||||
|
flusher, ok := output.(http.Flusher)
|
||||||
|
if !ok {
|
||||||
|
return "", fmt.Errorf("cannot stream the output")
|
||||||
|
}
|
||||||
|
// stream output over http
|
||||||
|
wf := writeFlusher{w: output, flusher: flusher}
|
||||||
|
// and also write to our internal log
|
||||||
|
logf, err := os.Create(filepath.Join(buildDir, "build.log"))
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("cannot create log file: %v", err)
|
||||||
|
}
|
||||||
|
defer logf.Close()
|
||||||
|
|
||||||
|
// use multi writer to get same output for stream and log
|
||||||
|
mw := io.MultiWriter(&wf, logf)
|
||||||
|
outputDir := filepath.Join(buildDir, "output")
|
||||||
|
storeDir := filepath.Join(buildDir, "store")
|
||||||
|
cmd := exec.Command(osbuildBinary)
|
||||||
|
cmd.Stdout = mw
|
||||||
|
cmd.Stderr = mw
|
||||||
|
for _, exp := range control.Exports {
|
||||||
|
cmd.Args = append(cmd.Args, []string{"--export", exp}...)
|
||||||
|
}
|
||||||
|
cmd.Env = append(cmd.Env, control.Environments...)
|
||||||
|
cmd.Args = append(cmd.Args, []string{"--output-dir", outputDir}...)
|
||||||
|
cmd.Args = append(cmd.Args, []string{"--store", storeDir}...)
|
||||||
|
cmd.Args = append(cmd.Args, "--json")
|
||||||
|
cmd.Args = append(cmd.Args, filepath.Join(buildDir, "manifest.json"))
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cmd.Wait(); err != nil {
|
||||||
|
// we cannot use "http.Error()" here because the http
|
||||||
|
// header was already set to "201" when we started streaming
|
||||||
|
mw.Write([]byte(fmt.Sprintf("cannot run osbuild: %v", err)))
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd = exec.Command(
|
||||||
|
"tar",
|
||||||
|
"-Scf",
|
||||||
|
filepath.Join(outputDir, "output.tar"),
|
||||||
|
"output",
|
||||||
|
)
|
||||||
|
cmd.Dir = buildDir
|
||||||
|
out, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("cannot tar output directory: %w, output:\n%s", err, out)
|
||||||
|
logrus.Errorf(err.Error())
|
||||||
|
mw.Write([]byte(err.Error()))
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
logrus.Infof("tar output:\n%s", out)
|
||||||
|
return outputDir, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type controlJSON struct {
|
||||||
|
Environments []string `json:"environments"`
|
||||||
|
Exports []string `json:"exports"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustRead(atar *tar.Reader, name string) error {
|
||||||
|
hdr, err := atar.Next()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot read tar %v: %v", name, err)
|
||||||
|
}
|
||||||
|
if hdr.Name != name {
|
||||||
|
return fmt.Errorf("expected tar %v, got %v", name, hdr.Name)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleControlJSON(atar *tar.Reader) (*controlJSON, error) {
|
||||||
|
if err := mustRead(atar, "control.json"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var control controlJSON
|
||||||
|
if err := json.NewDecoder(atar).Decode(&control); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &control, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createBuildDir(config *Config) (string, error) {
|
||||||
|
buildDirBase := config.BuildDirBase
|
||||||
|
|
||||||
|
// we could create a per-build dir here but the goal is to
|
||||||
|
// only have a single build only so we don't bother
|
||||||
|
if err := os.MkdirAll(buildDirBase, 0700); err != nil {
|
||||||
|
return "", fmt.Errorf("cannot create build base dir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure there is only a single build
|
||||||
|
buildDir := filepath.Join(buildDirBase, "build")
|
||||||
|
if err := os.Mkdir(buildDir, 0700); err != nil {
|
||||||
|
if os.IsExist(err) {
|
||||||
|
return "", ErrAlreadyBuilding
|
||||||
|
}
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildDir, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleManifestJSON(atar *tar.Reader, buildDir string) error {
|
||||||
|
if err := mustRead(atar, "manifest.json"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
manifestJSONPath := filepath.Join(buildDir, "manifest.json")
|
||||||
|
|
||||||
|
f, err := os.Create(manifestJSONPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot create manifest.json: %v", err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
if _, err := io.Copy(f, atar); err != nil {
|
||||||
|
return fmt.Errorf("cannot read body: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := f.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleIncludedSources(atar *tar.Reader, buildDir string) error {
|
||||||
|
for {
|
||||||
|
hdr, err := atar.Next()
|
||||||
|
if err == io.EOF {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot read from tar %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure we only allow "store/" things
|
||||||
|
if filepath.Clean(hdr.Name) != strings.TrimSuffix(hdr.Name, "/") {
|
||||||
|
return fmt.Errorf("name not clean: %v != %v", filepath.Clean(hdr.Name), hdr.Name)
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(hdr.Name, "store/") {
|
||||||
|
return fmt.Errorf("expected store/ prefix, got %v", hdr.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// this assume "well" behaving tars, i.e. all dirs that lead
|
||||||
|
// up to the tar are included etc
|
||||||
|
target := filepath.Join(buildDir, hdr.Name)
|
||||||
|
mode := os.FileMode(hdr.Mode)
|
||||||
|
switch hdr.Typeflag {
|
||||||
|
case tar.TypeDir:
|
||||||
|
if err := os.Mkdir(target, mode); err != nil {
|
||||||
|
return fmt.Errorf("unpack: %w", err)
|
||||||
|
}
|
||||||
|
case tar.TypeReg:
|
||||||
|
f, err := os.OpenFile(target, os.O_RDWR|os.O_CREATE, mode)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unpack: %w", err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
if _, err := io.Copy(f, atar); err != nil {
|
||||||
|
return fmt.Errorf("unpack: %w", err)
|
||||||
|
}
|
||||||
|
if err := f.Close(); err != nil {
|
||||||
|
return fmt.Errorf("unpack: %w", err)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported tar type %v", hdr.Typeflag)
|
||||||
|
}
|
||||||
|
if err := os.Chtimes(target, hdr.AccessTime, hdr.ModTime); err != nil {
|
||||||
|
return fmt.Errorf("unpack: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// test for real via:
|
||||||
|
// curl -o - --data-binary "@./test.tar" -H "Content-Type: application/x-tar" -X POST http://localhost:8001/api/v1/build
|
||||||
|
func handleBuild(logger *logrus.Logger, config *Config) http.Handler {
|
||||||
|
return http.HandlerFunc(
|
||||||
|
func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
logger.Debugf("handlerBuild called on %s", r.URL.Path)
|
||||||
|
defer r.Body.Close()
|
||||||
|
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "build endpoint only supports POST", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
contentType := r.Header.Get("Content-Type")
|
||||||
|
if !slices.Contains(supportedBuildContentTypes, contentType) {
|
||||||
|
http.Error(w, fmt.Sprintf("Content-Type must be %v, got %v", supportedBuildContentTypes, contentType), http.StatusUnsupportedMediaType)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// control.json passes the build parameters
|
||||||
|
atar := tar.NewReader(r.Body)
|
||||||
|
control, err := handleControlJSON(atar)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(err)
|
||||||
|
http.Error(w, "cannot decode control.json", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
buildDir, err := createBuildDir(config)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(err)
|
||||||
|
if err == ErrAlreadyBuilding {
|
||||||
|
http.Error(w, "build already started", http.StatusConflict)
|
||||||
|
} else {
|
||||||
|
http.Error(w, "create build dir", http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// manifest.json is the osbuild input
|
||||||
|
if err := handleManifestJSON(atar, buildDir); err != nil {
|
||||||
|
logger.Error(err)
|
||||||
|
http.Error(w, "manifest.json", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// extract ".osbuild/sources" here too from the tar
|
||||||
|
if err := handleIncludedSources(atar, buildDir); err != nil {
|
||||||
|
logger.Error(err)
|
||||||
|
http.Error(w, "included sources/", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
|
||||||
|
// run osbuild and stream the output to the client
|
||||||
|
buildResult := newBuildResult(config)
|
||||||
|
_, err = runOsbuild(buildDir, control, w)
|
||||||
|
if werr := buildResult.Mark(err); werr != nil {
|
||||||
|
logger.Errorf("cannot write result file %v", werr)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("canot run osbuild: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
321
cmd/osbuild-worker-executor/handler_build_test.go
Normal file
321
cmd/osbuild-worker-executor/handler_build_test.go
Normal file
|
|
@ -0,0 +1,321 @@
|
||||||
|
package main_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
main "github.com/osbuild/osbuild-composer/cmd/oaas"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBuildMustPOST(t *testing.T) {
|
||||||
|
baseURL, _, loggerHook := runTestServer(t)
|
||||||
|
|
||||||
|
endpoint := baseURL + "api/v1/build"
|
||||||
|
rsp, err := http.Get(endpoint)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
defer rsp.Body.Close()
|
||||||
|
assert.Equal(t, rsp.StatusCode, 405)
|
||||||
|
assert.Equal(t, loggerHook.LastEntry().Message, "handlerBuild called on /api/v1/build")
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeToTar(atar *tar.Writer, name, content string) error {
|
||||||
|
hdr := &tar.Header{
|
||||||
|
Name: name,
|
||||||
|
Mode: 0644,
|
||||||
|
Size: int64(len(content)),
|
||||||
|
}
|
||||||
|
if err := atar.WriteHeader(hdr); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err := atar.Write([]byte(content))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildChecksContentType(t *testing.T) {
|
||||||
|
baseURL, _, _ := runTestServer(t)
|
||||||
|
|
||||||
|
endpoint := baseURL + "api/v1/build"
|
||||||
|
rsp, err := http.Post(endpoint, "random/encoding", nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
defer rsp.Body.Close()
|
||||||
|
assert.Equal(t, rsp.StatusCode, http.StatusUnsupportedMediaType)
|
||||||
|
body, err := ioutil.ReadAll(rsp.Body)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, string(body), "Content-Type must be [application/x-tar], got random/encoding\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeTestPost(t *testing.T, controlJSON, manifestJSON string) *bytes.Buffer {
|
||||||
|
buf := bytes.NewBuffer(nil)
|
||||||
|
archive := tar.NewWriter(buf)
|
||||||
|
err := writeToTar(archive, "control.json", controlJSON)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = writeToTar(archive, "manifest.json", manifestJSON)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
// for now we assume we get validated data, for files we could
|
||||||
|
// trivially validate on the fly but for containers that is
|
||||||
|
// harder
|
||||||
|
for _, dir := range []string{"store/", "store/sources", "store/sources/org.osbuild.files"} {
|
||||||
|
err = archive.WriteHeader(&tar.Header{
|
||||||
|
Name: dir,
|
||||||
|
Mode: 0755,
|
||||||
|
Typeflag: tar.TypeDir,
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
err = writeToTar(archive, "store/sources/org.osbuild.files/sha256:ff800c5263b915d8a0776be5620575df2d478332ad35e8dd18def6a8c720f9c7", "random-data")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = writeToTar(archive, "store/sources/org.osbuild.files/sha256:aabbcc5263b915d8a0776be5620575df2d478332ad35e8dd18def6a8c720f9c7", "other-data")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildIntegration(t *testing.T) {
|
||||||
|
baseURL, baseBuildDir, _ := runTestServer(t)
|
||||||
|
endpoint := baseURL + "api/v1/build"
|
||||||
|
|
||||||
|
// osbuild is called with --export tree and then the manifest.json
|
||||||
|
restore := main.MockOsbuildBinary(t, fmt.Sprintf(`#!/bin/sh -e
|
||||||
|
# echo our inputs for the test to validate
|
||||||
|
echo fake-osbuild "$1" "$2" "$3" "$4" "$5" "$6" "$7"
|
||||||
|
echo ---
|
||||||
|
cat "$8"
|
||||||
|
|
||||||
|
test "$MY" = "env"
|
||||||
|
|
||||||
|
# simulate output
|
||||||
|
mkdir -p %[1]s/build/output/image
|
||||||
|
echo "fake-build-result" > %[1]s/build/output/image/disk.img
|
||||||
|
`, baseBuildDir))
|
||||||
|
defer restore()
|
||||||
|
|
||||||
|
buf := makeTestPost(t, `{"exports": ["tree"], "environments": ["MY=env"]}`, `{"fake": "manifest"}`)
|
||||||
|
rsp, err := http.Post(endpoint, "application/x-tar", buf)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
defer ioutil.ReadAll(rsp.Body)
|
||||||
|
defer rsp.Body.Close()
|
||||||
|
|
||||||
|
assert.Equal(t, rsp.StatusCode, http.StatusCreated)
|
||||||
|
reader := bufio.NewReader(rsp.Body)
|
||||||
|
// check that we get the output of osbuild streamed to us
|
||||||
|
expectedContent := fmt.Sprintf(`fake-osbuild --export tree --output-dir %[1]s/build/output --store %[1]s/build/store --json
|
||||||
|
---
|
||||||
|
{"fake": "manifest"}`, baseBuildDir)
|
||||||
|
content, err := ioutil.ReadAll(reader)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, string(content), expectedContent)
|
||||||
|
// check log too
|
||||||
|
logFileContent, err := ioutil.ReadFile(filepath.Join(baseBuildDir, "build/build.log"))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, expectedContent, string(logFileContent))
|
||||||
|
// check that the "store" dir got created
|
||||||
|
stat, err := os.Stat(filepath.Join(baseBuildDir, "build/store"))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, stat.IsDir())
|
||||||
|
|
||||||
|
// now get the result
|
||||||
|
endpoint = baseURL + "api/v1/result/image/disk.img"
|
||||||
|
rsp, err = http.Get(endpoint)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
defer rsp.Body.Close()
|
||||||
|
assert.Equal(t, http.StatusOK, rsp.StatusCode)
|
||||||
|
body, err := ioutil.ReadAll(rsp.Body)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "fake-build-result\n", string(body))
|
||||||
|
|
||||||
|
// check that the output tarball has the disk in it
|
||||||
|
endpoint = baseURL + "api/v1/result/output.tar"
|
||||||
|
rsp, err = http.Get(endpoint)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
defer rsp.Body.Close()
|
||||||
|
assert.Equal(t, http.StatusOK, rsp.StatusCode)
|
||||||
|
body, err = ioutil.ReadAll(rsp.Body)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
tarPath := filepath.Join(baseBuildDir, "output.tar")
|
||||||
|
assert.NoError(t, os.WriteFile(tarPath, body, 0644))
|
||||||
|
cmd := exec.Command("tar", "-tf", tarPath)
|
||||||
|
out, err := cmd.Output()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "output/\noutput/image/\noutput/image/disk.img\n", string(out))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildErrorsForMultipleBuilds(t *testing.T) {
|
||||||
|
baseURL, buildDir, loggerHook := runTestServer(t)
|
||||||
|
endpoint := baseURL + "api/v1/build"
|
||||||
|
|
||||||
|
restore := main.MockOsbuildBinary(t, fmt.Sprintf(`#!/bin/sh
|
||||||
|
|
||||||
|
mkdir -p %[1]s/build/output/image
|
||||||
|
echo "fake-build-result" > %[1]s/build/output/image/disk.img
|
||||||
|
`, buildDir))
|
||||||
|
defer restore()
|
||||||
|
|
||||||
|
buf := makeTestPost(t, `{"exports": ["tree"]}`, `{"fake": "manifest"}`)
|
||||||
|
rsp, err := http.Post(endpoint, "application/x-tar", buf)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, rsp.StatusCode, http.StatusCreated)
|
||||||
|
defer ioutil.ReadAll(rsp.Body)
|
||||||
|
defer rsp.Body.Close()
|
||||||
|
|
||||||
|
buf = makeTestPost(t, `{"exports": ["tree"]}`, `{"fake": "manifest"}`)
|
||||||
|
rsp, err = http.Post(endpoint, "application/x-tar", buf)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
defer rsp.Body.Close()
|
||||||
|
assert.Equal(t, rsp.StatusCode, http.StatusConflict)
|
||||||
|
assert.Equal(t, loggerHook.LastEntry().Message, main.ErrAlreadyBuilding.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleIncludedSourcesUnclean(t *testing.T) {
|
||||||
|
tmpdir := t.TempDir()
|
||||||
|
|
||||||
|
buf := bytes.NewBuffer(nil)
|
||||||
|
atar := tar.NewWriter(buf)
|
||||||
|
err := writeToTar(atar, "store/../../etc/passwd", "some-content")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = main.HandleIncludedSources(tar.NewReader(buf), tmpdir)
|
||||||
|
assert.EqualError(t, err, "name not clean: ../etc/passwd != store/../../etc/passwd")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleIncludedSourcesNotFromStore(t *testing.T) {
|
||||||
|
tmpdir := t.TempDir()
|
||||||
|
|
||||||
|
buf := bytes.NewBuffer(nil)
|
||||||
|
atar := tar.NewWriter(buf)
|
||||||
|
err := writeToTar(atar, "not-store", "some-content")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = main.HandleIncludedSources(tar.NewReader(buf), tmpdir)
|
||||||
|
assert.EqualError(t, err, "expected store/ prefix, got not-store")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleIncludedSourcesBadTypes(t *testing.T) {
|
||||||
|
tmpdir := t.TempDir()
|
||||||
|
|
||||||
|
for _, badType := range []byte{tar.TypeLink, tar.TypeSymlink, tar.TypeChar, tar.TypeBlock, tar.TypeFifo} {
|
||||||
|
buf := bytes.NewBuffer(nil)
|
||||||
|
atar := tar.NewWriter(buf)
|
||||||
|
err := atar.WriteHeader(&tar.Header{
|
||||||
|
Name: "store/bad-type",
|
||||||
|
Typeflag: badType,
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = main.HandleIncludedSources(tar.NewReader(buf), tmpdir)
|
||||||
|
assert.EqualError(t, err, fmt.Sprintf("unsupported tar type %v", badType))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildIntegrationOsbuildError(t *testing.T) {
|
||||||
|
baseURL, _, _ := runTestServer(t)
|
||||||
|
endpoint := baseURL + "api/v1/build"
|
||||||
|
|
||||||
|
// osbuild is called with --export tree and then the manifest.json
|
||||||
|
restore := main.MockOsbuildBinary(t, `#!/bin/sh -e
|
||||||
|
# simulate failure
|
||||||
|
echo "err on stdout"
|
||||||
|
>&2 echo "err on stderr"
|
||||||
|
exit 23
|
||||||
|
`)
|
||||||
|
defer restore()
|
||||||
|
|
||||||
|
buf := makeTestPost(t, `{"exports": ["tree"], "environments": ["MY=env"]}`, `{"fake": "manifest"}`)
|
||||||
|
rsp, err := http.Post(endpoint, "application/x-tar", buf)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
defer ioutil.ReadAll(rsp.Body)
|
||||||
|
defer rsp.Body.Close()
|
||||||
|
|
||||||
|
assert.Equal(t, rsp.StatusCode, http.StatusCreated)
|
||||||
|
reader := bufio.NewReader(rsp.Body)
|
||||||
|
content, err := ioutil.ReadAll(reader)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
expectedContent := `err on stdout
|
||||||
|
err on stderr
|
||||||
|
cannot run osbuild: exit status 23`
|
||||||
|
assert.Equal(t, expectedContent, string(content))
|
||||||
|
|
||||||
|
// check that the result is an error and we get the log
|
||||||
|
endpoint = baseURL + "api/v1/result/image/disk.img"
|
||||||
|
rsp, err = http.Get(endpoint)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
defer rsp.Body.Close()
|
||||||
|
assert.Equal(t, http.StatusBadRequest, rsp.StatusCode)
|
||||||
|
reader = bufio.NewReader(rsp.Body)
|
||||||
|
content, err = ioutil.ReadAll(reader)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "build failed\n"+expectedContent, string(content))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildStreamsOutput(t *testing.T) {
|
||||||
|
baseURL, baseBuildDir, _ := runTestServer(t)
|
||||||
|
endpoint := baseURL + "api/v1/build"
|
||||||
|
|
||||||
|
restore := main.MockOsbuildBinary(t, fmt.Sprintf(`#!/bin/sh -e
|
||||||
|
for i in $(seq 3); do
|
||||||
|
# generate the exact timestamp of the output line
|
||||||
|
echo "line-$i: $(date +'%%s.%%N')"
|
||||||
|
sleep 0.2
|
||||||
|
done
|
||||||
|
|
||||||
|
# simulate output
|
||||||
|
mkdir -p %[1]s/build/output/image
|
||||||
|
echo "fake-build-result" > %[1]s/build/output/image/disk.img
|
||||||
|
`, baseBuildDir))
|
||||||
|
defer restore()
|
||||||
|
|
||||||
|
buf := makeTestPost(t, `{"exports": ["tree"], "environments": ["MY=env"]}`, `{"fake": "manifest"}`)
|
||||||
|
rsp, err := http.Post(endpoint, "application/x-tar", buf)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
defer ioutil.ReadAll(rsp.Body)
|
||||||
|
defer rsp.Body.Close()
|
||||||
|
|
||||||
|
assert.Equal(t, rsp.StatusCode, http.StatusCreated)
|
||||||
|
reader := bufio.NewReader(rsp.Body)
|
||||||
|
|
||||||
|
var lineno, seconds, nano int64
|
||||||
|
for i := 1; i <= 3; i++ {
|
||||||
|
line, err := reader.ReadString('\n')
|
||||||
|
assert.NoError(t, err)
|
||||||
|
// the out contains when it was generated
|
||||||
|
_, err = fmt.Sscanf(line, "line-%d: %d.%d\n", &lineno, &seconds, &nano)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
timeSinceOutput := time.Now().Sub(time.Unix(seconds, nano))
|
||||||
|
// we expect lines to appear right away, for really slow VMs
|
||||||
|
// we give a grace time of 200ms (which should be plenty and
|
||||||
|
// is also a bit arbitrary)
|
||||||
|
assert.True(t, timeSinceOutput < 200*time.Millisecond, fmt.Sprintf("output did not arrive in the expected time interval, delay: %v", timeSinceOutput))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildErrorHandlingTar(t *testing.T) {
|
||||||
|
restore := main.MockOsbuildBinary(t, `#!/bin/sh
|
||||||
|
|
||||||
|
# not creating an output dir, this will lead to errors from the "tar"
|
||||||
|
# step
|
||||||
|
`)
|
||||||
|
defer restore()
|
||||||
|
|
||||||
|
baseURL, _, loggerHook := runTestServer(t)
|
||||||
|
endpoint := baseURL + "api/v1/build"
|
||||||
|
|
||||||
|
buf := makeTestPost(t, `{"exports": ["tree"]}`, `{"fake": "manifest"}`)
|
||||||
|
rsp, err := http.Post(endpoint, "application/x-tar", buf)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
defer rsp.Body.Close()
|
||||||
|
assert.Equal(t, rsp.StatusCode, http.StatusCreated)
|
||||||
|
|
||||||
|
body, err := ioutil.ReadAll(rsp.Body)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Contains(t, string(body), "cannot tar output directory:")
|
||||||
|
assert.Contains(t, loggerHook.LastEntry().Message, "cannot tar output directory:")
|
||||||
|
}
|
||||||
43
cmd/osbuild-worker-executor/handler_result.go
Normal file
43
cmd/osbuild-worker-executor/handler_result.go
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
func handleResult(logger *logrus.Logger, config *Config) http.Handler {
|
||||||
|
return http.HandlerFunc(
|
||||||
|
func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
logger.Debugf("handlerResult called on %s", r.URL.Path)
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
http.Error(w, "result endpoint only supports Get", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
buildResult := newBuildResult(config)
|
||||||
|
switch {
|
||||||
|
case buildResult.Bad():
|
||||||
|
http.Error(w, "build failed", http.StatusBadRequest)
|
||||||
|
f, err := os.Open(filepath.Join(config.BuildDirBase, "build/build.log"))
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("cannot open log: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
io.Copy(w, f)
|
||||||
|
return
|
||||||
|
case buildResult.Good():
|
||||||
|
// good result
|
||||||
|
default:
|
||||||
|
http.Error(w, "build still running", http.StatusTooEarly)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fss := http.FileServer(http.Dir(filepath.Join(config.BuildDirBase, "build/output")))
|
||||||
|
fss.ServeHTTP(w, r)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
62
cmd/osbuild-worker-executor/handler_result_test.go
Normal file
62
cmd/osbuild-worker-executor/handler_result_test.go
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
package main_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestResultTooEarly(t *testing.T) {
|
||||||
|
baseURL, _, _ := runTestServer(t)
|
||||||
|
endpoint := baseURL + "api/v1/result"
|
||||||
|
|
||||||
|
rsp, err := http.Get(endpoint)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, rsp.StatusCode, http.StatusTooEarly)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResultBad(t *testing.T) {
|
||||||
|
baseURL, buildBaseDir, _ := runTestServer(t)
|
||||||
|
endpoint := baseURL + "api/v1/result/disk.img"
|
||||||
|
|
||||||
|
// simulate build failure
|
||||||
|
// todo: make a nice helper method
|
||||||
|
err := os.MkdirAll(filepath.Join(buildBaseDir, "build"), 0755)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = ioutil.WriteFile(filepath.Join(buildBaseDir, "result.bad"), nil, 0644)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = ioutil.WriteFile(filepath.Join(buildBaseDir, "build/build.log"), []byte("failure log"), 0644)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
rsp, err := http.Get(endpoint)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, http.StatusBadRequest, rsp.StatusCode)
|
||||||
|
body, err := ioutil.ReadAll(rsp.Body)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "build failed\nfailure log", string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResultGood(t *testing.T) {
|
||||||
|
baseURL, buildBaseDir, _ := runTestServer(t)
|
||||||
|
endpoint := baseURL + "api/v1/result/disk.img"
|
||||||
|
|
||||||
|
// simulate build failure
|
||||||
|
// todo: make a nice helper method
|
||||||
|
err := os.MkdirAll(filepath.Join(buildBaseDir, "build/output"), 0755)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = ioutil.WriteFile(filepath.Join(buildBaseDir, "result.good"), nil, 0644)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = ioutil.WriteFile(filepath.Join(buildBaseDir, "build/output/disk.img"), []byte("fake-build-result"), 0644)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
rsp, err := http.Get(endpoint)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, http.StatusOK, rsp.StatusCode)
|
||||||
|
body, err := ioutil.ReadAll(rsp.Body)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "fake-build-result", string(body))
|
||||||
|
}
|
||||||
16
cmd/osbuild-worker-executor/handler_root.go
Normal file
16
cmd/osbuild-worker-executor/handler_root.go
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
func handleRoot(logger *logrus.Logger, _ *Config) http.Handler {
|
||||||
|
return http.HandlerFunc(
|
||||||
|
func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// we just return ok here
|
||||||
|
logger.Info("/ handler called")
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
18
cmd/osbuild-worker-executor/handler_root_test.go
Normal file
18
cmd/osbuild-worker-executor/handler_root_test.go
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
package main_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTrivialRootEndpoint(t *testing.T) {
|
||||||
|
baseURL, _, loggerHook := runTestServer(t)
|
||||||
|
|
||||||
|
endpoint := baseURL
|
||||||
|
resp, err := http.Get(endpoint)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, resp.StatusCode, 200)
|
||||||
|
assert.Equal(t, loggerHook.LastEntry().Message, "/ handler called")
|
||||||
|
}
|
||||||
83
cmd/osbuild-worker-executor/main.go
Normal file
83
cmd/osbuild-worker-executor/main.go
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// based on the excellent post
|
||||||
|
// https://grafana.com/blog/2024/02/09/how-i-write-http-services-in-go-after-13-years/
|
||||||
|
|
||||||
|
var logrusNew = logrus.New
|
||||||
|
|
||||||
|
func newServer(logger *logrus.Logger, config *Config) http.Handler {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
addRoutes(mux, logger, config)
|
||||||
|
var handler http.Handler = mux
|
||||||
|
// todo: consider centralize logginer here?
|
||||||
|
//handler = loggingMiddleware(handler)
|
||||||
|
return handler
|
||||||
|
}
|
||||||
|
|
||||||
|
func run(ctx context.Context, args []string, getenv func(string) string) error {
|
||||||
|
ctx, cancel := signal.NotifyContext(ctx, os.Interrupt)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
logger := logrusNew()
|
||||||
|
config, err := newConfigFromCmdline(args)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
srv := newServer(logger, config)
|
||||||
|
httpServer := &http.Server{
|
||||||
|
Addr: net.JoinHostPort(config.Host, config.Port),
|
||||||
|
Handler: srv,
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
logger.Printf("listening on %s\n", httpServer.Addr)
|
||||||
|
if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
|
fmt.Fprintf(os.Stderr, "error listening and serving: %s\n", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// todo: this seems kinda complicated, why a waitgroup and not
|
||||||
|
// do it flat?
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
<-ctx.Done()
|
||||||
|
shutdownCtx := context.Background()
|
||||||
|
shutdownCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if err := httpServer.Shutdown(shutdownCtx); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "error shutting down http server: %s\n", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
// cleanup
|
||||||
|
if err := os.RemoveAll(config.BuildDirBase); err != nil {
|
||||||
|
logger.Errorf("cannot cleanup: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
ctx := context.Background()
|
||||||
|
if err := run(ctx, os.Args, os.Getenv); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "%s\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
74
cmd/osbuild-worker-executor/main_test.go
Normal file
74
cmd/osbuild-worker-executor/main_test.go
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
package main_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
logrusTest "github.com/sirupsen/logrus/hooks/test"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
main "github.com/osbuild/osbuild-composer/cmd/oaas"
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultTimeout = 5 * time.Second
|
||||||
|
|
||||||
|
func waitReady(ctx context.Context, timeout time.Duration, endpoint string) error {
|
||||||
|
client := http.Client{}
|
||||||
|
startTime := time.Now()
|
||||||
|
for {
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if resp.StatusCode == http.StatusOK {
|
||||||
|
resp.Body.Close()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
resp.Body.Close()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
default:
|
||||||
|
if time.Since(startTime) >= timeout {
|
||||||
|
return fmt.Errorf("timeout reached while waiting for endpoint")
|
||||||
|
}
|
||||||
|
// wait a little while between checks
|
||||||
|
time.Sleep(250 * time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runTestServer(t *testing.T) (baseURL, buildBaseDir string, loggerHook *logrusTest.Hook) {
|
||||||
|
host := "localhost"
|
||||||
|
port := "18002"
|
||||||
|
buildBaseDir = t.TempDir()
|
||||||
|
baseURL = fmt.Sprintf("http://%s:%s/", host, port)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
t.Cleanup(cancel)
|
||||||
|
|
||||||
|
loggerHook, restore := main.MockLogger()
|
||||||
|
defer restore()
|
||||||
|
|
||||||
|
args := []string{
|
||||||
|
"-host", host,
|
||||||
|
"-port", port,
|
||||||
|
"-build-path", buildBaseDir,
|
||||||
|
}
|
||||||
|
go main.Run(ctx, args, os.Getenv)
|
||||||
|
|
||||||
|
err := waitReady(ctx, defaultTimeout, baseURL)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
return baseURL, buildBaseDir, loggerHook
|
||||||
|
}
|
||||||
13
cmd/osbuild-worker-executor/routes.go
Normal file
13
cmd/osbuild-worker-executor/routes.go
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
func addRoutes(mux *http.ServeMux, logger *logrus.Logger, config *Config) {
|
||||||
|
mux.Handle("/api/v1/build", handleBuild(logger, config))
|
||||||
|
mux.Handle("/api/v1/result/", http.StripPrefix("/api/v1/result/", handleResult(logger, config)))
|
||||||
|
mux.Handle("/", handleRoot(logger, config))
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue