From eb9f4d54005cccfc25e5870ab5fae7d881771216 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Motiejus=20Jak=C5=A1tys?= Date: Mon, 24 May 2021 00:11:58 +0300 Subject: [PATCH] preliminary unit tests --- lxcconfig/BUILD | 12 ++++- lxcconfig/lxcconfig.go | 97 +++++++++++++++++++++++++++++-------- lxcconfig/lxcconfig_test.go | 58 ++++++++++++++++++++++ rootfs/rootfs.go | 4 +- 4 files changed, 148 insertions(+), 23 deletions(-) create mode 100644 lxcconfig/lxcconfig_test.go diff --git a/lxcconfig/BUILD b/lxcconfig/BUILD index d69355b..2aca87a 100644 --- a/lxcconfig/BUILD +++ b/lxcconfig/BUILD @@ -1,4 +1,4 @@ -load("@io_bazel_rules_go//go:def.bzl", "go_library") +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") go_library( name = "go_default_library", @@ -6,3 +6,13 @@ go_library( importpath = "github.com/motiejus/code/undocker/lxcconfig", visibility = ["//visibility:public"], ) + +go_test( + name = "go_default_test", + srcs = ["lxcconfig_test.go"], + embed = [":go_default_library"], + deps = [ + "@com_github_stretchr_testify//assert:go_default_library", + "@com_github_stretchr_testify//require:go_default_library", + ], +) diff --git a/lxcconfig/lxcconfig.go b/lxcconfig/lxcconfig.go index 6a529e9..6f70c81 100644 --- a/lxcconfig/lxcconfig.go +++ b/lxcconfig/lxcconfig.go @@ -7,32 +7,89 @@ import ( "fmt" "io" "strings" + "text/template" ) -const _manifestJSON = "manifest.json" - -// dockerManifest is manifest.json -type dockerManifest []struct { - Config string `json:"Config"` -} - -// dockerConfig returns interesting configs for the container. user/group are -// skipped, since Docker allows specifying them by name, which would require -// peeking into the container image's /etc/passwd to resolve the names to ints. -type dockerConfig struct { - Architecture string `json:"architecture"` - Config struct { - Entrypoint []string `json:"Entrypoint"` - Cmd []string `json:"Cmd"` - Env []string `json:"Env"` - WorkingDir string `json:"WorkingDir"` - } `json:"config"` -} +const ( + _json = ".json" + _manifestJSON = "manifest.json" +) var ( + _lxcTemplate = template.Must( + template.New("lxcconfig").Parse("" + + "lxc.include = LXC_TEMPLATE_CONFIG/common.conf\n" + + "lxc.architecture = {{ .Architecture }}\n" + + "lxc.execute.cmd = '{{ .Cmd }}'\n" + + "{{ if .Cwd }}lxc.init.cwd = {{ .Cwd }}\n{{ end }}" + + "{{ range .Env }}lxc.environment = {{ . }}\n{{ end }}")) errBadManifest = errors.New("bad or missing manifest.json") ) +type ( + // lxcConfig is passed to _lxcTemplate + lxcConfig struct { + Architecture string + Cmd string + Cwd string + Env []string + } + + // dockerManifest is manifest.json + dockerManifest []struct { + Config string `json:"Config"` + } + + // dockerConfig returns interesting configs for the container. user/group are + // skipped, since Docker allows specifying them by name, which would require + // peeking into the container image's /etc/passwd to resolve the names to ints. + dockerConfig struct { + Architecture string `json:"architecture"` + Config dockerConfigConfig `json:"config"` + } + + dockerConfigConfig struct { + Entrypoint []string `json:"Entrypoint"` + Cmd []string `json:"Cmd"` + WorkingDir string `json:"WorkingDir"` + Env []string `json:"Env"` + } +) + +// LXCConfig accepts a Docker container image and returns lxc configuration. +func LXCConfig(in io.ReadSeeker, wr io.Writer) error { + dockerCfg, err := getDockerConfig(in) + if err != nil { + return err + } + lxcCfg := docker2lxc(dockerCfg) + return lxcCfg.WriteTo(wr) +} + +func docker2lxc(d dockerConfig) lxcConfig { + // cmd/entrypoint logic is copied from lxc-oci template + ep := strings.Join(d.Config.Entrypoint, " ") + cmd := strings.Join(d.Config.Cmd, " ") + if len(ep) == 0 { + ep = cmd + if len(ep) == 0 { + ep = "/bin/sh" + } + } else if len(cmd) != 0 { + ep = ep + " " + cmd + } + return lxcConfig{ + Architecture: d.Architecture, + Cmd: ep, + Env: d.Config.Env, + Cwd: d.Config.WorkingDir, + } +} + +func (l lxcConfig) WriteTo(wr io.Writer) error { + return _lxcTemplate.Execute(wr, l) +} + func getDockerConfig(in io.ReadSeeker) (dockerConfig, error) { tr := tar.NewReader(in) // get offsets to all json files in the archive @@ -45,7 +102,7 @@ func getDockerConfig(in io.ReadSeeker) (dockerConfig, error) { if hdr.Typeflag != tar.TypeReg { continue } - if !strings.HasSuffix(".json", hdr.Name) { + if !strings.HasSuffix(_json, hdr.Name) { continue } here, err := in.Seek(0, io.SeekCurrent) diff --git a/lxcconfig/lxcconfig_test.go b/lxcconfig/lxcconfig_test.go new file mode 100644 index 0000000..b523371 --- /dev/null +++ b/lxcconfig/lxcconfig_test.go @@ -0,0 +1,58 @@ +package lxcconfig + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestLXCConfig(t *testing.T) { + tests := []struct { + name string + docker dockerConfig + want string + }{ + { + name: "just architecture", + docker: dockerConfig{ + Architecture: "amd64", + }, + want: `lxc.include = LXC_TEMPLATE_CONFIG/common.conf +lxc.architecture = amd64 +lxc.execute.cmd = '/bin/sh' +`, + }, + { + name: "all fields", + docker: dockerConfig{ + Architecture: "amd64", + Config: dockerConfigConfig{ + Entrypoint: []string{"/entrypoint.sh"}, + Cmd: []string{"/bin/sh", "-c", "echo foo"}, + WorkingDir: "/x", + Env: []string{ + `LONGNAME="Foo Bar"`, + "SHELL=/bin/tcsh", + }, + }, + }, + want: `lxc.include = LXC_TEMPLATE_CONFIG/common.conf +lxc.architecture = amd64 +lxc.execute.cmd = '/entrypoint.sh /bin/sh -c echo foo' +lxc.init.cwd = /x +lxc.environment = LONGNAME="Foo Bar" +lxc.environment = SHELL=/bin/tcsh +`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var buf bytes.Buffer + require.NoError(t, docker2lxc(tt.docker).WriteTo(&buf)) + assert.Equal(t, tt.want, string(buf.Bytes())) + }) + } +} diff --git a/rootfs/rootfs.go b/rootfs/rootfs.go index 5028ea8..75e3050 100644 --- a/rootfs/rootfs.go +++ b/rootfs/rootfs.go @@ -36,9 +36,9 @@ type dockerManifestJSON []struct { // I) layer name // II) offset (0 being the first file in the layer) // 4. go through -func RootFS(in io.ReadSeeker, out io.Writer) (err error) { +func RootFS(in io.ReadSeeker, wr io.Writer) (err error) { tr := tar.NewReader(in) - tw := tar.NewWriter(out) + tw := tar.NewWriter(wr) defer func() { err = multierr.Append(err, tw.Close()) }() // layerOffsets maps a layer name (a9b123c0daa/layer.tar) to it's offset