Skip to content

Commit

Permalink
Merge pull request #5 from kubefirst/cexec2
Browse files Browse the repository at this point in the history
cexec2 removing scratch
  • Loading branch information
johndietz authored Apr 10, 2024
2 parents d590730 + 3caa206 commit aaeeeb5
Show file tree
Hide file tree
Showing 3 changed files with 212 additions and 0 deletions.
13 changes: 13 additions & 0 deletions cexec2/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# syntax=docker/dockerfile:1

FROM golang:1.21-alpine as cexec
RUN apk add --no-cache git ca-certificates gcc linux-headers musl-dev
COPY . /src
WORKDIR /src/cexec
RUN --mount=type=cache,sharing=locked,id=gomod,target=/go/pkg/mod/cache \
--mount=type=cache,sharing=locked,id=goroot,target=/root/.cache/go-build \
CGO_ENABLED=1 GOOS=linux go build -a -ldflags "-linkmode external -extldflags '-static' -s -w" -o cexec

FROM golang:1.21-alpine
COPY --from=cexec /src/cexec/cexec /usr/bin/cexec
ENTRYPOINT ["/usr/bin/cexec"]
37 changes: 37 additions & 0 deletions cexec2/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
```
cexec (update docs)
```

The `cexec` action performs *execution* either within a [chroot](https://en.wikipedia.org/wiki/Chroot) environment
or within the base filesystem. The primary use-case is being able to provision
files/an Operating System to disk and then being able to execute something that
perhaps resides within that filesystem.

```yaml
actions:
- name: "Install Grub"
image: quay.io/tinkerbell/actions/cexec:latest
timeout: 90
environment:
BLOCK_DEVICE: /dev/sda3
FS_TYPE: ext4
CHROOT: y
CMD_LINE: "grub-install --root-directory=/boot /dev/sda"
```
In order to execute multiple commands (seperated by a semi-colon) we will
need to leverage a shell. We do this by passing `sh -c` as a `DEFAULT_INTERPRETER`.
This interpreter will then parse your commands.

```yaml
actions:
- name: "Update packages"
image: quay.io/tinkerbell/actions/cexec:latest
timeout: 90
environment:
BLOCK_DEVICE: /dev/sda3
FS_TYPE: ext4
CHROOT: y
DEFAULT_INTERPRETER: "/bin/sh -c"
CMD_LINE: "apt-get -y update; apt-get -y upgrade"
```
162 changes: 162 additions & 0 deletions cexec2/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package main

import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"

log "github.com/sirupsen/logrus"
)

const mountAction = "/mountAction"

func main() {
fmt.Printf("CEXEC - Chroot Exec\n------------------------\n")

// Parse the environment variables that are passed into the action
blockDevice := os.Getenv("BLOCK_DEVICE")
filesystemType := os.Getenv("FS_TYPE")
chroot := os.Getenv("CHROOT")
defaultInterpreter := os.Getenv("DEFAULT_INTERPRETER")
cmdLine := os.Getenv("CMD_LINE")

var exitChroot func() error

if blockDevice == "" {
log.Fatalf("No Block Device speified with Environment Variable [BLOCK_DEVICE]")
}

// Create the /mountAction mountpoint (no folders exist previously in scratch container)
err := os.Mkdir(mountAction, os.ModeDir)
if err != nil {
log.Fatalf("Error creating the action Mountpoint [%s]", mountAction)
}

// Mount the block device to the /mountAction point
err = syscall.Mount(blockDevice, mountAction, filesystemType, 0, "")
if err != nil {
log.Fatalf("Mounting [%s] -> [%s] error [%v]", blockDevice, mountAction, err)
}
log.Infof("Mounted [%s] -> [%s]", blockDevice, mountAction)

if chroot != "" {
err = MountSpecialDirs()
if err != nil {
log.Fatal(err)
}
log.Infoln("Changing root before executing command")
exitChroot, err = Chroot(mountAction)
if err != nil {
log.Fatalf("Error changing root to [%s]", mountAction)
}
}

if defaultInterpreter != "" {
// Split the interpreter by space, in the event that the default intprepretter has flags.
di := strings.Split(defaultInterpreter, " ")
if len(di) == 0 {
log.Fatalf("Error parsing [\"DEFAULT_INTERPETER\"] [%s]\n", defaultInterpreter)
}
// Look for default shell intepreter
_, err = os.Stat(di[0])
if os.IsNotExist(err) {
log.Fatalf("Unable to find the [\"DEFAULT_INTERPETER\"] [%s], check chroot and interpreter path", defaultInterpreter)
}
di = append(di, cmdLine)
cmd := exec.Command(di[0], di[1:]...)
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
debugCMD := fmt.Sprintf("%s %v", di[0], di[1:])
err = cmd.Start()
if err != nil {
log.Fatalf("Error starting [%s] [%v]", debugCMD, err)
}
err = cmd.Wait()
if err != nil {
log.Fatalf("Error running [%s] [%v]", debugCMD, err)
}
} else {
// Format the cmdLine string into separate execution tasks
commandLines := strings.Split(cmdLine, ";")
for x := range commandLines {
command := strings.Split(commandLines[x], " ")
cmd := exec.Command(command[0], command[1:]...)
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
debugCMD := fmt.Sprintf("%s %v", command[0], command[1:])
err = cmd.Start()
if err != nil {
log.Fatalf("Error starting [%s] [%v]", debugCMD, err)
}
err = cmd.Wait()
if err != nil {
log.Fatalf("Error running [%s] [%v]", debugCMD, err)
}
}
}

if chroot != "" {
err = exitChroot()
if err != nil {
log.Errorf("Error exiting root from [%s], execution continuing", mountAction)
}
}
}

// Chroot handles changing the root, and returning a function to return back to the present directory.
func Chroot(path string) (func() error, error) {
cwd, err := os.Getwd()
if err != nil {
return nil, err
}
root, err := os.Open(cwd)
if err != nil {
return nil, err
}

if err := syscall.Chroot(path); err != nil {
root.Close()
return nil, err
}

// set the working directory inside container
if err := syscall.Chdir("/"); err != nil {
root.Close()
return nil, err
}

return func() error {
defer root.Close()
if err := root.Chdir(); err != nil {
return err
}
return syscall.Chroot(".")
}, nil
}

// MountSpecialDirs ensures that /dev /proc /sys exist in the chroot.
func MountSpecialDirs() error {
// Mount dev
dev := filepath.Join(mountAction, "dev")

if err := syscall.Mount("none", dev, "devtmpfs", syscall.MS_RDONLY, ""); err != nil {
return fmt.Errorf("couldn't mount /dev to %v: %w", dev, err)
}

// Mount proc
proc := filepath.Join(mountAction, "proc")

if err := syscall.Mount("none", proc, "proc", syscall.MS_RDONLY, ""); err != nil {
return fmt.Errorf("couldn't mount /proc to %v: %w", proc, err)
}

// Mount sys
sys := filepath.Join(mountAction, "sys")

if err := syscall.Mount("none", sys, "sysfs", syscall.MS_RDONLY, ""); err != nil {
return fmt.Errorf("couldn't mount /sys to %v: %w", sys, err)
}

return nil
}

0 comments on commit aaeeeb5

Please sign in to comment.