feat: Add compile and run commands for RUNC matrices
This commit introduces two new commands to the `borg` CLI: - `borg compile`: Compiles a `Borgfile` into a "Terminal Isolation Matrix" (`.matrix` file). The `Borgfile` format is a simple text file with `ADD <src> <dest>` instructions. - `borg run`: Executes a `.matrix` file using `runc`. The command unpacks the matrix into a temporary directory and then executes `runc run`. Tests have been added for both commands, mocking the `runc` execution to avoid environment dependencies.
This commit is contained in:
parent
4529aba089
commit
03337982dd
9 changed files with 333 additions and 13 deletions
63
cmd/compile.go
Normal file
63
cmd/compile.go
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/Snider/Borg/pkg/matrix"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var borgfile string
|
||||
var output string
|
||||
|
||||
var compileCmd = &cobra.Command{
|
||||
Use: "compile",
|
||||
Short: "Compile a Borgfile into a Terminal Isolation Matrix.",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
content, err := os.ReadFile(borgfile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m, err := matrix.New()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
lines := strings.Split(string(content), "\n")
|
||||
for _, line := range lines {
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) == 0 {
|
||||
continue
|
||||
}
|
||||
switch parts[0] {
|
||||
case "ADD":
|
||||
if len(parts) != 3 {
|
||||
return fmt.Errorf("invalid ADD instruction: %s", line)
|
||||
}
|
||||
src := parts[1]
|
||||
dest := parts[2]
|
||||
data, err := os.ReadFile(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.RootFS.AddData(dest, data)
|
||||
}
|
||||
}
|
||||
|
||||
tarball, err := m.ToTar()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(output, tarball, 0644)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
RootCmd.AddCommand(compileCmd)
|
||||
compileCmd.Flags().StringVarP(&borgfile, "file", "f", "Borgfile", "Path to the Borgfile.")
|
||||
compileCmd.Flags().StringVarP(&output, "output", "o", "a.matrix", "Path to the output matrix file.")
|
||||
}
|
||||
77
cmd/compile_test.go
Normal file
77
cmd/compile_test.go
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCompileCmd_Good(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
borgfilePath := filepath.Join(tempDir, "Borgfile")
|
||||
outputMatrixPath := filepath.Join(tempDir, "test.matrix")
|
||||
fileToAddPath := filepath.Join(tempDir, "test.txt")
|
||||
|
||||
// Create a dummy file to add to the matrix.
|
||||
err := os.WriteFile(fileToAddPath, []byte("hello world"), 0644)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create test file: %v", err)
|
||||
}
|
||||
|
||||
// Create a dummy Borgfile.
|
||||
borgfileContent := "ADD " + fileToAddPath + " /test.txt"
|
||||
err = os.WriteFile(borgfilePath, []byte(borgfileContent), 0644)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create Borgfile: %v", err)
|
||||
}
|
||||
|
||||
// Run the compile command.
|
||||
rootCmd := NewRootCmd()
|
||||
rootCmd.AddCommand(compileCmd)
|
||||
_, err = executeCommand(rootCmd, "compile", "-f", borgfilePath, "-o", outputMatrixPath)
|
||||
if err != nil {
|
||||
t.Fatalf("compile command failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify the output matrix file.
|
||||
matrixFile, err := os.Open(outputMatrixPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to open output matrix file: %v", err)
|
||||
}
|
||||
defer matrixFile.Close()
|
||||
|
||||
tr := tar.NewReader(matrixFile)
|
||||
foundConfig := false
|
||||
foundRootFS := false
|
||||
foundTestFile := false
|
||||
for {
|
||||
header, err := tr.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read tar header: %v", err)
|
||||
}
|
||||
|
||||
switch header.Name {
|
||||
case "config.json":
|
||||
foundConfig = true
|
||||
case "rootfs/":
|
||||
foundRootFS = true
|
||||
case "rootfs/test.txt":
|
||||
foundTestFile = true
|
||||
}
|
||||
}
|
||||
|
||||
if !foundConfig {
|
||||
t.Error("config.json not found in matrix")
|
||||
}
|
||||
if !foundRootFS {
|
||||
t.Error("rootfs/ not found in matrix")
|
||||
}
|
||||
if !foundTestFile {
|
||||
t.Error("rootfs/test.txt not found in matrix")
|
||||
}
|
||||
}
|
||||
5
cmd/exec.go
Normal file
5
cmd/exec.go
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
package cmd
|
||||
|
||||
import "os/exec"
|
||||
|
||||
var execCommand = exec.Command
|
||||
25
cmd/main_test.go
Normal file
25
cmd/main_test.go
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// executeCommand is a helper function to execute a cobra command and return the output.
|
||||
func executeCommand(root *cobra.Command, args ...string) (string, error) {
|
||||
_, output, err := executeCommandC(root, args...)
|
||||
return output, err
|
||||
}
|
||||
|
||||
// executeCommandC is a helper function to execute a cobra command and return the output.
|
||||
func executeCommandC(root *cobra.Command, args ...string) (*cobra.Command, string, error) {
|
||||
buf := new(bytes.Buffer)
|
||||
root.SetOut(buf)
|
||||
root.SetErr(buf)
|
||||
root.SetArgs(args)
|
||||
|
||||
c, err := root.ExecuteC()
|
||||
|
||||
return c, buf.String(), err
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"log/slog"
|
||||
"testing"
|
||||
|
|
@ -16,15 +15,6 @@ func TestExecute(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func executeCommand(cmd *cobra.Command, args ...string) (string, error) {
|
||||
buf := new(bytes.Buffer)
|
||||
cmd.SetOut(buf)
|
||||
cmd.SetErr(buf)
|
||||
cmd.SetArgs(args)
|
||||
|
||||
err := cmd.Execute()
|
||||
return buf.String(), err
|
||||
}
|
||||
|
||||
func Test_NewRootCmd(t *testing.T) {
|
||||
if NewRootCmd() == nil {
|
||||
|
|
|
|||
73
cmd/run.go
Normal file
73
cmd/run.go
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var runCmd = &cobra.Command{
|
||||
Use: "run [matrix file]",
|
||||
Short: "Run a Terminal Isolation Matrix.",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
matrixFile := args[0]
|
||||
|
||||
// Create a temporary directory to unpack the matrix file.
|
||||
tempDir, err := os.MkdirTemp("", "borg-run-*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Unpack the matrix file.
|
||||
file, err := os.Open(matrixFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
tr := tar.NewReader(file)
|
||||
for {
|
||||
header, err := tr.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
path := filepath.Join(tempDir, header.Name)
|
||||
if header.Typeflag == tar.TypeDir {
|
||||
if err := os.MkdirAll(path, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
outFile, err := os.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer outFile.Close()
|
||||
if _, err := io.Copy(outFile, tr); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Run the matrix.
|
||||
runc := execCommand("runc", "run", "borg-container")
|
||||
runc.Dir = tempDir
|
||||
runc.Stdout = os.Stdout
|
||||
runc.Stderr = os.Stderr
|
||||
runc.Stdin = os.Stdin
|
||||
return runc.Run()
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
RootCmd.AddCommand(runCmd)
|
||||
}
|
||||
77
cmd/run_test.go
Normal file
77
cmd/run_test.go
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/Snider/Borg/pkg/matrix"
|
||||
)
|
||||
|
||||
func helperProcess(command string, args ...string) *exec.Cmd {
|
||||
cs := []string{"-test.run=TestHelperProcess", "--", command}
|
||||
cs = append(cs, args...)
|
||||
cmd := exec.Command(os.Args[0], cs...)
|
||||
cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"}
|
||||
return cmd
|
||||
}
|
||||
|
||||
func TestHelperProcess(t *testing.T) {
|
||||
if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
|
||||
return
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func TestRunCmd_Good(t *testing.T) {
|
||||
// Create a dummy matrix file.
|
||||
tempDir := t.TempDir()
|
||||
matrixPath := filepath.Join(tempDir, "test.matrix")
|
||||
matrixFile, err := os.Create(matrixPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create dummy matrix file: %v", err)
|
||||
}
|
||||
defer matrixFile.Close()
|
||||
|
||||
tw := tar.NewWriter(matrixFile)
|
||||
// Add a dummy config.json.
|
||||
configContent := []byte(matrix.DefaultConfigJSON)
|
||||
hdr := &tar.Header{
|
||||
Name: "config.json",
|
||||
Mode: 0600,
|
||||
Size: int64(len(configContent)),
|
||||
}
|
||||
if err := tw.WriteHeader(hdr); err != nil {
|
||||
t.Fatalf("failed to write tar header: %v", err)
|
||||
}
|
||||
if _, err := tw.Write(configContent); err != nil {
|
||||
t.Fatalf("failed to write tar content: %v", err)
|
||||
}
|
||||
|
||||
// Add the rootfs directory.
|
||||
hdr = &tar.Header{
|
||||
Name: "rootfs/",
|
||||
Mode: 0755,
|
||||
Typeflag: tar.TypeDir,
|
||||
}
|
||||
if err := tw.WriteHeader(hdr); err != nil {
|
||||
t.Fatalf("failed to write tar header: %v", err)
|
||||
}
|
||||
|
||||
if err := tw.Close(); err != nil {
|
||||
t.Fatalf("failed to close tar writer: %v", err)
|
||||
}
|
||||
|
||||
// Mock the exec.Command function.
|
||||
execCommand = helperProcess
|
||||
|
||||
// Run the run command.
|
||||
rootCmd := NewRootCmd()
|
||||
rootCmd.AddCommand(runCmd)
|
||||
_, err = executeCommand(rootCmd, "run", matrixPath)
|
||||
if err != nil {
|
||||
t.Fatalf("run command failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@ import (
|
|||
)
|
||||
|
||||
// This is the default runc spec, generated by `runc spec`.
|
||||
const defaultConfigJSON = `{
|
||||
const DefaultConfigJSON = `{
|
||||
"ociVersion": "1.2.1",
|
||||
"process": {
|
||||
"terminal": true,
|
||||
|
|
@ -79,7 +79,7 @@ const defaultConfigJSON = `{
|
|||
"newinstance",
|
||||
"ptmxmode=0666",
|
||||
"mode=0620",
|
||||
"gid":5
|
||||
"gid=5"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
@ -182,7 +182,7 @@ const defaultConfigJSON = `{
|
|||
// defaultConfig returns the default runc spec.
|
||||
func defaultConfig() (map[string]interface{}, error) {
|
||||
var spec map[string]interface{}
|
||||
err := json.Unmarshal([]byte(defaultConfigJSON), &spec)
|
||||
err := json.Unmarshal([]byte(DefaultConfigJSON), &spec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,6 +63,16 @@ func (m *TerminalIsolationMatrix) ToTar() ([]byte, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
// Add the rootfs directory.
|
||||
hdr = &tar.Header{
|
||||
Name: "rootfs/",
|
||||
Mode: 0755,
|
||||
Typeflag: tar.TypeDir,
|
||||
}
|
||||
if err := tw.WriteHeader(hdr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Add the rootfs files.
|
||||
err := m.RootFS.Walk(".", func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue