Move avoidable daemon shell-outs into Go
Reduce the control plane's dependency on helper scripts while keeping the hard Linux integration points in the approved shell-out layer. Replace the bash-driven image build path with a native Go builder that clones and optionally resizes the rootfs, boots a temporary Firecracker VM, provisions the guest over SSH, installs packages and modules, and preserves the package-manifest sidecar. Also replace a few small convenience shell-outs with Go helpers: read process stats from /proc, use os.Truncate for ext4 image growth, add file-clone and normalized-line helpers, drop the sh -c work-disk flattening path, and launch Firecracker via a direct sudo command. Add tests for the new SSH/archive and system helpers, plus a policy test that keeps os/exec imports confined to cli/firecracker/system. Update the docs to describe customize.sh as a manual helper rather than the daemon's image-build backend. Validated with go mod tidy, go test ./..., and make build.
This commit is contained in:
parent
0a0b0b617b
commit
942d242c03
17 changed files with 936 additions and 145 deletions
|
|
@ -37,6 +37,7 @@ type Daemon struct {
|
|||
pid int
|
||||
listener net.Listener
|
||||
vmDNS *vmdns.Server
|
||||
imageBuild func(context.Context, imageBuildSpec) error
|
||||
requestHandler func(context.Context, rpc.Request) rpc.Response
|
||||
}
|
||||
|
||||
|
|
|
|||
275
internal/daemon/imagebuild.go
Normal file
275
internal/daemon/imagebuild.go
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
package daemon
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"banger/internal/firecracker"
|
||||
"banger/internal/guest"
|
||||
"banger/internal/hostnat"
|
||||
"banger/internal/model"
|
||||
"banger/internal/system"
|
||||
)
|
||||
|
||||
type imageBuildSpec struct {
|
||||
ID string
|
||||
Name string
|
||||
BaseRootfs string
|
||||
RootfsPath string
|
||||
BuildLog io.Writer
|
||||
KernelPath string
|
||||
InitrdPath string
|
||||
ModulesDir string
|
||||
PackagesPath string
|
||||
InstallDocker bool
|
||||
Size string
|
||||
}
|
||||
|
||||
type imageBuildVM struct {
|
||||
Name string
|
||||
GuestIP string
|
||||
TapDevice string
|
||||
APISock string
|
||||
PID int
|
||||
}
|
||||
|
||||
func (d *Daemon) runImageBuild(ctx context.Context, spec imageBuildSpec) error {
|
||||
if d.imageBuild != nil {
|
||||
return d.imageBuild(ctx, spec)
|
||||
}
|
||||
return d.runImageBuildNative(ctx, spec)
|
||||
}
|
||||
|
||||
func (d *Daemon) runImageBuildNative(ctx context.Context, spec imageBuildSpec) (err error) {
|
||||
packages, err := system.ReadNormalizedLines(spec.PackagesPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := system.CopyFilePreferClone(spec.BaseRootfs, spec.RootfsPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if spec.Size != "" {
|
||||
if err := resizeRootfs(spec.BaseRootfs, spec.RootfsPath, spec.Size); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
vm, cleanup, err := d.startImageBuildVM(ctx, spec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
cleanupErr := cleanup(context.Background())
|
||||
if cleanupErr != nil {
|
||||
err = errors.Join(err, cleanupErr)
|
||||
}
|
||||
}()
|
||||
|
||||
sshAddress := vm.GuestIP + ":22"
|
||||
if _, err := fmt.Fprintf(spec.BuildLog, "[image.build] waiting for ssh on %s\n", sshAddress); err != nil {
|
||||
return err
|
||||
}
|
||||
waitCtx, cancel := context.WithTimeout(ctx, 60*time.Second)
|
||||
defer cancel()
|
||||
if err := guest.WaitForSSH(waitCtx, sshAddress, d.config.SSHKeyPath, time.Second); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client, err := guest.Dial(ctx, sshAddress, d.config.SSHKeyPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
if err := writeBuildLog(spec.BuildLog, "configuring guest"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := client.RunScript(ctx, buildProvisionScript(vm.Name, d.config.DefaultDNS, packages, spec.InstallDocker), spec.BuildLog); err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.TrimSpace(spec.ModulesDir) != "" {
|
||||
if err := writeBuildLog(spec.BuildLog, "copying kernel modules"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := client.StreamTar(ctx, spec.ModulesDir, buildModulesCommand(filepath.Base(spec.ModulesDir)), spec.BuildLog); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := writeBuildLog(spec.BuildLog, "shutting down guest"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := client.RunScript(ctx, "set -e\nsync\n", spec.BuildLog); err != nil {
|
||||
return err
|
||||
}
|
||||
return d.shutdownImageBuildVM(ctx, vm)
|
||||
}
|
||||
|
||||
func resizeRootfs(baseRootfs, rootfsPath, sizeSpec string) error {
|
||||
sizeBytes, err := model.ParseSize(sizeSpec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
info, err := os.Stat(baseRootfs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if sizeBytes < info.Size() {
|
||||
return fmt.Errorf("size must be >= base image size")
|
||||
}
|
||||
return system.ResizeExt4Image(context.Background(), system.NewRunner(), rootfsPath, sizeBytes)
|
||||
}
|
||||
|
||||
func (d *Daemon) startImageBuildVM(ctx context.Context, spec imageBuildSpec) (imageBuildVM, func(context.Context) error, error) {
|
||||
if err := d.ensureBridge(ctx); err != nil {
|
||||
return imageBuildVM{}, nil, err
|
||||
}
|
||||
if err := d.ensureSocketDir(); err != nil {
|
||||
return imageBuildVM{}, nil, err
|
||||
}
|
||||
fcPath, err := d.firecrackerBinary()
|
||||
if err != nil {
|
||||
return imageBuildVM{}, nil, err
|
||||
}
|
||||
|
||||
shortID := system.ShortID(spec.ID)
|
||||
guestIP, err := d.store.NextGuestIP(ctx, bridgePrefix(d.config.BridgeIP))
|
||||
if err != nil {
|
||||
return imageBuildVM{}, nil, err
|
||||
}
|
||||
vm := imageBuildVM{
|
||||
Name: "image-build-" + shortID,
|
||||
GuestIP: guestIP,
|
||||
TapDevice: "tap-img-" + shortID,
|
||||
APISock: filepath.Join(d.layout.RuntimeDir, "img-"+shortID+".sock"),
|
||||
}
|
||||
if err := os.RemoveAll(vm.APISock); err != nil && !os.IsNotExist(err) {
|
||||
return imageBuildVM{}, nil, err
|
||||
}
|
||||
if err := d.createTap(ctx, vm.TapDevice); err != nil {
|
||||
return imageBuildVM{}, nil, err
|
||||
}
|
||||
if err := hostnat.Ensure(ctx, d.runner, vm.GuestIP, vm.TapDevice, true); err != nil {
|
||||
_, _ = d.runner.RunSudo(ctx, "ip", "link", "del", vm.TapDevice)
|
||||
return imageBuildVM{}, nil, err
|
||||
}
|
||||
|
||||
firecrackerCtx := context.Background()
|
||||
machine, err := firecracker.NewMachine(firecrackerCtx, firecracker.MachineConfig{
|
||||
BinaryPath: fcPath,
|
||||
VMID: spec.ID,
|
||||
SocketPath: vm.APISock,
|
||||
LogPath: spec.RootfsPath + ".firecracker.log",
|
||||
MetricsPath: filepath.Join(filepath.Dir(spec.RootfsPath), "metrics.json"),
|
||||
KernelImagePath: spec.KernelPath,
|
||||
InitrdPath: spec.InitrdPath,
|
||||
KernelArgs: system.BuildBootArgs(vm.Name, vm.GuestIP, d.config.BridgeIP, d.config.DefaultDNS),
|
||||
RootDrivePath: spec.RootfsPath,
|
||||
TapDevice: vm.TapDevice,
|
||||
VCPUCount: model.DefaultVCPUCount,
|
||||
MemoryMiB: model.DefaultMemoryMiB,
|
||||
Logger: d.logger,
|
||||
})
|
||||
if err != nil {
|
||||
_ = hostnat.Ensure(ctx, d.runner, vm.GuestIP, vm.TapDevice, false)
|
||||
_, _ = d.runner.RunSudo(ctx, "ip", "link", "del", vm.TapDevice)
|
||||
return imageBuildVM{}, nil, err
|
||||
}
|
||||
if err := machine.Start(firecrackerCtx); err != nil {
|
||||
_ = hostnat.Ensure(ctx, d.runner, vm.GuestIP, vm.TapDevice, false)
|
||||
_, _ = d.runner.RunSudo(ctx, "ip", "link", "del", vm.TapDevice)
|
||||
return imageBuildVM{}, nil, err
|
||||
}
|
||||
vm.PID = d.resolveFirecrackerPID(firecrackerCtx, machine, vm.APISock)
|
||||
if err := d.ensureSocketAccess(ctx, vm.APISock); err != nil {
|
||||
_ = d.killVMProcess(context.Background(), vm.PID)
|
||||
_ = hostnat.Ensure(ctx, d.runner, vm.GuestIP, vm.TapDevice, false)
|
||||
_, _ = d.runner.RunSudo(ctx, "ip", "link", "del", vm.TapDevice)
|
||||
return imageBuildVM{}, nil, err
|
||||
}
|
||||
|
||||
cleanup := func(cleanupCtx context.Context) error {
|
||||
if vm.PID > 0 && system.ProcessRunning(vm.PID, vm.APISock) {
|
||||
_ = d.killVMProcess(cleanupCtx, vm.PID)
|
||||
_ = d.waitForExit(cleanupCtx, vm.PID, vm.APISock, 10*time.Second)
|
||||
}
|
||||
_ = hostnat.Ensure(cleanupCtx, d.runner, vm.GuestIP, vm.TapDevice, false)
|
||||
if vm.TapDevice != "" {
|
||||
_, _ = d.runner.RunSudo(cleanupCtx, "ip", "link", "del", vm.TapDevice)
|
||||
}
|
||||
if vm.APISock != "" {
|
||||
_ = os.Remove(vm.APISock)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return vm, cleanup, nil
|
||||
}
|
||||
|
||||
func (d *Daemon) shutdownImageBuildVM(ctx context.Context, vm imageBuildVM) error {
|
||||
buildVM := model.VMRecord{Runtime: model.VMRuntime{APISockPath: vm.APISock}}
|
||||
if err := d.sendCtrlAltDel(ctx, buildVM); err != nil {
|
||||
return err
|
||||
}
|
||||
return d.waitForExit(ctx, vm.PID, vm.APISock, 15*time.Second)
|
||||
}
|
||||
|
||||
func buildProvisionScript(vmName, dnsServer string, packages []string, installDocker bool) string {
|
||||
var script bytes.Buffer
|
||||
script.WriteString("set -euo pipefail\n")
|
||||
fmt.Fprintf(&script, "printf 'nameserver %%s\\n' %s > /etc/resolv.conf\n", shellQuote(dnsServer))
|
||||
fmt.Fprintf(&script, "printf '%%s\\n' %s > /etc/hostname\n", shellQuote(vmName))
|
||||
fmt.Fprintf(&script, "printf '127.0.0.1 localhost\\n127.0.1.1 %%s\\n' %s > /etc/hosts\n", shellQuote(vmName))
|
||||
script.WriteString("touch /etc/fstab\n")
|
||||
script.WriteString("sed -i '\\|^/dev/vdb[[:space:]]\\+/home[[:space:]]|d; \\|^/dev/vdc[[:space:]]\\+/var[[:space:]]|d' /etc/fstab\n")
|
||||
script.WriteString("if ! grep -q '^tmpfs /run ' /etc/fstab; then echo 'tmpfs /run tmpfs defaults,nodev,nosuid,mode=0755 0 0' >> /etc/fstab; fi\n")
|
||||
script.WriteString("if ! grep -q '^tmpfs /tmp ' /etc/fstab; then echo 'tmpfs /tmp tmpfs defaults,nodev,nosuid,mode=1777 0 0' >> /etc/fstab; fi\n")
|
||||
script.WriteString("apt-get update\n")
|
||||
script.WriteString("DEBIAN_FRONTEND=noninteractive apt-get -y upgrade\n")
|
||||
fmt.Fprintf(&script, "PACKAGES=%s\n", shellArray(packages))
|
||||
script.WriteString("DEBIAN_FRONTEND=noninteractive apt-get -y install \"${PACKAGES[@]}\"\n")
|
||||
if installDocker {
|
||||
script.WriteString("DEBIAN_FRONTEND=noninteractive apt-get -y remove containerd || true\n")
|
||||
script.WriteString("if ! DEBIAN_FRONTEND=noninteractive apt-get -y install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin; then\n")
|
||||
script.WriteString(" DEBIAN_FRONTEND=noninteractive apt-get -y install docker.io\n")
|
||||
script.WriteString("fi\n")
|
||||
script.WriteString("if command -v systemctl >/dev/null 2>&1; then systemctl enable --now docker || true; fi\n")
|
||||
}
|
||||
script.WriteString("git config --system init.defaultBranch main\n")
|
||||
return script.String()
|
||||
}
|
||||
|
||||
func buildModulesCommand(modulesBase string) string {
|
||||
return fmt.Sprintf("bash -se <<'EOF'\nset -euo pipefail\nmkdir -p /lib/modules\ntar -C /lib/modules -xf -\ndepmod -a %s\nmkdir -p /etc/modules-load.d\nprintf 'nf_tables\\nnft_chain_nat\\nveth\\nbr_netfilter\\noverlay\\n' > /etc/modules-load.d/docker-netfilter.conf\nmkdir -p /etc/sysctl.d\ncat > /etc/sysctl.d/99-docker.conf <<'SYSCTL'\nnet.bridge.bridge-nf-call-iptables = 1\nnet.bridge.bridge-nf-call-ip6tables = 1\nnet.ipv4.ip_forward = 1\nSYSCTL\nsysctl --system >/dev/null 2>&1 || true\nEOF", shellQuote(modulesBase))
|
||||
}
|
||||
|
||||
func shellArray(values []string) string {
|
||||
quoted := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
quoted = append(quoted, shellQuote(value))
|
||||
}
|
||||
return "(" + strings.Join(quoted, " ") + ")"
|
||||
}
|
||||
|
||||
func shellQuote(value string) string {
|
||||
return "'" + strings.ReplaceAll(value, "'", `'"'"'`) + "'"
|
||||
}
|
||||
|
||||
func writeBuildLog(w io.Writer, message string) error {
|
||||
if w == nil {
|
||||
return nil
|
||||
}
|
||||
_, err := fmt.Fprintf(w, "[image.build] %s\n", message)
|
||||
return err
|
||||
}
|
||||
|
||||
func packagesHash(lines []string) string {
|
||||
sum := sha256.Sum256([]byte(strings.Join(lines, "\n") + "\n"))
|
||||
return fmt.Sprintf("%x", sum)
|
||||
}
|
||||
|
|
@ -4,12 +4,12 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
||||
"banger/internal/api"
|
||||
"banger/internal/model"
|
||||
"banger/internal/paths"
|
||||
"banger/internal/system"
|
||||
)
|
||||
|
||||
func (d *Daemon) BuildImage(ctx context.Context, params api.ImageBuildParams) (image model.Image, err error) {
|
||||
|
|
@ -60,56 +60,40 @@ func (d *Daemon) BuildImage(ctx context.Context, params api.ImageBuildParams) (i
|
|||
}
|
||||
defer logFile.Close()
|
||||
rootfsPath := filepath.Join(artifactDir, "rootfs.ext4")
|
||||
script := d.config.CustomizeScript
|
||||
if script == "" {
|
||||
return model.Image{}, fmt.Errorf("customize script not configured; %s", paths.RuntimeBundleHint())
|
||||
}
|
||||
if _, err := os.Stat(script); err != nil {
|
||||
return model.Image{}, fmt.Errorf("customize.sh not found at %s; %s", script, paths.RuntimeBundleHint())
|
||||
}
|
||||
args := []string{script, baseRootfs, "--out", rootfsPath}
|
||||
if params.Size != "" {
|
||||
args = append(args, "--size", params.Size)
|
||||
}
|
||||
kernelPath := params.KernelPath
|
||||
if kernelPath == "" {
|
||||
kernelPath = d.config.DefaultKernel
|
||||
}
|
||||
if kernelPath != "" {
|
||||
args = append(args, "--kernel", kernelPath)
|
||||
}
|
||||
initrdPath := params.InitrdPath
|
||||
if initrdPath == "" {
|
||||
initrdPath = d.config.DefaultInitrd
|
||||
}
|
||||
if initrdPath != "" {
|
||||
args = append(args, "--initrd", initrdPath)
|
||||
}
|
||||
modulesDir := params.ModulesDir
|
||||
if modulesDir == "" {
|
||||
modulesDir = d.config.DefaultModulesDir
|
||||
}
|
||||
if modulesDir != "" {
|
||||
args = append(args, "--modules", modulesDir)
|
||||
}
|
||||
if params.Docker {
|
||||
args = append(args, "--docker")
|
||||
}
|
||||
if err := d.validateImageBuildPrereqs(ctx, baseRootfs, kernelPath, initrdPath, modulesDir); err != nil {
|
||||
if err := d.validateImageBuildPrereqs(ctx, baseRootfs, kernelPath, initrdPath, modulesDir, params.Size); err != nil {
|
||||
return model.Image{}, err
|
||||
}
|
||||
op.stage("launch_helper", "script", script, "build_log_path", buildLogPath, "artifact_dir", artifactDir)
|
||||
cmd := exec.CommandContext(ctx, "bash", args...)
|
||||
cmd.Stdout = logFile
|
||||
cmd.Stderr = logFile
|
||||
cmd.Stdin = nil
|
||||
cmd.Dir = d.layout.StateDir
|
||||
cmd.Env = append(
|
||||
os.Environ(),
|
||||
"BANGER_RUNTIME_DIR="+d.config.RuntimeDir,
|
||||
"BANGER_STATE_DIR="+filepath.Join(d.layout.StateDir, "image-build"),
|
||||
)
|
||||
if err := cmd.Run(); err != nil {
|
||||
spec := imageBuildSpec{
|
||||
ID: id,
|
||||
Name: name,
|
||||
BaseRootfs: baseRootfs,
|
||||
RootfsPath: rootfsPath,
|
||||
BuildLog: logFile,
|
||||
KernelPath: kernelPath,
|
||||
InitrdPath: initrdPath,
|
||||
ModulesDir: modulesDir,
|
||||
PackagesPath: d.config.DefaultPackagesFile,
|
||||
InstallDocker: params.Docker,
|
||||
Size: params.Size,
|
||||
}
|
||||
op.stage("launch_builder", "build_log_path", buildLogPath, "artifact_dir", artifactDir)
|
||||
if err := d.runImageBuild(ctx, spec); err != nil {
|
||||
_ = os.RemoveAll(artifactDir)
|
||||
return model.Image{}, err
|
||||
}
|
||||
if err := writePackagesMetadata(rootfsPath, d.config.DefaultPackagesFile); err != nil {
|
||||
_ = os.RemoveAll(artifactDir)
|
||||
return model.Image{}, err
|
||||
}
|
||||
|
|
@ -138,6 +122,18 @@ func (d *Daemon) BuildImage(ctx context.Context, params api.ImageBuildParams) (i
|
|||
return image, nil
|
||||
}
|
||||
|
||||
func writePackagesMetadata(rootfsPath, packagesPath string) error {
|
||||
if rootfsPath == "" || packagesPath == "" {
|
||||
return nil
|
||||
}
|
||||
lines, err := system.ReadNormalizedLines(packagesPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
metadataPath := rootfsPath + ".packages.sha256"
|
||||
return os.WriteFile(metadataPath, []byte(packagesHash(lines)+"\n"), 0o644)
|
||||
}
|
||||
|
||||
func (d *Daemon) DeleteImage(ctx context.Context, idOrName string) (model.Image, error) {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
|
@ -129,31 +128,30 @@ func TestBuildImagePreservesBuildLogOnFailure(t *testing.T) {
|
|||
}
|
||||
|
||||
binDir := t.TempDir()
|
||||
for _, name := range []string{"sudo", "ip", "curl", "ssh", "jq", "sha256sum", "e2fsck", "resize2fs"} {
|
||||
for _, name := range []string{"sudo", "ip", "pgrep", "chown", "chmod", "kill", "iptables", "sysctl", "e2fsck", "resize2fs"} {
|
||||
writeFakeExecutable(t, filepath.Join(binDir, name))
|
||||
}
|
||||
bashPath, err := exec.LookPath("bash")
|
||||
if err != nil {
|
||||
t.Fatalf("lookpath bash: %v", err)
|
||||
}
|
||||
bashWrapper := filepath.Join(binDir, "bash")
|
||||
if err := os.WriteFile(bashWrapper, []byte(fmt.Sprintf("#!/bin/sh\nexec %q \"$@\"\n", bashPath)), 0o755); err != nil {
|
||||
t.Fatalf("write bash wrapper: %v", err)
|
||||
}
|
||||
t.Setenv("PATH", binDir)
|
||||
|
||||
script := filepath.Join(t.TempDir(), "customize.sh")
|
||||
scriptBody := "#!/bin/sh\necho helper-stdout\necho helper-stderr >&2\nexit 17\n"
|
||||
if err := os.WriteFile(script, []byte(scriptBody), 0o755); err != nil {
|
||||
t.Fatalf("write customize script: %v", err)
|
||||
}
|
||||
baseRootfs := filepath.Join(t.TempDir(), "base.ext4")
|
||||
kernelPath := filepath.Join(t.TempDir(), "vmlinux")
|
||||
for _, path := range []string{baseRootfs, kernelPath} {
|
||||
packagesPath := filepath.Join(t.TempDir(), "packages.apt")
|
||||
sshKeyPath := filepath.Join(t.TempDir(), "id_ed25519")
|
||||
firecrackerBin := filepath.Join(t.TempDir(), "firecracker")
|
||||
for _, path := range []string{baseRootfs, kernelPath, packagesPath, sshKeyPath} {
|
||||
if err := os.WriteFile(path, []byte("artifact"), 0o644); err != nil {
|
||||
t.Fatalf("write %s: %v", path, err)
|
||||
}
|
||||
}
|
||||
if err := os.WriteFile(firecrackerBin, []byte("#!/bin/sh\nexit 0\n"), 0o755); err != nil {
|
||||
t.Fatalf("write %s: %v", firecrackerBin, err)
|
||||
}
|
||||
runner := &scriptedRunner{
|
||||
t: t,
|
||||
steps: []runnerStep{
|
||||
{call: runnerCall{name: "ip", args: []string{"route", "show", "default"}}, out: []byte("default via 192.0.2.1 dev eth0\n")},
|
||||
},
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
logger, _, err := newDaemonLogger(&buf, "info")
|
||||
|
|
@ -166,12 +164,24 @@ func TestBuildImagePreservesBuildLogOnFailure(t *testing.T) {
|
|||
ImagesDir: imagesDir,
|
||||
},
|
||||
config: model.DaemonConfig{
|
||||
RuntimeDir: t.TempDir(),
|
||||
CustomizeScript: script,
|
||||
DefaultImageName: "default",
|
||||
RuntimeDir: t.TempDir(),
|
||||
DefaultImageName: "default",
|
||||
DefaultPackagesFile: packagesPath,
|
||||
SSHKeyPath: sshKeyPath,
|
||||
FirecrackerBin: firecrackerBin,
|
||||
},
|
||||
store: store,
|
||||
runner: runner,
|
||||
logger: logger,
|
||||
imageBuild: func(ctx context.Context, spec imageBuildSpec) error {
|
||||
if _, err := fmt.Fprintln(spec.BuildLog, "builder-stdout"); err != nil {
|
||||
return err
|
||||
}
|
||||
if spec.BaseRootfs != baseRootfs || spec.KernelPath != kernelPath || spec.PackagesPath != packagesPath {
|
||||
t.Fatalf("unexpected image build spec: %+v", spec)
|
||||
}
|
||||
return errors.New("builder failed")
|
||||
},
|
||||
}
|
||||
|
||||
_, err = d.BuildImage(ctx, api.ImageBuildParams{
|
||||
|
|
@ -194,13 +204,14 @@ func TestBuildImagePreservesBuildLogOnFailure(t *testing.T) {
|
|||
if readErr != nil {
|
||||
t.Fatalf("read build log: %v", readErr)
|
||||
}
|
||||
if !strings.Contains(string(logData), "helper-stdout") || !strings.Contains(string(logData), "helper-stderr") {
|
||||
t.Fatalf("build log = %q, want helper stdout/stderr", string(logData))
|
||||
if !strings.Contains(string(logData), "builder-stdout") {
|
||||
t.Fatalf("build log = %q, want builder output", string(logData))
|
||||
}
|
||||
runner.assertExhausted()
|
||||
|
||||
entries := parseLogEntries(t, buf.Bytes())
|
||||
if !hasLogEntry(entries, map[string]string{"msg": "operation stage", "operation": "image.build", "stage": "launch_helper"}) {
|
||||
t.Fatalf("expected launch_helper log, got %v", entries)
|
||||
if !hasLogEntry(entries, map[string]string{"msg": "operation stage", "operation": "image.build", "stage": "launch_builder"}) {
|
||||
t.Fatalf("expected launch_builder log, got %v", entries)
|
||||
}
|
||||
if !strings.Contains(buf.String(), buildLogs[0]) {
|
||||
t.Fatalf("daemon logs = %q, want build log path %s", buf.String(), buildLogs[0])
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ func (d *Daemon) validateStartPrereqs(ctx context.Context, vm model.VMRecord, im
|
|||
checks := system.NewPreflight()
|
||||
hint := paths.RuntimeBundleHint()
|
||||
|
||||
for _, command := range []string{"sudo", "ip", "dmsetup", "losetup", "blockdev", "truncate", "pgrep", "ps", "chown", "chmod", "kill", "e2cp", "e2rm", "debugfs"} {
|
||||
for _, command := range []string{"sudo", "ip", "dmsetup", "losetup", "blockdev", "truncate", "pgrep", "chown", "chmod", "kill", "e2cp", "e2rm", "debugfs"} {
|
||||
checks.RequireCommand(command, toolHint(command))
|
||||
}
|
||||
checks.RequireExecutable(d.config.FirecrackerBin, "firecracker binary", hint)
|
||||
|
|
@ -33,22 +33,34 @@ func (d *Daemon) validateStartPrereqs(ctx context.Context, vm model.VMRecord, im
|
|||
return checks.Err("vm start preflight failed")
|
||||
}
|
||||
|
||||
func (d *Daemon) validateImageBuildPrereqs(ctx context.Context, baseRootfs, kernelPath, initrdPath, modulesDir string) error {
|
||||
func (d *Daemon) validateImageBuildPrereqs(ctx context.Context, baseRootfs, kernelPath, initrdPath, modulesDir, sizeSpec string) error {
|
||||
checks := system.NewPreflight()
|
||||
hint := paths.RuntimeBundleHint()
|
||||
|
||||
for _, command := range []string{"bash", "sudo", "ip", "curl", "ssh", "jq", "sha256sum", "e2fsck", "resize2fs"} {
|
||||
for _, command := range []string{"sudo", "ip", "pgrep", "chown", "chmod", "kill"} {
|
||||
checks.RequireCommand(command, toolHint(command))
|
||||
}
|
||||
checks.RequireExecutable(d.config.CustomizeScript, "customize.sh helper", hint)
|
||||
checks.RequireExecutable(d.config.FirecrackerBin, "firecracker binary", hint)
|
||||
checks.RequireFile(d.config.SSHKeyPath, "ssh private key", `set "ssh_key_path" or refresh the runtime bundle`)
|
||||
checks.RequireFile(baseRootfs, "base rootfs image", `pass --base-rootfs or set "default_base_rootfs"`)
|
||||
checks.RequireFile(kernelPath, "kernel image", `pass --kernel or set "default_kernel"`)
|
||||
checks.RequireFile(d.config.DefaultPackagesFile, "package manifest", `set "default_packages_file" or refresh the runtime bundle`)
|
||||
if strings.TrimSpace(initrdPath) != "" {
|
||||
checks.RequireFile(initrdPath, "initrd image", `pass --initrd or set "default_initrd"`)
|
||||
}
|
||||
if strings.TrimSpace(modulesDir) != "" {
|
||||
checks.RequireDir(modulesDir, "modules directory", `pass --modules or set "default_modules_dir"`)
|
||||
}
|
||||
if strings.TrimSpace(d.config.DefaultPackagesFile) != "" {
|
||||
if _, err := system.ReadNormalizedLines(d.config.DefaultPackagesFile); err != nil {
|
||||
checks.Addf("package manifest at %s is invalid: %v", d.config.DefaultPackagesFile, err)
|
||||
}
|
||||
}
|
||||
if strings.TrimSpace(sizeSpec) != "" {
|
||||
checks.RequireCommand("e2fsck", toolHint("e2fsck"))
|
||||
checks.RequireCommand("resize2fs", toolHint("resize2fs"))
|
||||
}
|
||||
d.addNATPrereqs(ctx, checks)
|
||||
return checks.Err("image build preflight failed")
|
||||
}
|
||||
|
||||
|
|
@ -63,7 +75,11 @@ func (d *Daemon) validateWorkDiskResizePrereqs() error {
|
|||
func (d *Daemon) addNATPrereqs(ctx context.Context, checks *system.Preflight) {
|
||||
checks.RequireCommand("iptables", toolHint("iptables"))
|
||||
checks.RequireCommand("sysctl", toolHint("sysctl"))
|
||||
out, err := d.runner.Run(ctx, "ip", "route", "show", "default")
|
||||
runner := d.runner
|
||||
if runner == nil {
|
||||
runner = system.NewRunner()
|
||||
}
|
||||
out, err := runner.Run(ctx, "ip", "route", "show", "default")
|
||||
if err != nil {
|
||||
checks.Addf("failed to inspect the default route for NAT: %v", err)
|
||||
return
|
||||
|
|
@ -83,7 +99,7 @@ func toolHint(command string) string {
|
|||
return "install util-linux"
|
||||
case "dmsetup":
|
||||
return "install device-mapper"
|
||||
case "pgrep", "ps", "kill":
|
||||
case "pgrep", "kill":
|
||||
return "install procps"
|
||||
case "chown", "chmod", "cp", "truncate":
|
||||
return "install coreutils"
|
||||
|
|
@ -91,16 +107,6 @@ func toolHint(command string) string {
|
|||
return "install e2fsprogs"
|
||||
case "e2cp", "e2rm":
|
||||
return "install e2tools"
|
||||
case "curl":
|
||||
return "install curl"
|
||||
case "jq":
|
||||
return "install jq"
|
||||
case "sha256sum":
|
||||
return "install coreutils"
|
||||
case "ssh":
|
||||
return "install openssh-client"
|
||||
case "bash":
|
||||
return "install bash"
|
||||
case "sudo":
|
||||
return "install sudo"
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -703,14 +703,7 @@ func (d *Daemon) flattenNestedWorkHome(ctx context.Context, workMount string) er
|
|||
if !exists(nestedHome) {
|
||||
return nil
|
||||
}
|
||||
script := `set -e
|
||||
src="$1"
|
||||
dst="$2"
|
||||
for path in "$src"/.[!.]* "$src"/..?* "$src"/*; do
|
||||
[ -e "$path" ] || continue
|
||||
cp -a "$path" "$dst"/
|
||||
done`
|
||||
if _, err := d.runner.RunSudo(ctx, "sh", "-c", script, "sh", nestedHome, workMount); err != nil {
|
||||
if err := system.CopyDirContents(ctx, d.runner, nestedHome, workMount, true); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := d.runner.RunSudo(ctx, "rm", "-rf", nestedHome)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue