25 Commits
v1.0 ... v1.0.4

Author SHA1 Message Date
Motiejus Jakštys
6d24d0da7a replace io/ioutil with os 2023-01-03 16:00:34 +02:00
Motiejus Jakštys
d3efb410d0 make: fix clean target 2022-08-29 18:02:22 +03:00
1c46fb8617 release: a better changelog 2022-06-09 14:28:39 +03:00
4408a9e005 rootfs: remove ./ prefix from filenames
I've seen a container (private one) whose filenames start with ./, but
the layer name does not have the prefix, causing undocker to fail.

Let's always normalize the names to not have the prefix.
2022-06-09 14:22:21 +03:00
41baf180a3 [ci] merge test and lint steps to one 2021-09-29 14:45:53 +03:00
4754869190 [ci] replace tee with awk 2021-09-29 14:43:33 +03:00
ec62b65ecd [ci] add a usage step 2021-09-29 14:38:31 +03:00
0ff72b99d9 remove unnecessary support for prebuilt binaries
the users will know how to build the binaries themselves.
2021-09-29 14:30:18 +03:00
e4c0d4d72a remove references to 'official' binaries 2021-09-27 19:08:04 +03:00
eab7c3c1ef [release] fix nullglob issue 2021-09-06 15:36:50 +03:00
ec61d24fd4 [nit] Makefile style 2021-09-06 09:01:05 +03:00
d3933efd74 Update README
- add communication section
- remove systemd-nspawn section
2021-09-06 08:25:11 +03:00
a265915b21 Shellcheck release 2021-09-01 09:41:00 +03:00
8c00e74df9 Update release script
- fix paths of sha256sum
- check for old artifacts before doing the release
2021-09-01 09:38:13 +03:00
5e297827d3 update README with more actionable examples 2021-09-01 09:32:26 +03:00
dc03b29e6e Revert "add WithFilePrefix"
This reverts commit 436a866f5d.

This is not a good place for such auxiliary functionality; will be moved
elsewhere.
2021-08-29 18:40:11 +03:00
f02af98ac5 nit: shorter defer body 2021-08-29 18:39:43 +03:00
cca579d218 update Makefile and default target
Adds a `sha256sum-$(VSN).txt`. For easier CI.
2021-08-29 18:23:49 +03:00
436a866f5d add WithFilePrefix
This adds an option to prefix every file path with a given string.
2021-08-29 16:55:32 +03:00
71aab65b75 version sha256sum files 2021-08-29 07:49:32 +03:00
984752b812 remove unused archs
I am not using nor need arm64 and windows. Removing these from the
Makefile. If you see this message and feel like they should be added
back (or different archs), ping me.
2021-08-25 08:57:10 +03:00
4568429a69 main.go: remove file if on Flatten(..) failure
When a Flatten() fails, the main program used to leave an incomplete
file (usually zero-length). Now it will clean itself up on failure.
2021-08-25 08:49:30 +03:00
6f40bc91be simplify linter 2021-08-24 17:23:31 +03:00
63d171007e shellcheck 2021-08-24 16:44:58 +03:00
5d4d6410da remove windows/arm64
not available on go 1.16
2021-08-24 14:50:30 +03:00
8 changed files with 142 additions and 124 deletions

View File

@@ -3,17 +3,17 @@ packages:
- go - go
- git - git
- make - make
- shellcheck
sources: sources:
- https://git.sr.ht/~motiejus/undocker - https://git.sr.ht/~motiejus/undocker
tasks: tasks:
- setup: | - setup: |
go install honnef.co/go/tools/cmd/staticcheck@latest go install honnef.co/go/tools/cmd/staticcheck@latest
- test: | - built-test-lint: |
make -C undocker coverage.html make -C undocker -O -j$(nproc) undocker coverage.html lint
- lint: | - usage: |
make -C undocker -O -j$(nproc) lint # like 'grep -q', but prints output too.
- binaries: | # | tee /dev/stderr doesn't work on sourcehut workers; permission denied.
make -C undocker -O -j$(nproc) sha256sum.txt ./undocker/undocker |& awk 'BEGIN{c=1};/Built with /{c=0};{print};END{exit c}'
cat undocker/sha256sum.txt
artifacts: artifacts:
- undocker/coverage.html - undocker/coverage.html

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
/undocker
/undocker-* /undocker-*
coverage.html coverage.html
sha256sum.txt* sha256sum.txt*

View File

@@ -1,58 +1,29 @@
SCRIPTS = $(shell awk '/#!\/bin\/(ba)?sh/&&FNR==1{print FILENAME}' $(shell git ls-files))
GODEPS = $(shell git ls-files '*.go' go.mod go.sum) GODEPS = $(shell git ls-files '*.go' go.mod go.sum)
GOBIN = $(shell go env GOPATH)/bin/
GOOSARCHS = $(sort \
darwin/amd64 \
darwin/arm64 \
linux/amd64 \
linux/arm64 \
windows/amd64/.exe \
windows/arm64/.exe)
VSN ?= $(shell git describe --dirty) VSN ?= $(shell git describe --dirty)
VSNHASH = $(shell git rev-parse --verify HEAD) VSNHASH = $(shell git rev-parse --verify HEAD)
LDFLAGS = -ldflags "-X main.Version=$(VSN) -X main.VersionHash=$(VSNHASH)" LDFLAGS = -ldflags "-X main.Version=$(VSN) -X main.VersionHash=$(VSNHASH)"
undocker: ## builds binary for the current architecture
go build $(LDFLAGS) -o $@
.PHONY: test .PHONY: test
test: test: coverage.out
go test -race -cover ./...
define undockertarget
UNDOCKERS += undocker-$(1)-$(2)-$(VSN)$(firstword $(3))
undocker-$(1)-$(2)-$(VSN)$(firstword $(3)): $(GODEPS)
CGO_ENABLED=0 GOOS=$(1) GOARCH=$(2) go build $(LDFLAGS) -o $$@
endef
$(foreach goosarch,$(GOOSARCHS),\
$(eval $(call undockertarget,$(word 1,$(subst /, ,$(goosarch))),$(word 2,$(subst /, ,$(goosarch))),$(word 3,$(subst /, ,$(goosarch))))))
.PHONY: all
all: $(UNDOCKERS)
.PHONY: lint .PHONY: lint
lint: vet staticcheck lint:
.PHONY: vet
vet:
go vet ./... go vet ./...
$(shell go env GOPATH)/bin/staticcheck -f stylish ./...
.PHONY: staticcheck shellcheck $(SCRIPTS)
staticcheck:
$(GOBIN)staticcheck -f stylish ./...
.INTERMEDIATE: coverage.out .INTERMEDIATE: coverage.out
coverage.out: $(GODEPS) coverage.out: $(GODEPS)
go test -coverprofile $@ ./... go test -race -cover -coverprofile $@ ./...
coverage.html: coverage.out coverage.html: coverage.out
go tool cover -html=$< -o $@ go tool cover -html=$< -o $@
sha256sum.txt: $(UNDOCKERS)
sha256sum $(UNDOCKERS) > $@
sha256sum.txt.asc: sha256sum.txt
gpg --clearsign $<
.PHONY: clean .PHONY: clean
clean: clean:
rm -f undocker-*-v* coverage.html sha256sum.txt sha256sum.txt.asc rm -f undocker coverage.html

View File

@@ -18,7 +18,19 @@ and application isolation ("container") runtimes: once the docker image is
extracted, it can be run with old-fashioned tools: lxc, systemd-nspawn, extracted, it can be run with old-fashioned tools: lxc, systemd-nspawn,
systemd, FreeBSD Jails, and many others. systemd, FreeBSD Jails, and many others.
Undocker has no dependencies outside Golang stdlib.
Installation
------------
Build it like this for the "current" platform:
```
$ make undocker
```
`make -B` will print the extra flags (`-X <...>`) for cross-compiling with
other archs. It's all `go build <...>` in the back, and depends only on Go's
compiler and stdlib.
Usage: convert docker image to rootfs Usage: convert docker image to rootfs
------------------------------------- -------------------------------------
@@ -27,50 +39,41 @@ Download `busybox` docker image from docker hub and convert it to a rootfs:
``` ```
$ skopeo copy docker://docker.io/busybox:latest docker-archive:busybox.tar $ skopeo copy docker://docker.io/busybox:latest docker-archive:busybox.tar
$ undocker busybox.tar - | tar -tv | head -10 $ undocker busybox.tar - | tar -xv | sponge | head -10; echo '<...>'
drwxr-xr-x 0/0 0 2021-05-17 22:07 bin/ bin/
-rwxr-xr-x 0/0 1149184 2021-05-17 22:07 bin/[ bin/[
hrwxr-xr-x 0/0 0 2021-05-17 22:07 bin/[[ link to bin/[ bin/[[
hrwxr-xr-x 0/0 0 2021-05-17 22:07 bin/acpid link to bin/[ bin/acpid
hrwxr-xr-x 0/0 0 2021-05-17 22:07 bin/add-shell link to bin/[ bin/add-shell
hrwxr-xr-x 0/0 0 2021-05-17 22:07 bin/addgroup link to bin/[ bin/addgroup
hrwxr-xr-x 0/0 0 2021-05-17 22:07 bin/adduser link to bin/[ bin/adduser
hrwxr-xr-x 0/0 0 2021-05-17 22:07 bin/adjtimex link to bin/[ bin/adjtimex
hrwxr-xr-x 0/0 0 2021-05-17 22:07 bin/ar link to bin/[ bin/ar
hrwxr-xr-x 0/0 0 2021-05-17 22:07 bin/arch link to bin/[ bin/arch
<...>
``` ```
You can also refer [here][2] for other ways to download Docker images. There Refer [here][2] for other ways to download Docker images. There are many.
are many.
Converting a [1.1GB Docker image with 77 On author's laptop converting a [1.1GB Docker image with 77
layers](https://hub.docker.com/r/homeassistant/home-assistant) takes around 4 layers](https://hub.docker.com/r/homeassistant/home-assistant) takes around 3
seconds and on a reasonably powerful Intel laptop. seconds and uses ~65MB of residential memory.
Usage example: systemd-nspawn Usage example: systemd
----------------------------- ----------------------
Start with systemd-nspawn:
```
systemd-nspawn -D $PWD busybox httpd -vfp 8080
```
Usage example: plain old systemd
--------------------------------
``` ```
systemd-run \ systemd-run \
--wait --pty --collect --service-type=exec \ --wait --pty --collect --service-type=exec \
-p RootDirectory=$PWD \
-p ProtectProc=invisible \
-p PrivateUsers=true \ -p PrivateUsers=true \
-p DynamicUser=yes \ -p DynamicUser=yes \
-p ProtectProc=invisible \
-p RootDirectory=$PWD \
-- busybox httpd -vfp 8080 -- busybox httpd -vfp 8080
``` ```
Good things like `PrivateUsers`, `DynamicUser`, `ProtectProc` and other [Systemd protections][1] like `PrivateUsers`, `DynamicUser`, `ProtectProc` and
[systemd protections][1] are available, just like to any systemd unit. others are available, just like to any systemd unit.
Similar Projects Similar Projects
---------------- ----------------
@@ -89,7 +92,7 @@ Contributions
The following contributions may be accepted: The following contributions may be accepted:
- Pull requests (patchsets) with accompanying tests. - Patchsets, with accompanying tests.
- Regression reports. - Regression reports.
If you found a container that undocker cannot extract, or extracts incorrectly If you found a container that undocker cannot extract, or extracts incorrectly
@@ -100,6 +103,12 @@ Reports of regression reports must provide examples of "works before" and "does
not work after". Issues without an accompanying patch will most likely be not work after". Issues without an accompanying patch will most likely be
rejected. rejected.
Communication
-------------
Use [~motiejus/undocker@lists.sr.ht](mailto:~motiejus/undocker@lists.sr.ht) for
questions or patches. Subscribe [here][4].
LICENSE LICENSE
------- -------
@@ -107,3 +116,5 @@ MIT
[1]: https://www.freedesktop.org/software/systemd/man/systemd.exec.html [1]: https://www.freedesktop.org/software/systemd/man/systemd.exec.html
[2]: https://fly.io/blog/docker-without-docker/ [2]: https://fly.io/blog/docker-without-docker/
[3]: http://git.sr.ht/~motiejus/undocker
[4]: https://lists.sr.ht/~motiejus/undocker

16
main.go
View File

@@ -53,15 +53,15 @@ type command struct {
Stdout io.Writer Stdout io.Writer
} }
func (c *command) execute(infile string, outfile string) (err error) { func (c *command) execute(infile string, outfile string) (_err error) {
rd, err := os.Open(infile) rd, err := os.Open(infile)
if err != nil { if err != nil {
return err return err
} }
defer func() { defer func() {
err1 := rd.Close() err := rd.Close()
if err == nil { if _err == nil {
err = err1 _err = err
} }
}() }()
@@ -74,9 +74,11 @@ func (c *command) execute(infile string, outfile string) (err error) {
return fmt.Errorf("create: %w", err) return fmt.Errorf("create: %w", err)
} }
defer func() { defer func() {
err1 := outf.Close() err := outf.Close()
if err == nil { if _err != nil {
err = err1 os.Remove(outfile)
} else {
_err = err
} }
}() }()
out = outf out = outf

View File

@@ -2,8 +2,9 @@ package main
import ( import (
"bytes" "bytes"
"errors"
"io" "io"
"io/ioutil" "os"
"path/filepath" "path/filepath"
"regexp" "regexp"
"testing" "testing"
@@ -13,18 +14,20 @@ func TestExecute(t *testing.T) {
var _foo = []byte("foo foo") var _foo = []byte("foo foo")
tests := []struct { tests := []struct {
name string name string
fixture func(*testing.T, string) fixture func(*testing.T, string)
infile string flattener func(io.ReadSeeker, io.Writer) error
outfile string infile string
wantErr string outfile string
wantErr string
assertion func(*testing.T, string)
}{ }{
{ {
name: "ok passthrough via stdout", name: "ok passthrough via stdout",
infile: "t10-in.txt", infile: "t10-in.txt",
fixture: func(t *testing.T, dir string) { fixture: func(t *testing.T, dir string) {
fname := filepath.Join(dir, "t10-in.txt") fname := filepath.Join(dir, "t10-in.txt")
if err := ioutil.WriteFile(fname, _foo, 0644); err != nil { if err := os.WriteFile(fname, _foo, 0644); err != nil {
t.Fatalf("unexpected error: %v", err) t.Fatalf("unexpected error: %v", err)
} }
}, },
@@ -35,12 +38,37 @@ func TestExecute(t *testing.T) {
infile: "t20-in.txt", infile: "t20-in.txt",
fixture: func(t *testing.T, dir string) { fixture: func(t *testing.T, dir string) {
fname := filepath.Join(dir, "t20-in.txt") fname := filepath.Join(dir, "t20-in.txt")
if err := ioutil.WriteFile(fname, _foo, 0644); err != nil { if err := os.WriteFile(fname, _foo, 0644); err != nil {
t.Fatalf("unexpected error: %v", err) t.Fatalf("unexpected error: %v", err)
} }
}, },
outfile: "t20-out.txt", outfile: "t20-out.txt",
}, },
{
name: "bad flattener should remove the file",
infile: "t30-in.txt",
fixture: func(t *testing.T, dir string) {
fname := filepath.Join(dir, "t30-in.txt")
if err := os.WriteFile(fname, _foo, 0644); err != nil {
t.Fatalf("unexpected error: %v", err)
}
},
flattener: flattenBad,
outfile: "t30-out.txt",
wantErr: "some error",
assertion: func(t *testing.T, dir string) {
d, err := os.ReadDir(dir)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(d) != 1 {
t.Fatalf("expected 1 entry, got %d", len(d))
}
if d[0].Name() != "t30-in.txt" {
t.Fatalf("expected to find only t30-in.txt, got %s", d[0].Name())
}
},
},
{ {
name: "infile does not exist", name: "infile does not exist",
infile: "t3-does-not-exist.txt", infile: "t3-does-not-exist.txt",
@@ -57,7 +85,10 @@ func TestExecute(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
dir := t.TempDir() dir := t.TempDir()
var stdout bytes.Buffer var stdout bytes.Buffer
c := &command{Stdout: &stdout} if tt.flattener == nil {
tt.flattener = flattenPassthrough
}
if tt.fixture != nil { if tt.fixture != nil {
tt.fixture(t, dir) tt.fixture(t, dir)
} }
@@ -65,9 +96,14 @@ func TestExecute(t *testing.T) {
tt.outfile = filepath.Join(dir, tt.outfile) tt.outfile = filepath.Join(dir, tt.outfile)
} }
inf := filepath.Join(dir, tt.infile) inf := filepath.Join(dir, tt.infile)
c.flattener = flattenPassthrough
c := &command{Stdout: &stdout, flattener: tt.flattener}
err := c.execute(inf, tt.outfile) err := c.execute(inf, tt.outfile)
if tt.assertion != nil {
tt.assertion(t, dir)
}
if tt.wantErr != "" { if tt.wantErr != "" {
if err == nil { if err == nil {
t.Fatal("expected error, got nil") t.Fatal("expected error, got nil")
@@ -78,21 +114,22 @@ func TestExecute(t *testing.T) {
} }
return return
} }
var out []byte
if err != nil { if err != nil {
t.Fatalf("unexpected error: %v", err) t.Fatalf("unexpected error: %v", err)
} }
var out []byte
if tt.outfile == "-" { if tt.outfile == "-" {
out = stdout.Bytes() out = stdout.Bytes()
} else { } else {
out, err = ioutil.ReadFile(tt.outfile) out, err = os.ReadFile(tt.outfile)
if err != nil { if err != nil {
t.Fatalf("unexpected error: %v", err) t.Fatalf("unexpected error: %v", err)
} }
} }
if !bytes.Equal([]byte("foo foo"), out) { if !bytes.Equal([]byte("foo foo"), out) {
t.Errorf("out != foo foo: %s", string(out)) t.Errorf("out != foo foo: %q", string(out))
} }
}) })
} }
} }
@@ -101,3 +138,7 @@ func flattenPassthrough(r io.ReadSeeker, w io.Writer) error {
_, err := io.Copy(w, r) _, err := io.Copy(w, r)
return err return err
} }
func flattenBad(_ io.ReadSeeker, _ io.Writer) error {
return errors.New("some error")
}

33
release
View File

@@ -1,32 +1,25 @@
#!/bin/bash #!/bin/bash
set -euo pipefail set -euo pipefail
err() { _err(){ >&2 echo "ERROR: $*"; exit 1; }
>&2 echo "ERROR: $*"
exit 1
}
[[ -n "$(git status --porcelain)" ]] && \ git status --porcelain | grep -q "" &&
err "working tree is dirty, commit your changes first." _err "working tree is dirty, commit your changes first."
[[ ! "$1" =~ ^v([0-9]+)\.([0-9]+)(\.([0-9]+))?$ ]] && \ [[ "$1" =~ ^v([0-9]+)\.([0-9]+)(\.([0-9]+))?(-rc([0-9]+))?$ ]] || \
err "arg1 accepts the following formats: v1.0 v1.0.0" _err "arg1 accepts the following formats: v1.0 v1.0.0 v1.0-rc1 v1.0.1-rc1"
[[ -n "$(git tag | grep "^$1$")" ]] && \ git tag | grep -q "^$1$" &&
err "tag $1 already exists" _err "tag $1 already exists"
# sanity test: do the tests pass?
make -B -j"$(nproc)" test lint
last_tag=$(git tag | tail -1) last_tag=$(git tag | tail -1)
make -B -j$(nproc) VSN=$1 sha256sum.txt.asc
{ {
echo undocker $1 echo undocker "$1"
echo
echo Changelog since $last_tag:
git log --pretty=format:"- [%cn] %s" $last_tag..HEAD
echo
echo
echo sha256sums of released binaries:
cat sha256sum.txt
echo echo
echo Changelog since "$last_tag":
git log --pretty=format:"- [%an] %s" "$last_tag"..HEAD
} | git tag -u motiejus@jakstys.lt -F - "$1" } | git tag -u motiejus@jakstys.lt -F - "$1"

View File

@@ -69,7 +69,7 @@ func Flatten(rd io.ReadSeeker, w io.Writer) (_err error) {
if err != nil { if err != nil {
return err return err
} }
layerOffsets[hdr.Name] = here layerOffsets[strings.TrimPrefix(hdr.Name, "./")] = here
} }
} }
@@ -82,7 +82,7 @@ func Flatten(rd io.ReadSeeker, w io.Writer) (_err error) {
for i, name := range manifest[0].Layers { for i, name := range manifest[0].Layers {
layers[i] = nameOffset{ layers[i] = nameOffset{
name: name, name: name,
offset: layerOffsets[name], offset: layerOffsets[strings.TrimPrefix(name, "./")],
} }
} }
@@ -146,9 +146,8 @@ func Flatten(rd io.ReadSeeker, w io.Writer) (_err error) {
defer func() { defer func() {
// Avoiding use of multierr: if error is present, return // Avoiding use of multierr: if error is present, return
// that. Otherwise return whatever `Close` returns. // that. Otherwise return whatever `Close` returns.
err1 := tw.Close() if err := tw.Close(); err != nil && _err == nil {
if _err == nil { _err = err
_err = err1
} }
}() }()
// iterate through all layers, all files, and write files. // iterate through all layers, all files, and write files.