feat: create, mount and cleanup volumes for job containers

This commit is contained in:
Marc 2024-08-11 23:23:41 -04:00
parent c352b05d6c
commit a974c36c74
Signed by: marc
GPG key ID: 048E042F22B5DC79
3 changed files with 209 additions and 2 deletions

View file

@ -10,3 +10,9 @@ The first goal of this project is to reach feature parity with Github Actions's
tracker](./WORKFLOW_SUPPORT.md)).
Once this is achieved, extensions to the format will be considered to enhance the design and fix design flaws of the original spec.
## Testing
Certain tests are gated behind environment flags due to their dependencies (skipped by default):
- Defining `PODMAN_TESTS=1` will include Podman-dependent integration tests for the Podman driver.

View file

@ -4,6 +4,8 @@
package runner
import (
logger "courgette/internal/logging"
"fmt"
"os"
"os/exec"
)
@ -22,8 +24,21 @@ func (d PodmanDriver) Pull(uri string) error {
return nil
}
// Sets up a new container with an attached workspace volume.
//
// The volume created is labeled with `owner=<containerName>` so it
// can be easily collected and cleaned up on stop.
func (d PodmanDriver) Start(uri string, containerName string) error {
cmd := exec.Command("podman", "run", "-td", "--name", containerName, uri)
volumeName := fmt.Sprintf("%s-workspace", containerName)
cmd := exec.Command("podman", "volume", "create", volumeName, "--label", fmt.Sprintf("owner=%s", containerName))
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return err
}
cmd = exec.Command("podman", "run", "-td", "--name", containerName, "-v", fmt.Sprintf("%s:/workspace", volumeName), uri)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
@ -34,8 +49,26 @@ func (d PodmanDriver) Start(uri string, containerName string) error {
return nil
}
// Stops a container and removes any volumes labelled with its name
// as owner.
func (d PodmanDriver) Stop(containerName string) error {
cmd := exec.Command("podman", "rm", "-f", containerName)
cmd := exec.Command("podman", "rm", "-f", "-t", "2", containerName)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return err
}
cmd = exec.Command("podman", "wait", containerName, "--ignore")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
logger.Error("%+v", err)
}
cmd = exec.Command("podman", "volume", "prune", "--filter", fmt.Sprintf("label=owner=%s", containerName), "-f")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

View file

@ -0,0 +1,168 @@
package runner
import (
"encoding/json"
"fmt"
"os"
"os/exec"
"strings"
"testing"
)
const TEST_IMAGE_NAME = "busybox:stable"
type PodmanMountDetails struct {
Name string `json:"Name"`
Destination string `json:"Destination"`
}
type PodmanInspect struct {
Name string `json:"Name"`
ImageName string `json:"ImageName"`
Mounts []PodmanMountDetails `json:"Mounts"`
}
func MustPodman(t *testing.T, testCase func()) {
cmd := exec.Command("podman", "--version")
err := cmd.Run()
if err != nil || cmd.ProcessState.ExitCode() != 0 {
t.Error("Podman is required for Podman driver tests.")
}
if os.Getenv("PODMAN_TESTS") != "1" {
t.Log("Skipping because PODMAN_TESTS is not 1.")
return
}
testCase()
}
func CleanupContainer(name string) {
exec.Command("podman", "rm", "-f", "-t", "1", name).Run()
}
func CleanupVolume(name string) {
exec.Command("podman", "volume", "rm", "-f", name).Run()
}
func CleanAll(name string) {
CleanupContainer(name)
CleanupVolume(fmt.Sprintf("%s-workspace", name))
}
func ContainerExists(name string) bool {
exists := exec.Command("podman", "container", "exists", name)
exists.Run()
exitCode := exists.ProcessState.ExitCode()
return exitCode == 0
}
func VolumeExists(name string) bool {
exists := exec.Command("podman", "volume", "exists", name)
exists.Run()
exitCode := exists.ProcessState.ExitCode()
return exitCode == 0
}
func InspectContainer(name string) PodmanInspect {
inspect := exec.Command("podman", "inspect", name)
out, _ := inspect.Output()
var inspectOut []PodmanInspect
json.Unmarshal(out, &inspectOut)
return inspectOut[0]
}
func TestStartCreatesAContainerWithTheDesiredUriAndName(t *testing.T) {
MustPodman(t, func() {
driver := PodmanDriver{}
containerName := "test-container"
defer CleanAll(containerName)
driver.Start(TEST_IMAGE_NAME, containerName)
inspectOut := InspectContainer(containerName)
if inspectOut.Name != containerName {
t.Errorf("Expected container name to be %s, got %s instead.", containerName, inspectOut.Name)
}
if !strings.HasSuffix(inspectOut.ImageName, TEST_IMAGE_NAME) {
t.Errorf("Expected image name to be %s, got %s instead.", TEST_IMAGE_NAME, inspectOut.ImageName)
}
})
}
func TestStartCreatesAContainerWithAnAttachedVolume(t *testing.T) {
MustPodman(t, func() {
driver := PodmanDriver{}
containerName := "test-container"
defer CleanAll(containerName)
driver.Start(TEST_IMAGE_NAME, containerName)
inspectOut := InspectContainer(containerName)
if len(inspectOut.Mounts) != 1 {
t.Error("Expected a mount.")
}
expectedVolumeName := fmt.Sprintf("%s-workspace", containerName)
if !VolumeExists(expectedVolumeName) {
t.Error("Expected volume.")
}
if inspectOut.Mounts[0].Name != expectedVolumeName {
t.Errorf("Expected volume name to be %s, got %s instead.", expectedVolumeName, inspectOut.Mounts[0].Name)
}
if inspectOut.Mounts[0].Destination != "/workspace" {
t.Errorf("Expected volume mountpoint to be /workspace, got %s instead.", inspectOut.Mounts[0].Destination)
}
})
}
func TestStopStopsAContainerByName(t *testing.T) {
MustPodman(t, func() {
driver := PodmanDriver{}
containerName := "test-container"
defer CleanAll(containerName)
driver.Start(TEST_IMAGE_NAME, containerName)
driver.Stop(containerName)
if ContainerExists(containerName) {
t.Error("Expected container to be cleaned up, still exists.")
}
})
}
func TestStopCleansUpWorkspaceVolume(t *testing.T) {
MustPodman(t, func() {
driver := PodmanDriver{}
containerName := "test-container"
defer CleanAll(containerName)
driver.Start(TEST_IMAGE_NAME, containerName)
driver.Stop(containerName)
if VolumeExists(fmt.Sprintf("%s-workspace", containerName)) {
t.Error("Expected volume to be cleaned up, still exists.")
}
})
}