In-house Alpine builder

This commit is contained in:
AlexSSD7 2023-08-27 15:30:51 +01:00
commit f9cdbe5ac9
17 changed files with 315 additions and 678 deletions

View file

@ -0,0 +1,200 @@
package builder
import (
"context"
"fmt"
"os"
"os/exec"
"os/signal"
"path/filepath"
"strings"
"sync"
"syscall"
"log/slog"
"github.com/AlexSSD7/linsk/vm"
"github.com/alessio/shellescape"
"github.com/pkg/errors"
"golang.org/x/crypto/ssh"
)
type BuildContext struct {
logger *slog.Logger
vi *vm.VM
}
func NewBuildContext(logger *slog.Logger, baseISOPath string, outPath string, showVMDisplay bool) (*BuildContext, error) {
baseISOPath = filepath.Clean(baseISOPath)
outPath = filepath.Clean(outPath)
_, err := os.Stat(outPath)
if err != nil {
if !errors.Is(err, os.ErrNotExist) {
return nil, errors.Wrap(err, "stat output file")
}
// File doesn't exist. Continuing.
} else {
return nil, fmt.Errorf("output file already exists")
}
err = createQEMUImg(outPath)
if err != nil {
return nil, errors.Wrap(err, "create temporary qemu image")
}
vi, err := vm.NewVM(logger.With("subcaller", "vm"), vm.VMConfig{
CdromImagePath: baseISOPath,
Drives: []vm.DriveConfig{{
Path: outPath,
}},
UnrestrictedNetworking: true,
ShowDisplay: showVMDisplay,
InstallBaseUtilities: true,
})
if err != nil {
return nil, errors.Wrap(err, "create vm instance")
}
return &BuildContext{
logger: logger,
vi: vi,
}, nil
}
func createQEMUImg(outPath string) error {
outPath = filepath.Clean(outPath)
err := exec.Command("qemu-img", "create", "-f", "qcow2", outPath, "1G").Run()
if err != nil {
return errors.Wrap(err, "run qemu-img create cmd")
}
return nil
}
func (bc *BuildContext) BuildWithInterruptHandler() error {
defer func() {
err := bc.vi.Cancel()
if err != nil {
bc.logger.Error("Failed to cancel VM context", "error", err)
}
}()
runErrCh := make(chan error, 1)
var wg sync.WaitGroup
ctx, ctxCancel := context.WithCancel(context.Background())
defer ctxCancel()
interrupt := make(chan os.Signal, 2)
signal.Notify(interrupt, syscall.SIGTERM, syscall.SIGINT)
defer signal.Reset()
wg.Add(1)
go func() {
defer wg.Done()
err := bc.vi.Run()
ctxCancel()
runErrCh <- err
}()
go func() {
for i := 0; ; i++ {
select {
case <-ctx.Done():
return
case sig := <-interrupt:
lg := slog.With("signal", sig)
if i == 0 {
lg.Warn("Caught interrupt, safely shutting down")
} else if i < 10 {
lg.Warn("Caught subsequent interrupt, please interrupt n more times to panic", "n", 10-i)
} else {
panic("force interrupt")
}
err := bc.vi.Cancel()
if err != nil {
lg.Warn("Failed to cancel VM context", "error", err)
}
}
}
}()
for {
select {
case err := <-runErrCh:
if err == nil {
return fmt.Errorf("operation canceled by user")
}
return errors.Wrap(err, "start vm")
case <-bc.vi.SSHUpNotifyChan():
sc, err := bc.vi.DialSSH()
if err != nil {
return errors.Wrap(err, "dial vm ssh")
}
defer func() { _ = sc.Close() }()
bc.logger.Info("Installation in progress")
err = runAlpineSetupCmd(sc, []string{"openssh", "lvm2", "util-linux", "cryptsetup", "samba"})
if err != nil {
return errors.Wrap(err, "run alpine setup cmd")
}
err = bc.vi.Cancel()
if err != nil {
return errors.Wrap(err, "cancel vm context")
}
select {
case err := <-runErrCh:
if err != nil {
return errors.Wrap(err, "run vm")
}
default:
}
return nil
}
}
}
func runAlpineSetupCmd(sc *ssh.Client, pkgs []string) error {
sess, err := sc.NewSession()
if err != nil {
return errors.Wrap(err, "new session")
}
// TODO: Timeout for this command.
defer func() {
_ = sess.Close()
}()
cmd := "ifconfig eth0 up && ifconfig lo up && udhcpc && true > /etc/apk/repositories && setup-apkrepos -1 && printf 'y' | setup-disk -m sys /dev/vda"
if len(pkgs) != 0 {
pkgsQuoted := make([]string, len(pkgs))
for i, rawPkg := range pkgs {
pkgsQuoted[i] = shellescape.Quote(rawPkg)
}
cmd += " && mount /dev/vda3 /mnt && chroot /mnt apk add " + strings.Join(pkgsQuoted, " ")
}
err = sess.Run(cmd)
if err != nil {
return errors.Wrap(err, "run setup cmd")
}
return nil
}

51
cmd/imgbuilder/main.go Normal file
View file

@ -0,0 +1,51 @@
package main
import (
"log/slog"
"os"
"path/filepath"
"github.com/AlexSSD7/linsk/cmd/imgbuilder/builder"
"github.com/spf13/cobra"
)
var rootCmd = &cobra.Command{
Use: "imgbuilder",
// TODO: Fill this
// Short: "",
// Long: ``,
Args: cobra.ExactArgs(2),
Run: func(cmd *cobra.Command, args []string) {
baseISOPath := filepath.Clean(args[0])
outImagePath := filepath.Clean(args[1])
bc, err := builder.NewBuildContext(slog.With("caller", "build-context"), baseISOPath, outImagePath, vmDebugFlag)
if err != nil {
slog.Error("Failed to create a new build context", "error", err)
os.Exit(1)
}
err = bc.BuildWithInterruptHandler()
if err != nil {
slog.Error("Failed to build an image", "error", err)
os.Exit(1)
}
slog.Info("Success")
},
}
var vmDebugFlag bool
func init() {
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, nil)))
rootCmd.PersistentFlags().BoolVar(&vmDebugFlag, "vmdebug", false, "Enable VM debug mode. This will open an accessible VM monitor. You can log in with root user and no password.")
}
func main() {
err := rootCmd.Execute()
if err != nil {
os.Exit(1)
}
}