Add e2e cli integration test suite (#31)

Closes #30

Co-authored-by: James Mills <prologic@shortcircuit.net.au>
Reviewed-on: #31
master
James Mills 2 days ago
parent fb640de82e
commit f7c11af57d
  1. 47
      .drone.yml
  2. 26
      Makefile
  3. 30
      cgroups/check.go
  4. 2
      cli/build.go
  5. 2
      cli/doctor.go
  6. 2
      cli/exec.go
  7. 2
      cli/images.go
  8. 2
      cli/init.go
  9. 95
      cli/main.go
  10. 2
      cli/ps.go
  11. 2
      cli/pull.go
  12. 2
      cli/rm.go
  13. 2
      cli/rmi.go
  14. 2
      cli/run.go
  15. 2
      cli/stop.go
  16. 2
      cli/version.go
  17. 77
      cmd/box/main.go
  18. 3
      go.mod
  19. 3
      go.sum
  20. 140
      preflight.sh
  21. 29
      tests/cli_test.go
  22. 53
      tests/testdata/box.ct

@ -1,6 +1,7 @@
---
kind: pipeline
name: default
type: docker
name: unit
steps:
- name: build & run tests
@ -9,34 +10,38 @@ steps:
- make build
- make test
- name: notify
image: plugins/webhook
settings:
urls:
- https://msgbus.mills.io/ci.mills.io
when:
status:
- success
- failure
---
kind: pipeline
type: exec
name: test
name: integration
platform:
os: linux
arch: amd64
steps:
- name: greeting
- name: build & run tests
commands:
- echo hello world
trigger:
branch:
- master
event:
- tag
- push
- pull_request
- make build
- make test
---
kind: pipeline
type: docker
name: nnotification
steps:
- name: notify
image: plugins/webhook
settings:
urls:
- https://msgbus.mills.io/ci.mills.io
when:
status:
- success
- failure
depends_on:
- unit
- integration

@ -1,31 +1,47 @@
.PHONY: dev build install release test clean
.PHONY: dev build install release test cover clean
export GOOS=linux
export CGO_ENABLED=0
VERSION=$(shell git describe --abbrev=0 --tags 2>/dev/null || echo "$VERSION")
COMMIT=$(shell git rev-parse --short HEAD || echo "$COMMIT")
BRANCH=$(shell git rev-parse --abbrev-ref HEAD)
GOCMD=go
all: build
all: preflight build
preflight:
@./preflight.sh
deps:
@$(GOCMD) install github.com/goreleaser/goreleaser@latest
@$(GOCMD) install github.com/orlangure/gocovsh@latest
dev: build
@./box version
build:
@go build -tags "static_build" \
@$(GOCMD) build -tags "static_build" \
-ldflags "-w \
-X git.mills.io/prologic/box/internal.Version=$(VERSION) \
-X git.mills.io/prologic/box/internal.Commit=$(COMMIT)" \
./cmd/box/...
install: build
@go install .
@$(GOCMD) install .
release:
@./tools/release.sh
test:
@CGO_ENABLED=1 go test -v -cover -race ./...
@CGO_ENABLED=1 $(GOCMD) test -v \
-race -cover \
-coverpkg ./... \
-coverprofile coverage.out \
./...
conver: test
@gocovsh
clean:
@git clean -f -d -X

@ -72,29 +72,22 @@ func isMounted(path string) (bool, error) {
return ok, nil
}
var Fix = Check
func Check() error {
if _, err := os.Stat(procCgroupsPath); err != nil && os.IsNotExist(err) {
return fmt.Errorf("error cgroups not found: %w", err)
}
if stat, err := os.Stat(cgroupsPath); os.IsNotExist(err) || !stat.IsDir() {
if err := os.MkdirAll(cgroupsPath, 0755); err != nil {
return fmt.Errorf("error creating cgroup mountpoint: %w", err)
}
return fmt.Errorf("error cgroups not found: %w", err)
}
mounted, err := isMounted(cgroupsPath)
isMounted, err := isMounted(cgroupsPath)
if err != nil {
return fmt.Errorf("error checking if cgroup system is mounted: %w", err)
return fmt.Errorf("error checking if cgroups is mounted: %w", err)
}
if !mounted {
// mount -t cgroup2 -o nosuid,nodev,noexec,relatime,nsdelegate,memory_recursiveprot cgroup2 /sys/fs/cgorup
if err := unix.Mount("cgroup", cgroupsPath, "cgroup2", unix.MS_NOSUID|unix.MS_NODEV|unix.MS_NOEXEC|unix.MS_RELATIME, "nsdelegate,memory_recursiveprot"); err != nil {
return fmt.Errorf("error mounting cgroup system: %w", err)
}
if !isMounted {
return fmt.Errorf("error cgroups is not mounted")
}
if Mode() != Unified {
@ -103,3 +96,16 @@ func Check() error {
return nil
}
func Fix() error {
if err := os.MkdirAll(cgroupsPath, 0755); err != nil {
return fmt.Errorf("error creating cgroups mountpoint: %w", err)
}
// mount -t cgroup2 -o nosuid,nodev,noexec,relatime,nsdelegate,memory_recursiveprot cgroup2 /sys/fs/cgorup
if err := unix.Mount("cgroup", cgroupsPath, "cgroup2", unix.MS_NOSUID|unix.MS_NODEV|unix.MS_NOEXEC|unix.MS_RELATIME, "nsdelegate,memory_recursiveprot"); err != nil {
return fmt.Errorf("error mounting cgroup system: %w", err)
}
return nil
}

@ -1,4 +1,4 @@
package main
package cli
import (
"github.com/spf13/cobra"

@ -1,4 +1,4 @@
package main
package cli
import (
"github.com/spf13/cobra"

@ -1,4 +1,4 @@
package main
package cli
import (
"git.mills.io/prologic/box/internal"

@ -1,4 +1,4 @@
package main
package cli
import (
"github.com/spf13/cobra"

@ -1,4 +1,4 @@
package main
package cli
import (
"os"

@ -0,0 +1,95 @@
package cli
import (
"errors"
"io"
"os"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
const (
imagesPath = "/var/lib/box/images"
containersPath = "/var/lib/box/containers"
netnsPath = "/var/lib/box/netns"
)
var ErrNotPermitted = errors.New("operation not permitted")
// Make box directories first.
// TODO: Refactor this out of init()
// TODO: Add a -R/--root-dir persistent flag
func init() {
os.MkdirAll(netnsPath, 0700)
os.MkdirAll(imagesPath, 0700)
os.MkdirAll(containersPath, 0700)
}
// NewBoxCommand returns the root cobra.Command for box.
func NewBoxCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "box [OPTIONS] COMMAND",
Short: "A tiny tool for managing containers and sandbox processes",
TraverseChildren: true,
DisableFlagsInUseLine: true,
PersistentPreRun: func(cmd *cobra.Command, args []string) {
// set logging level
isDebug, _ := cmd.Flags().GetBool("debug")
if isDebug {
log.SetLevel(log.DebugLevel)
} else {
log.SetLevel(log.InfoLevel)
}
},
}
cmd.PersistentFlags().BoolP("help", "", false, "display this help and exit")
cmd.PersistentFlags().BoolP(
"debug", "D", false,
"Enable debug logging",
)
cmd.AddCommand(NewDoctorCommand())
cmd.AddCommand(NewRunCommand())
cmd.AddCommand(NewInitCommand())
cmd.AddCommand(NewExecCommand())
cmd.AddCommand(NewPsCommand())
cmd.AddCommand(NewImagesCommand())
cmd.AddCommand(NewVersionCommand())
cmd.AddCommand(NewPullCommand())
cmd.AddCommand(NewRmCommand())
cmd.AddCommand(NewStopCommand())
cmd.AddCommand(NewRmiCommand())
cmd.AddCommand(NewBuildCommand())
return cmd
}
// isRoot implements a cobra acceptable function and
// returns ErrNotPermitted if user is not root.
func isRoot(_ *cobra.Command, _ []string) error {
if os.Geteuid() != 0 {
return ErrNotPermitted
}
return nil
}
// Main is the main entrypoint to our Grreeting CLI used by the actual main
// program as a wrapper to faviliate integration testing
func Main(w io.Writer, args []string) error {
return NewBoxCommand().Execute()
}
// Run runs the Main entrypoint and returns 1 on error or 0 on success,
// this is mostly useful to faciliate e2e integration testing of the CLI.
// It passes `os.Stdout` as the output `io.Writer` and `os.Args[:]` as the
// args passed to `Main`.
func Run() int {
if err := Main(os.Stdout, os.Args[:]); err != nil {
log.Printf("error running main: %s", err)
return 1
}
return 0
}

@ -1,4 +1,4 @@
package main
package cli
import (
"git.mills.io/prologic/box/internal"

@ -1,4 +1,4 @@
package main
package cli
import (
"git.mills.io/prologic/box/internal"

@ -1,4 +1,4 @@
package main
package cli
import (
"git.mills.io/prologic/box/internal"

@ -1,4 +1,4 @@
package main
package cli
import (
"github.com/spf13/cobra"

@ -1,4 +1,4 @@
package main
package cli
import (
"github.com/spf13/cobra"

@ -1,4 +1,4 @@
package main
package cli
import (
"git.mills.io/prologic/box/internal"

@ -1,4 +1,4 @@
package main
package cli
import (
"github.com/spf13/cobra"

@ -1,80 +1,15 @@
package main
import (
"errors"
"fmt"
"os"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"git.mills.io/prologic/box/cli"
)
const (
imagesPath = "/var/lib/box/images"
containersPath = "/var/lib/box/containers"
netnsPath = "/var/lib/box/netns"
)
var ErrNotPermitted = errors.New("operation not permitted")
// Make box directories first.
// TODO: Refactor this out of init()
// TODO: Add a -R/--root-dir persistent flag
func init() {
os.MkdirAll(netnsPath, 0700)
os.MkdirAll(imagesPath, 0700)
os.MkdirAll(containersPath, 0700)
}
// NewBoxCommand returns the root cobra.Command for box.
func NewBoxCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "box [OPTIONS] COMMAND",
Short: "A tiny tool for managing containers and sandbox processes",
TraverseChildren: true,
DisableFlagsInUseLine: true,
PersistentPreRun: func(cmd *cobra.Command, args []string) {
// set logging level
isDebug, _ := cmd.Flags().GetBool("debug")
if isDebug {
log.SetLevel(log.DebugLevel)
} else {
log.SetLevel(log.InfoLevel)
}
},
}
cmd.PersistentFlags().BoolP("help", "", false, "display this help and exit")
cmd.PersistentFlags().BoolP(
"debug", "D", false,
"Enable debug logging",
)
cmd.AddCommand(NewDoctorCommand())
cmd.AddCommand(NewRunCommand())
cmd.AddCommand(NewInitCommand())
cmd.AddCommand(NewExecCommand())
cmd.AddCommand(NewPsCommand())
cmd.AddCommand(NewImagesCommand())
cmd.AddCommand(NewVersionCommand())
cmd.AddCommand(NewPullCommand())
cmd.AddCommand(NewRmCommand())
cmd.AddCommand(NewStopCommand())
cmd.AddCommand(NewRmiCommand())
cmd.AddCommand(NewBuildCommand())
return cmd
}
// isRoot implements a cobra acceptable function and
// returns ErrNotPermitted if user is not root.
func isRoot(_ *cobra.Command, _ []string) error {
if os.Geteuid() != 0 {
return ErrNotPermitted
}
return nil
}
func main() {
NewBoxCommand().Execute()
if err := cli.Main(os.Stdout, os.Args[:]); err != nil {
fmt.Fprintf(os.Stderr, "error: %s\n", err)
os.Exit(1)
}
}

@ -1,6 +1,6 @@
module git.mills.io/prologic/box
go 1.15
go 1.16
require (
github.com/cyphar/filepath-securejoin v0.2.2
@ -8,6 +8,7 @@ require (
github.com/docker/distribution v2.7.1+incompatible
github.com/docker/docker v20.10.0-beta1.0.20201110211921-af34b94a78a1+incompatible // indirect
github.com/docker/go-units v0.4.0
github.com/google/go-cmdtest v0.4.0
github.com/google/go-containerregistry v0.1.4
github.com/google/nftables v0.0.0-20220906152720-cbeb0fb1eccf
github.com/kylelemons/godebug v1.1.0 // indirect

@ -190,6 +190,8 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmdtest v0.4.0 h1:ToXh6W5spLp3npJV92tk6d5hIpUPYEzHLkD+rncbyhI=
github.com/google/go-cmdtest v0.4.0/go.mod h1:apVn/GCasLZUVpAJ6oWAuyP7Ne7CEsQbTnc0plM3m+o=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
@ -213,6 +215,7 @@ github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OI
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=

@ -0,0 +1,140 @@
#!/bin/sh
set -e
color() {
fg="$1"
bg="${2}"
ft="${3:-0}"
printf "\33[%s;%s;%s" "$ft" "$fg" "$bg"
}
color_reset() {
printf "\033[0m"
}
ok() {
if [ -t 1 ]; then
printf "%s[ OK ]%s\n" "$(color 37 42m 1)" "$(color_reset)"
else
printf "%s\n" "[ OK ]"
fi
}
err() {
if [ -t 1 ]; then
printf "%s[ ERR ]%s\n" "$(color 37 41m 1)" "$(color_reset)"
else
printf "%s\n" "[ ERR ]"
fi
}
run() {
retval=0
logfile="$(mktemp -t "run-XXXXXX")"
if "$@" 2> "$logfile"; then
ok
else
retval=$?
err
cat "$logfile" || true
fi
rm -rf "$logfile"
return $retval
}
progress() {
printf "%-40s" "$(printf "%s ... " "$1")"
}
log() {
printf "%s\n" "$1"
}
log2() {
printf "%s\n" "$1" 1>&2
}
error() {
log "ERROR: ${1}"
}
fail() {
log "FATAL: ${1}"
exit 1
}
check_goversion() {
progress "Checking Go version"
if ! command -v go > /dev/null 2>&1; then
log2 "Cannot find the Go compiler"
return 1
fi
gover="$(go version | grep -o -E 'go[0-9]+\.[0-9]+(\.[0-9]+)?')"
if ! go version | grep -E 'go1\.1[6789](\.[0-9]+)?' > /dev/null; then
log2 "Go 1.16+ is required, found ${gover}"
return 1
fi
return 0
}
check_path() {
progress "Checking \$PATH"
gobin="$(eval "$(go env | grep GOBIN)")"
gopath="$(eval "$(go env | grep GOPATH)")"
if [ -n "$gobin" ] && ! echo "$PATH" | grep "$gobin" > /dev/null; then
log2 "\$GOBIN '$gobin' is not in your \$PATH"
return 1
fi
if [ -n "$gopath" ] && ! echo "$PATH" | grep "$gopath/bin" > /dev/null; then
log2 "\$GOPATH/bin '$gopath/bin' is not in your \$PATH"
return 1
fi
if ! echo "$PATH" | grep "$HOME/go/bin" > /dev/null; then
log2 "\$HOME/go/bin is not in your \$PATH"
return 1
fi
return 0
}
check_deps() {
progress "Checking deps"
if ! command -v goreleaser > /dev/null 2>&1; then
log2 "minify not found, Try running: make deps"
return 1
fi
if ! command -v gocovsh > /dev/null 2>&1; then
log2 "goi18n not found, Try running: make deps"
return 1
fi
return 0
}
steps="check_goversion check_path check_deps"
_main() {
for step in $steps; do
if ! run "$step"; then
fail "🙁 preflight failed"
fi
done
log "🥳 All Done! Ready to build, run: make build"
}
if [ -n "$0" ] && [ x"$0" != x"-bash" ]; then
_main "$@"
fi

@ -0,0 +1,29 @@
package tests
import (
"flag"
"testing"
"github.com/google/go-cmdtest"
"git.mills.io/prologic/box/cgroups"
"git.mills.io/prologic/box/cli"
)
const binaryName = "box"
var update = flag.Bool("update", false, "update test files with results")
func TestCLI(t *testing.T) {
if err := cgroups.Check(); err != nil {
t.Skip("cgroups not supported on this system, skipping cli integration tests")
return
}
ts, err := cmdtest.Read("testdata")
if err != nil {
t.Fatal(err)
}
ts.Commands[binaryName] = cmdtest.InProcessProgram(binaryName, cli.Run)
ts.Run(t, *update)
}

@ -0,0 +1,53 @@
# Test the -h/--help flag
$ box --help
A tiny tool for managing containers and sandbox processes
Usage:
box [command]
Available Commands:
build Builds an image
doctor Perform self-diagnoseis and attempt to fix missing configuration
exec Run a command inside a existing Container.
help Help about any command
images List local images
ps List Containers
pull Pulls an OCI compatible image from a registry
rm Removes a Container.
rmi Remove a local image
run Run a command inside a new Container.
stop Stops a Container.
version Display the version of box and exit
Flags:
-D, --debug Enable debug logging
--help display this help and exit
Use "box [command] --help" for more information about a command.
# Verify the default behaviour with no command provided
$ box
A tiny tool for managing containers and sandbox processes
Usage:
box [command]
Available Commands:
build Builds an image
doctor Perform self-diagnoseis and attempt to fix missing configuration
exec Run a command inside a existing Container.
help Help about any command
images List local images
ps List Containers
pull Pulls an OCI compatible image from a registry
rm Removes a Container.
rmi Remove a local image
run Run a command inside a new Container.
stop Stops a Container.
version Display the version of box and exit
Flags:
-D, --debug Enable debug logging
--help display this help and exit
Use "box [command] --help" for more information about a command.
Loading…
Cancel
Save