16 Commits
v1.0 ... v1.0.2

Author SHA1 Message Date
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 139 additions and 85 deletions

View File

@@ -3,6 +3,7 @@ packages:
- go - go
- git - git
- make - make
- shellcheck
sources: sources:
- https://git.sr.ht/~motiejus/undocker - https://git.sr.ht/~motiejus/undocker
tasks: tasks:
@@ -13,7 +14,7 @@ tasks:
- lint: | - lint: |
make -C undocker -O -j$(nproc) lint make -C undocker -O -j$(nproc) lint
- binaries: | - binaries: |
make -C undocker -O -j$(nproc) sha256sum.txt make -C undocker -O -j$(nproc) all
cat undocker/sha256sum.txt 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,44 +1,38 @@
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 linux/amd64)
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
CGO_ENABLED=0 go build $(LDFLAGS) -o $@
.PHONY: test .PHONY: test
test: test:
go test -race -cover ./... go test -race -cover ./...
define undockertarget define undockertarget
UNDOCKERS += undocker-$(1)-$(2)-$(VSN)$(firstword $(3)) UNDOCKERS += undocker-$(1)-$(2)-$(VSN)
undocker-$(1)-$(2)-$(VSN)$(firstword $(3)): $(GODEPS) undocker-$(1)-$(2)-$(VSN): $(GODEPS)
CGO_ENABLED=0 GOOS=$(1) GOARCH=$(2) go build $(LDFLAGS) -o $$@ CGO_ENABLED=0 GOOS=$(1) GOARCH=$(2) go build $(LDFLAGS) -o $$@
endef endef
$(foreach goosarch,$(GOOSARCHS),\ $(foreach goosarch,$(GOOSARCHS),\
$(eval $(call undockertarget,$(word 1,$(subst /, ,$(goosarch))),$(word 2,$(subst /, ,$(goosarch))),$(word 3,$(subst /, ,$(goosarch)))))) $(eval $(call undockertarget,$(word 1,$(subst /, ,$(goosarch))),$(word 2,$(subst /, ,$(goosarch))))))
.PHONY: all .PHONY: all
all: $(UNDOCKERS) all: $(UNDOCKERS) sha256sum-$(VSN).txt
.PHONY: sha256sum-asc
sha256sum-asc: sha256sum-$(VSN).txt.asc
.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)
@@ -47,12 +41,12 @@ coverage.out: $(GODEPS)
coverage.html: coverage.out coverage.html: coverage.out
go tool cover -html=$< -o $@ go tool cover -html=$< -o $@
sha256sum.txt: $(UNDOCKERS) sha256sum-$(VSN).txt: $(UNDOCKERS)
sha256sum $(UNDOCKERS) > $@ sha256sum $(UNDOCKERS) > $@
sha256sum.txt.asc: sha256sum.txt sha256sum-$(VSN).txt.asc: sha256sum-$(VSN).txt
gpg --clearsign $< gpg --clearsign $<
.PHONY: clean .PHONY: clean
clean: clean:
rm -f undocker-*-v* coverage.html sha256sum.txt sha256sum.txt.asc rm -f undocker-*-v* coverage.html sha256sum*.txt sha256sum*.txt.asc

View File

@@ -20,6 +20,15 @@ systemd, FreeBSD Jails, and many others.
Undocker has no dependencies outside Golang stdlib. Undocker has no dependencies outside Golang stdlib.
Installation
------------
You may use [officially released binaries][3], or build it:
```
$ make undocker
```
Usage: convert docker image to rootfs Usage: convert docker image to rootfs
------------------------------------- -------------------------------------
@@ -27,50 +36,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 +89,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 +100,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 +113,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,10 @@ package main
import ( import (
"bytes" "bytes"
"errors"
"io" "io"
"io/ioutil" "io/ioutil"
"os"
"path/filepath" "path/filepath"
"regexp" "regexp"
"testing" "testing"
@@ -13,11 +15,13 @@ 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",
@@ -41,6 +45,31 @@ func TestExecute(t *testing.T) {
}, },
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 := ioutil.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 +86,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 +97,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,10 +115,10 @@ 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 {
@@ -91,8 +128,9 @@ func TestExecute(t *testing.T) {
} }
} }
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 +139,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")
}

23
release
View File

@@ -6,27 +6,34 @@ err() {
exit 1 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]+))?$ ]] && \ # https://raimue.blog/2010/09/09/bash-for-loop-with-glob-patterns/
shopt -s nullglob
for file in sha256sum-*; do
err "found $file from previous release, delete it first"
done
shopt -u nullglob
[[ "$1" =~ ^v([0-9]+)\.([0-9]+)(\.([0-9]+))?$ ]] || \
err "arg1 accepts the following formats: v1.0 v1.0.0" err "arg1 accepts the following formats: v1.0 v1.0.0"
[[ -n "$(git tag | grep "^$1$")" ]] && \ git tag | grep -q "^$1$" &&
err "tag $1 already exists" err "tag $1 already exists"
last_tag=$(git tag | tail -1) last_tag=$(git tag | tail -1)
make -B -j$(nproc) VSN=$1 sha256sum.txt.asc make -B -j"$(nproc)" VSN="$1" sha256sum-asc
{ {
echo undocker $1 echo undocker "$1"
echo echo
echo Changelog since $last_tag: echo Changelog since "$last_tag":
git log --pretty=format:"- [%cn] %s" $last_tag..HEAD git log --pretty=format:"- [%cn] %s" "$last_tag"..HEAD
echo echo
echo echo
echo sha256sums of released binaries: echo sha256sums of released binaries:
cat sha256sum.txt cat sha256sum-*.txt
echo echo
} | git tag -u motiejus@jakstys.lt -F - "$1" } | git tag -u motiejus@jakstys.lt -F - "$1"

View File

@@ -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.