In-house Alpine builder
This commit is contained in:
parent
ee447087f6
commit
f9cdbe5ac9
17 changed files with 315 additions and 678 deletions
200
cmd/imgbuilder/builder/build.go
Normal file
200
cmd/imgbuilder/builder/build.go
Normal 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
51
cmd/imgbuilder/main.go
Normal 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)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue