diff --git a/cmd/compile.go b/cmd/compile.go new file mode 100644 index 0000000..62308e9 --- /dev/null +++ b/cmd/compile.go @@ -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.") +} diff --git a/cmd/compile_test.go b/cmd/compile_test.go new file mode 100644 index 0000000..42c8a93 --- /dev/null +++ b/cmd/compile_test.go @@ -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") + } +} diff --git a/cmd/exec.go b/cmd/exec.go new file mode 100644 index 0000000..91fd126 --- /dev/null +++ b/cmd/exec.go @@ -0,0 +1,5 @@ +package cmd + +import "os/exec" + +var execCommand = exec.Command diff --git a/cmd/main_test.go b/cmd/main_test.go new file mode 100644 index 0000000..70aa8fa --- /dev/null +++ b/cmd/main_test.go @@ -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 +} diff --git a/cmd/root_test.go b/cmd/root_test.go index 34e5b4a..e632e29 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -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 { diff --git a/cmd/run.go b/cmd/run.go new file mode 100644 index 0000000..2044826 --- /dev/null +++ b/cmd/run.go @@ -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) +} diff --git a/cmd/run_test.go b/cmd/run_test.go new file mode 100644 index 0000000..bec33fc --- /dev/null +++ b/cmd/run_test.go @@ -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) + } +} diff --git a/pkg/matrix/config.go b/pkg/matrix/config.go index 3ba19f3..80d9ab5 100644 --- a/pkg/matrix/config.go +++ b/pkg/matrix/config.go @@ -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 } diff --git a/pkg/matrix/matrix.go b/pkg/matrix/matrix.go index 3738577..ef15bec 100644 --- a/pkg/matrix/matrix.go +++ b/pkg/matrix/matrix.go @@ -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 {