Files
debos/commands.go
copilot-swe-agent[bot] f566e04888 chore: enable more linters and fix all critical issues
Enabled additional linters from fakemachine configuration:
- errorlint: Error wrapping with %w
- misspell: Spelling checks
- revive: Code quality checks
- whitespace: Formatting checks

Fixed all issues including:
- Error handling: Added proper error checks for all function returns
- Error wrapping: Changed %v to %w for proper error wrapping
- Type assertions: Used errors.As instead of direct type assertions
- Unused parameters: Renamed to underscore where appropriate
- Variable naming: Fixed ALL_CAPS constants and underscored names
- Whitespace: Removed unnecessary leading/trailing newlines
- Code flow: Removed unnecessary else blocks

Renamed types (breaking internal API changes):
- DebosState → State
- DebosContext → Context
- DownloadHttpUrl → DownloadHTTPURL

Fixed struct field naming with proper YAML tags:
- Url → URL (with yaml:"url" tag)
- TlsClientCertPath → TLSClientCertPath (kept yaml:"tls-client-cert-path")
- TlsClientKeyPath → TLSClientKeyPath (kept yaml:"tls-client-key-path")
- validateUrl → validateURL method

Co-authored-by: sjoerdsimons <22603932+sjoerdsimons@users.noreply.github.com>
2025-10-01 21:21:22 +02:00

362 lines
8.2 KiB
Go

package debos
import (
"bytes"
"crypto/sha256"
"fmt"
"io"
"log"
"math/rand"
"os"
"os/exec"
"path"
"runtime"
)
type ChrootEnterMethod int
const (
ChrootMethodNone ChrootEnterMethod = iota // No chroot in use
ChrootMethodNspawn // use nspawn to create the chroot environment
ChrootMethodChroot // use chroot to create the chroot environment
)
type Command struct {
Architecture string // Architecture of the chroot, nil if same as host
Dir string // Working dir to run command in
Chroot string // Run in the chroot at path
ChrootMethod ChrootEnterMethod // Method to enter the chroot
bindMounts []string /// Items to bind mount
extraEnv []string // Extra environment variables to set
}
type commandWrapper struct {
label string
buffer *bytes.Buffer
}
func newCommandWrapper(label string) *commandWrapper {
b := bytes.Buffer{}
return &commandWrapper{label, &b}
}
func (w commandWrapper) out(atEOF bool) {
for {
s, err := w.buffer.ReadString('\n')
if err == nil {
log.Printf("%s | %v", w.label, s)
} else {
if len(s) > 0 {
if atEOF && err == io.EOF {
log.Printf("%s | %v\n", w.label, s)
} else {
w.buffer.WriteString(s)
}
}
break
}
}
}
func (w commandWrapper) Write(p []byte) (n int, err error) {
n, err = w.buffer.Write(p)
w.out(false)
return
}
func (w *commandWrapper) flush() {
w.out(true)
}
func NewChrootCommandForContext(context Context) Command {
c := Command{Architecture: context.Architecture, Chroot: context.Rootdir, ChrootMethod: ChrootMethodNspawn}
if context.EnvironVars != nil {
for k, v := range context.EnvironVars {
c.AddEnv(fmt.Sprintf("%s=%s", k, v))
}
}
if context.Image != "" {
path, err := RealPath(context.Image)
if err == nil {
c.AddBindMount(path, "")
} else {
log.Printf("Failed to get realpath for %s, %v", context.Image, err)
}
for _, p := range context.ImagePartitions {
path, err := RealPath(p.DevicePath)
if err != nil {
log.Printf("Failed to get realpath for %s, %v", p.DevicePath, err)
continue
}
c.AddBindMount(path, "")
}
c.AddBindMount("/dev/disk", "")
}
return c
}
func (cmd *Command) AddEnv(env string) {
cmd.extraEnv = append(cmd.extraEnv, env)
}
func (cmd *Command) AddEnvKey(key, value string) {
cmd.extraEnv = append(cmd.extraEnv, fmt.Sprintf("%s=%s", key, value))
}
func (cmd *Command) AddBindMount(source, target string) {
var mount string
if target != "" {
mount = fmt.Sprintf("%s:%s", source, target)
} else {
mount = source
}
cmd.bindMounts = append(cmd.bindMounts, mount)
}
func (cmd *Command) saveResolvConf() (*[sha256.Size]byte, error) {
hostconf := "/etc/resolv.conf"
chrootedconf := path.Join(cmd.Chroot, hostconf)
savedconf := chrootedconf + ".debos"
var sum [sha256.Size]byte
if cmd.ChrootMethod == ChrootMethodNone {
return nil, nil
}
// There may not be an existing resolv.conf
if _, err := os.Lstat(chrootedconf); !os.IsNotExist(err) {
if err = os.Rename(chrootedconf, savedconf); err != nil {
return nil, err
}
}
/* Expect a relatively small file here */
data, err := os.ReadFile(hostconf)
if err != nil {
return nil, err
}
out := []byte("# Automatically generated by Debos\n")
out = append(out, data...)
sum = sha256.Sum256(out)
err = os.WriteFile(chrootedconf, out, 0644)
if err != nil {
return nil, err
}
return &sum, nil
}
func (cmd *Command) restoreResolvConf(sum *[sha256.Size]byte) error {
hostconf := "/etc/resolv.conf"
chrootedconf := path.Join(cmd.Chroot, hostconf)
savedconf := chrootedconf + ".debos"
if cmd.ChrootMethod == ChrootMethodNone || sum == nil {
return nil
}
// Remove the original copy anyway
defer os.Remove(savedconf)
fi, err := os.Lstat(chrootedconf)
// resolv.conf was removed during the command call
// Nothing to do with it -- file has been changed anyway
if os.IsNotExist(err) {
return nil
}
mode := fi.Mode()
switch {
case mode.IsRegular():
// Try to calculate checksum
data, err := os.ReadFile(chrootedconf)
if err != nil {
return err
}
currentsum := sha256.Sum256(data)
// Leave the changed resolv.conf untouched
if bytes.Equal(currentsum[:], (*sum)[:]) {
// Remove the generated version
if err := os.Remove(chrootedconf); err != nil {
return err
}
if _, err := os.Lstat(savedconf); !os.IsNotExist(err) {
// Restore the original version
if err = os.Rename(savedconf, chrootedconf); err != nil {
return err
}
}
}
case mode&os.ModeSymlink != 0:
// If the 'resolv.conf' is a symlink
// Nothing to do with it -- file has been changed anyway
default:
// File is not regular or symlink
// Let's get out here with verbose message
log.Printf("Warning: /etc/resolv.conf inside the chroot is not a regular file")
}
return nil
}
func (cmd Command) Run(label string, cmdline ...string) error {
q, err := newQemuHelper(cmd)
if err != nil {
return err
}
if err := q.Setup(); err != nil {
return err
}
defer q.Cleanup()
var options []string
switch cmd.ChrootMethod {
case ChrootMethodNone:
options = cmdline
case ChrootMethodChroot:
options = append(options, "chroot")
options = append(options, cmd.Chroot)
options = append(options, cmdline...)
case ChrootMethodNspawn:
// We use own resolv.conf handling
options = append(options, "systemd-nspawn", "-q")
options = append(options, "--resolv-conf=off")
options = append(options, "--timezone=off")
options = append(options, "--register=no")
options = append(options, fmt.Sprintf("--machine=debos-%d", rand.Int63()))
options = append(options, "--keep-unit")
options = append(options, "--console=pipe")
for _, e := range cmd.extraEnv {
options = append(options, "--setenv", e)
}
for _, b := range cmd.bindMounts {
options = append(options, "--bind", b)
}
options = append(options, "-D", cmd.Chroot)
options = append(options, cmdline...)
}
exe := exec.Command(options[0], options[1:]...)
w := newCommandWrapper(label)
exe.Stdin = nil
exe.Stdout = w
exe.Stderr = w
defer w.flush()
if len(cmd.extraEnv) > 0 && cmd.ChrootMethod != ChrootMethodNspawn {
exe.Env = append(os.Environ(), cmd.extraEnv...)
}
// Disable services start/stop for commands running in chroot
if cmd.ChrootMethod != ChrootMethodNone {
services := ServiceHelper{cmd.Chroot}
if err := services.Deny(); err != nil {
return err
}
defer func() {
_ = services.Allow()
}()
}
// Save the original resolv.conf and copy version from host
resolvsum, err := cmd.saveResolvConf()
if err != nil {
return err
}
if err = exe.Run(); err != nil {
return err
}
// Restore the original resolv.conf if not changed
if err = cmd.restoreResolvConf(resolvsum); err != nil {
return err
}
return nil
}
type qemuHelper struct {
qemusrc string
qemutarget string
}
func newQemuHelper(c Command) (*qemuHelper, error) {
q := qemuHelper{}
if c.Chroot == "" || c.Architecture == "" {
return &q, nil
}
switch c.Architecture {
case "armhf", "armel", "arm":
if runtime.GOARCH != "arm64" && runtime.GOARCH != "arm" {
q.qemusrc = "/usr/bin/qemu-arm-static"
}
case "arm64":
if runtime.GOARCH != "arm64" {
q.qemusrc = "/usr/bin/qemu-aarch64-static"
}
case "mips":
q.qemusrc = "/usr/bin/qemu-mips-static"
case "mipsel":
if runtime.GOARCH != "mips64le" && runtime.GOARCH != "mipsle" {
q.qemusrc = "/usr/bin/qemu-mipsel-static"
}
case "mips64el":
if runtime.GOARCH != "mips64le" {
q.qemusrc = "/usr/bin/qemu-mips64el-static"
}
case "riscv64":
if runtime.GOARCH != "riscv64" {
q.qemusrc = "/usr/bin/qemu-riscv64-static"
}
case "i386":
if runtime.GOARCH != "amd64" && runtime.GOARCH != "386" {
q.qemusrc = "/usr/bin/qemu-i386-static"
}
case "amd64":
if runtime.GOARCH != "amd64" {
q.qemusrc = "/usr/bin/qemu-x86_64-static"
}
case "sh4":
if runtime.GOARCH != "sh4" {
q.qemusrc = "/usr/bin/qemu-sh4-static"
}
default:
return nil, fmt.Errorf("unsupported qemu architecture %s", c.Architecture)
}
if q.qemusrc != "" {
q.qemutarget = path.Join(c.Chroot, q.qemusrc)
}
return &q, nil
}
func (q qemuHelper) Setup() error {
if q.qemusrc == "" {
return nil
}
return CopyFile(q.qemusrc, q.qemutarget, 0755)
}
func (q qemuHelper) Cleanup() {
if q.qemusrc != "" {
os.Remove(q.qemutarget)
}
}