diff --git a/README.md b/README.md index 3359284..f257939 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/internal/runner/podman_driver.go b/internal/runner/podman_driver.go index b3bed8e..2269da8 100644 --- a/internal/runner/podman_driver.go +++ b/internal/runner/podman_driver.go @@ -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=` 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 diff --git a/internal/runner/podman_driver_test.go b/internal/runner/podman_driver_test.go new file mode 100644 index 0000000..cce5c7b --- /dev/null +++ b/internal/runner/podman_driver_test.go @@ -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.") + } + }) +}