1
0
out-of-tree/container/container.go

532 lines
9.6 KiB
Go

// Copyright 2023 Mikhail Klementev. All rights reserved.
// Use of this source code is governed by a AGPLv3 license
// (or later) that can be found in the LICENSE file.
package container
import (
"bufio"
"errors"
"fmt"
"os"
"os/exec"
"path"
"path/filepath"
"regexp"
"strings"
"time"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"code.dumpstack.io/tools/out-of-tree/config/dotfiles"
"code.dumpstack.io/tools/out-of-tree/distro"
)
var Runtime = "docker"
var Registry = ""
var Timeout time.Duration
var Commands []distro.Command
var UseCache = true
var Prune = true
type Image struct {
Name string
Distro distro.Distro
}
func Images() (diis []Image, err error) {
cmd := exec.Command(Runtime, "images")
log.Debug().Msgf("%v", cmd)
rawOutput, err := cmd.CombinedOutput()
if err != nil {
return
}
r, err := regexp.Compile("out_of_tree_.*")
if err != nil {
return
}
containers := r.FindAll(rawOutput, -1)
for _, c := range containers {
containerName := strings.Fields(string(c))[0]
s := strings.Replace(containerName, "__", ".", -1)
values := strings.Split(s, "_")
distroName, ver := values[3], values[4]
dii := Image{
Name: containerName,
}
dii.Distro.Release = ver
dii.Distro.ID, err = distro.NewID(distroName)
if err != nil {
return
}
diis = append(diis, dii)
}
return
}
func Load(localpath string, name string) (err error) {
exist := Container{name: name}.Exist()
if exist && UseCache {
return
}
cmd := exec.Command(Runtime, "load", "-i", localpath)
log.Debug().Msgf("%v", cmd)
raw, err := cmd.CombinedOutput()
if err != nil {
log.Debug().Err(err).Msg(string(raw))
return
}
return
}
func Import(path, name string) (err error) {
exist := Container{name: name}.Exist()
if exist && UseCache {
return
}
cmd := exec.Command(Runtime, "import", path, name)
log.Debug().Msgf("%v", cmd)
raw, err := cmd.CombinedOutput()
if err != nil {
log.Debug().Err(err).Msg(string(raw))
return
}
return
}
func Save(name, path string) (err error) {
exist := Container{name: name}.Exist()
if !exist {
err = errors.New("container does not exist")
log.Error().Err(err).Msg("")
return
}
cmd := exec.Command(Runtime, "save", name, "-o", path)
log.Debug().Msgf("%v", cmd)
raw, err := cmd.CombinedOutput()
if err != nil {
log.Error().Err(err).Msg(string(raw))
return
}
return
}
type Volume struct {
Src, Dest string
}
type Container struct {
name string
dist distro.Distro
Volumes []Volume
// Additional arguments
Args []string
Log zerolog.Logger
}
func New(dist distro.Distro) (c Container, err error) {
distro := strings.ToLower(dist.ID.String())
release := strings.Replace(dist.Release, ".", "__", -1)
c.name = fmt.Sprintf("out_of_tree_%s_%s", distro, release)
c.Log = log.With().
Str("container", c.name).
Logger()
c.dist = dist
c.Volumes = append(c.Volumes, Volume{
Src: dotfiles.Dir("volumes", c.name, "lib", "modules"),
Dest: "/lib/modules",
})
c.Volumes = append(c.Volumes, Volume{
Src: dotfiles.Dir("volumes", c.name, "usr", "src"),
Dest: "/usr/src",
})
c.Volumes = append(c.Volumes, Volume{
Src: dotfiles.Dir("volumes", c.name, "boot"),
Dest: "/boot",
})
return
}
func NewFromKernelInfo(ki distro.KernelInfo) (
c Container, err error) {
c.name = ki.ContainerName
c.Log = log.With().
Str("container", c.name).
Logger()
c.Volumes = append(c.Volumes, Volume{
Src: path.Dir(ki.ModulesPath),
Dest: "/lib/modules",
})
c.Volumes = append(c.Volumes, Volume{
Src: filepath.Join(path.Dir(ki.KernelPath), "../usr/src"),
Dest: "/usr/src",
})
c.Volumes = append(c.Volumes, Volume{
Src: path.Dir(ki.KernelPath),
Dest: "/boot",
})
return
}
func (c Container) Name() string {
return c.name
}
func (c Container) Exist() (yes bool) {
cmd := exec.Command(Runtime, "images", "-q", c.name)
c.Log.Debug().Msgf("run %v", cmd)
raw, err := cmd.CombinedOutput()
if err != nil {
c.Log.Error().Err(err).Msg(string(raw))
return false
}
yes = string(raw) != ""
if yes {
c.Log.Debug().Msg("exist")
} else {
c.Log.Debug().Msg("does not exist")
}
return
}
func (c Container) Build(image string, envs, runs []string) (err error) {
cdir := dotfiles.Dir("containers", c.name)
cfile := filepath.Join(cdir, "Dockerfile")
cf := "FROM "
if Registry != "" {
cf += Registry + "/"
}
cf += image + "\n"
for _, c := range Commands {
// TODO check for distro type
cf += "RUN " + c.Command + "\n"
}
for _, e := range envs {
cf += "ENV " + e + "\n"
}
for _, c := range runs {
cf += "RUN " + c + "\n"
}
buf, err := os.ReadFile(cfile)
if err != nil {
err = os.WriteFile(cfile, []byte(cf), os.ModePerm)
if err != nil {
return
}
}
if string(buf) == cf && c.Exist() && UseCache {
return
}
err = os.WriteFile(cfile, []byte(cf), os.ModePerm)
if err != nil {
return
}
if c.Exist() {
c.Log.Info().Msg("update")
} else {
c.Log.Info().Msg("build")
}
output, err := c.build(cdir)
if err != nil {
c.Log.Error().Err(err).Msg(output)
return
}
c.Log.Info().Msg("success")
return
}
func (c Container) prune() error {
c.Log.Debug().Msg("remove dangling or unused images from local storage")
return exec.Command(Runtime, "image", "prune", "-f").Run()
}
func (c Container) build(imagePath string) (output string, err error) {
if Prune {
defer c.prune()
}
args := []string{"build"}
if !UseCache {
args = append(args, "--pull", "--no-cache")
}
args = append(args, "-t", c.name, imagePath)
cmd := exec.Command(Runtime, args...)
flog := c.Log.With().
Str("command", fmt.Sprintf("%v", cmd)).
Logger()
stdout, err := cmd.StdoutPipe()
if err != nil {
return
}
cmd.Stderr = cmd.Stdout
err = cmd.Start()
if err != nil {
return
}
go func() {
scanner := bufio.NewScanner(stdout)
for scanner.Scan() {
m := scanner.Text()
output += m + "\n"
flog.Trace().Str("stdout", m).Msg("")
}
}()
err = cmd.Wait()
return
}
func (c Container) Run(workdir string, cmds []string) (out string, err error) {
flog := c.Log.With().
Str("workdir", workdir).
Str("command", fmt.Sprintf("%v", cmds)).
Logger()
var args []string
args = append(args, "run", "--rm")
args = append(args, c.Args...)
if workdir != "" {
args = append(args, "-v", workdir+":/work")
}
for _, volume := range c.Volumes {
mount := fmt.Sprintf("%s:%s", volume.Src, volume.Dest)
args = append(args, "-v", mount)
}
command := "true"
for _, c := range cmds {
command += fmt.Sprintf(" && %s", c)
}
args = append(args, c.name, "bash", "-c")
if workdir != "" {
args = append(args, "cd /work && "+command)
} else {
args = append(args, command)
}
cmd := exec.Command(Runtime, args...)
flog.Debug().Msgf("%v", cmd)
stdout, err := cmd.StdoutPipe()
if err != nil {
return
}
cmd.Stderr = cmd.Stdout
if Timeout != 0 {
timer := time.AfterFunc(Timeout, func() {
flog.Info().Msg("killing container by timeout")
flog.Debug().Msg("SIGINT")
cmd.Process.Signal(os.Interrupt)
time.Sleep(time.Minute)
flog.Debug().Msg("SIGKILL")
cmd.Process.Kill()
})
defer timer.Stop()
}
err = cmd.Start()
if err != nil {
return
}
go func() {
scanner := bufio.NewScanner(stdout)
for scanner.Scan() {
m := scanner.Text()
out += m + "\n"
flog.Trace().Str("stdout", m).Msg("")
}
}()
err = cmd.Wait()
if err != nil {
e := fmt.Sprintf("error `%v` for cmd `%v` with output `%v`",
err, cmds, out)
err = errors.New(e)
return
}
return
}
func FindKernel(entries []os.DirEntry, kname string) (name string, err error) {
for _, e := range entries {
var fi os.FileInfo
fi, err = e.Info()
if err != nil {
return
}
if strings.HasPrefix(fi.Name(), "vmlinuz") {
if strings.Contains(fi.Name(), kname) {
name = fi.Name()
return
}
}
}
err = errors.New("cannot find kernel")
return
}
func FindInitrd(entries []os.DirEntry, kname string) (name string, err error) {
for _, e := range entries {
var fi os.FileInfo
fi, err = e.Info()
if err != nil {
return
}
if strings.HasPrefix(fi.Name(), "initrd") ||
strings.HasPrefix(fi.Name(), "initramfs") {
if strings.Contains(fi.Name(), kname) {
name = fi.Name()
return
}
}
}
err = errors.New("cannot find kernel")
return
}
func (c Container) Kernels() (kernels []distro.KernelInfo, err error) {
if !c.Exist() {
return
}
var libmodules, boot string
for _, volume := range c.Volumes {
switch volume.Dest {
case "/lib/modules":
libmodules = volume.Src
case "/boot":
boot = volume.Src
}
}
moddirs, err := os.ReadDir(libmodules)
if err != nil {
return
}
bootfiles, err := os.ReadDir(boot)
if err != nil {
return
}
for _, e := range moddirs {
var krel os.FileInfo
krel, err = e.Info()
if err != nil {
return
}
c.Log.Debug().Msgf("generate config entry for %s", krel.Name())
var kernelFile, initrdFile string
kernelFile, err = FindKernel(bootfiles, krel.Name())
if err != nil {
c.Log.Warn().Msgf("cannot find kernel %s", krel.Name())
continue
}
initrdFile, err = FindInitrd(bootfiles, krel.Name())
if err != nil {
c.Log.Warn().Msgf("cannot find initrd %s", krel.Name())
continue
}
ki := distro.KernelInfo{
Distro: c.dist,
KernelVersion: krel.Name(),
KernelRelease: krel.Name(),
ContainerName: c.name,
KernelPath: filepath.Join(boot, kernelFile),
InitrdPath: filepath.Join(boot, initrdFile),
ModulesPath: filepath.Join(libmodules, krel.Name()),
RootFS: dotfiles.File("images", c.dist.RootFS()),
}
kernels = append(kernels, ki)
}
for _, cmd := range []string{
"find /boot -type f -exec chmod a+r {} \\;",
} {
_, err = c.Run(dotfiles.Dir("tmp"), []string{cmd})
if err != nil {
return
}
}
return
}