Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
hansmi committed May 27, 2024
0 parents commit f007a0b
Show file tree
Hide file tree
Showing 24 changed files with 1,213 additions and 0 deletions.
14 changes: 14 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
version: 2
updates:
- package-ecosystem: gomod
directory: /
schedule:
interval: monthly

- package-ecosystem: github-actions
directory: /
schedule:
interval: monthly

# vim: set sw=2 sts=2 et :
17 changes: 17 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: Run tests

on:
workflow_dispatch:
pull_request:
push:

permissions:
contents: read

jobs:
test:
uses: hansmi/ghactions-go-test-workflow/.github/workflows/[email protected]
with:
runs-on: ubuntu-latest

# vim: set sw=2 sts=2 et :
17 changes: 17 additions & 0 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: Release

on:
workflow_dispatch:
pull_request:
push:

permissions: {}

jobs:
release:
uses: hansmi/ghactions-goreleaser-workflow/.github/workflows/[email protected]
permissions:
contents: write
packages: write

# vim: set sw=2 sts=2 et :
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/dist/
/cocoon
49 changes: 49 additions & 0 deletions .goreleaser.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Configuration for GoReleaser
# https://goreleaser.com/
#
# Local test: contrib/build-all
#

project_name: cocoon

builds:
- main: .
binary: cocoon
env:
- CGO_ENABLED=0
goos:
- linux
flags:
- -trimpath
ldflags: |
-s -w
nfpms:
- description: Run command in container while preserving local execution environment
maintainer: M. Hanselmann
bindir: /usr/bin
license: BSD-3-Clause
formats:
- deb
- rpm
contents:
- src: ./README.md
dst: /usr/share/doc/cocoon/README.md
- src: ./LICENSE
dst: /usr/share/doc/cocoon/LICENSE

archives:
- format: tar.gz
wrap_in_directory: true
files:
- LICENSE
- README.md

release:
draft: true
prerelease: auto

snapshot:
name_template: '{{ incpatch .Version }}-snapshot{{ replace (replace .Date ":" "") "-" "" }}+g{{ .ShortCommit }}'

# vim: set sw=2 sts=2 et :
27 changes: 27 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
Copyright (c) 2024 Michael Hanselmann. All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:

* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Run command in container while preserving local execution environment

[![Latest release](https://img.shields.io/github/v/release/hansmi/cocoon)][releases]
[![CI workflow](https://github.com/hansmi/cocoon/actions/workflows/ci.yaml/badge.svg)](https://github.com/hansmi/cocoon/actions/workflows/ci.yaml)
[![Go reference](https://pkg.go.dev/badge/github.com/hansmi/cocoon.svg)](https://pkg.go.dev/github.com/hansmi/cocoon)

Cocoon is command line program for running a command in a Linux container while
having the most important files and directories from the host system
bind-mounted. Environment variables are configurable as well.


## Installation

[Pre-built binaries][releases]:

* Binary archives (`.tar.gz`)
* Debian/Ubuntu (`.deb`)
* RHEL/Fedora (`.rpm`)

It's also possible to build locally using [Go][golang] or
[GoReleaser][goreleaser].


[golang]: https://golang.org/
[goreleaser]: https://goreleaser.com/
[releases]: https://github.com/hansmi/cocoon/releases/latest

<!-- vim: set sw=2 sts=2 et : -->
23 changes: 23 additions & 0 deletions cloexec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package main

import (
"fmt"

"golang.org/x/sys/unix"
)

func clearCloseOnExec(fd uintptr) error {
flags, err := unix.FcntlInt(fd, unix.F_GETFD, 0)
if err != nil {
return fmt.Errorf("getting file descriptor flags: %w", err)
}

if (flags & unix.FD_CLOEXEC) != 0 {
_, err = unix.FcntlInt(fd, unix.F_SETFD, flags & ^unix.FD_CLOEXEC)
if err != nil {
return fmt.Errorf("clearing close-on-exec file descriptor flag: %w", err)
}
}

return nil
}
42 changes: 42 additions & 0 deletions cloexec_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package main

import (
"os"
"testing"

"golang.org/x/sys/unix"
)

func getCloseOnExec(t *testing.T, f *os.File) bool {
t.Helper()

flags, err := unix.FcntlInt(f.Fd(), unix.F_GETFD, 0)
if err != nil {
t.Fatalf("Getting %v file descriptor flags: %v", f, err)
}

return flags&unix.FD_CLOEXEC != 0
}

func TestClearCloseOnExec(t *testing.T) {
tmpdir := t.TempDir()

f, err := os.CreateTemp(tmpdir, "")
if err != nil {
t.Errorf("CreateTemp(%q) failed: %v", tmpdir, err)
}

if !getCloseOnExec(t, f) {
t.Errorf("Expected close-on-exit to be set by default, but it's not on %#v", f)
}

for i := 0; i < 3; i++ {
if err := clearCloseOnExec(f.Fd()); err != nil {
t.Errorf("clearCloseOnExec() failed: %v", err)
}

if getCloseOnExec(t, f) {
t.Error("Expected close-on-exit to be unset")
}
}
}
14 changes: 14 additions & 0 deletions contrib/build-all
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/bin/bash

set -e -u -o pipefail

package=github.com/hansmi/cocoon

docker run --rm \
--user "$(id -u):$(id -g)" \
--env HOME=/tmp \
-v "${PWD}:/go/src/${package}" \
-w "/go/src/${package}" \
goreleaser/goreleaser:latest release --snapshot --clean --skip=publish

# vim: set sw=2 sts=2 et :
125 changes: 125 additions & 0 deletions docker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package main

import (
"errors"
"fmt"
"os"
"os/exec"
"sort"
"strings"

"golang.org/x/exp/maps"
"golang.org/x/sys/unix"
)

var errDockerEnvironNewline = errors.New("newline characters not supported in Docker environment variables")

type dockerEnviron struct {
file *os.File
}

func newDockerEnviron() (*dockerEnviron, error) {
// Create a temporary file without name.
fd, err := unix.Open(os.TempDir(), unix.O_TMPFILE|unix.O_RDWR|unix.O_CLOEXEC, 0o600)
if err != nil {
return nil, fmt.Errorf("creating temporary file: %w", err)
}

return &dockerEnviron{
file: os.NewFile(uintptr(fd), ""),
}, nil
}

func (e *dockerEnviron) add(variable string, value *string) error {
if value != nil && strings.ContainsAny(*value, "\r\n") {
return errDockerEnvironNewline
}

var err error

if value == nil {
// Pass-through variable
_, err = fmt.Fprintf(e.file, "%s\n", variable)
} else {
_, err = fmt.Fprintf(e.file, "%s=%s\n", variable, *value)
}

return err
}

func toDockerEnviron(environ envMap) (*dockerEnviron, error) {
result, err := newDockerEnviron()
if err != nil {
return nil, err
}

variables := maps.Keys(environ)

sort.Strings(variables)

for _, variable := range variables {
if err := result.add(variable, environ[variable]); err != nil {
return nil, fmt.Errorf("%s: %w", variable, err)
}
}

return result, nil
}

func (p *program) toDockerCommand(environ envMap) ([]string, error) {
dockerCli, err := exec.LookPath(p.dockerCliProgram)
if err != nil {
return nil, fmt.Errorf("unable to find Docker CLI: %w", err)
}

entrypoint := p.shell

if len(p.args) > 0 {
entrypoint = p.args[0]
}

args := []string{
dockerCli, "run",

"--entrypoint=" + entrypoint,
"--init",
"--name=" + p.containerName,
"--network=host",
"--pid=host",
"--read-only",
"--rm",
"--user=" + p.user + ":" + p.group,
"--uts=host",
"--workdir=" + p.workdir,

// TODO: Should there be a "--mount-tmpfs" flag?
"--tmpfs=/tmp",
}

args = append(args, p.mounts.toDockerFlags()...)

if p.interactive {
args = append(args, "--interactive", "--tty")
}

if len(environ) > 0 {
env, err := toDockerEnviron(environ)
if err != nil {
return nil, err
}

if err := clearCloseOnExec(env.file.Fd()); err != nil {
return nil, err
}

args = append(args, fmt.Sprintf("--env-file=/dev/fd/%d", env.file.Fd()))
}

args = append(args, p.image)

if len(p.args) > 1 {
args = append(args, p.args[1:]...)
}

return args, nil
}
Loading

0 comments on commit f007a0b

Please sign in to comment.