Merge pull request #13 from Snider/feat-runc-compile-run
Feat runc compile run
This commit is contained in:
commit
936e2a7134
27 changed files with 811 additions and 208 deletions
|
|
@ -1,110 +0,0 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/Snider/Borg/pkg/mocks"
|
||||
borg_github "github.com/Snider/Borg/pkg/github"
|
||||
"github.com/google/go-github/v39/github"
|
||||
)
|
||||
|
||||
func TestGetRelease_Good(t *testing.T) {
|
||||
// Create a temporary directory for the output
|
||||
dir, err := os.MkdirTemp("", "test-get-release")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
mockClient := mocks.NewMockClient(map[string]*http.Response{
|
||||
"https://api.github.com/repos/owner/repo/releases/latest": {
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(bytes.NewBufferString(`{"tag_name": "v1.0.0", "assets": [{"name": "asset1.zip", "browser_download_url": "https://github.com/owner/repo/releases/download/v1.0.0/asset1.zip"}]}`)),
|
||||
},
|
||||
"https://github.com/owner/repo/releases/download/v1.0.0/asset1.zip": {
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(bytes.NewBufferString("asset content")),
|
||||
},
|
||||
})
|
||||
|
||||
oldNewClient := borg_github.NewClient
|
||||
borg_github.NewClient = func(httpClient *http.Client) *github.Client {
|
||||
return github.NewClient(mockClient)
|
||||
}
|
||||
defer func() {
|
||||
borg_github.NewClient = oldNewClient
|
||||
}()
|
||||
|
||||
oldDefaultClient := borg_github.DefaultClient
|
||||
borg_github.DefaultClient = mockClient
|
||||
defer func() {
|
||||
borg_github.DefaultClient = oldDefaultClient
|
||||
}()
|
||||
|
||||
log := slog.New(slog.NewJSONHandler(io.Discard, nil))
|
||||
|
||||
// Test downloading a single asset
|
||||
_, err = GetRelease(log, "https://github.com/owner/repo", dir, false, "asset1.zip", "")
|
||||
if err != nil {
|
||||
t.Fatalf("GetRelease failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify the asset was downloaded
|
||||
content, err := os.ReadFile(filepath.Join(dir, "asset1.zip"))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read downloaded asset: %v", err)
|
||||
}
|
||||
if string(content) != "asset content" {
|
||||
t.Errorf("unexpected asset content: %s", string(content))
|
||||
}
|
||||
|
||||
// Test packing all assets
|
||||
packedDir := filepath.Join(dir, "packed")
|
||||
_, err = GetRelease(log, "https://github.com/owner/repo", packedDir, true, "", "")
|
||||
if err != nil {
|
||||
t.Fatalf("GetRelease with --pack failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify the datanode was created
|
||||
if _, err := os.Stat(filepath.Join(packedDir, "v1.0.0.dat")); os.IsNotExist(err) {
|
||||
t.Fatalf("datanode not created")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRelease_Bad(t *testing.T) {
|
||||
// Create a temporary directory for the output
|
||||
dir, err := os.MkdirTemp("", "test-get-release")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
mockClient := mocks.NewMockClient(map[string]*http.Response{
|
||||
"https://api.github.com/repos/owner/repo/releases/latest": {
|
||||
StatusCode: http.StatusNotFound,
|
||||
Body: io.NopCloser(bytes.NewBufferString(`{"message": "Not Found"}`)),
|
||||
},
|
||||
})
|
||||
|
||||
oldNewClient := borg_github.NewClient
|
||||
borg_github.NewClient = func(httpClient *http.Client) *github.Client {
|
||||
return github.NewClient(mockClient)
|
||||
}
|
||||
defer func() {
|
||||
borg_github.NewClient = oldNewClient
|
||||
}()
|
||||
|
||||
log := slog.New(slog.NewJSONHandler(io.Discard, nil))
|
||||
|
||||
// Test failed release lookup
|
||||
_, err = GetRelease(log, "https://github.com/owner/repo", dir, false, "", "")
|
||||
if err == nil {
|
||||
t.Fatalf("expected an error, but got none")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/Snider/Borg/pkg/datanode"
|
||||
"github.com/Snider/Borg/pkg/pwa"
|
||||
)
|
||||
|
||||
func TestCollectPWACmd_NoURI(t *testing.T) {
|
||||
rootCmd := NewRootCmd()
|
||||
collectCmd := NewCollectCmd()
|
||||
collectPWACmd := NewCollectPWACmd()
|
||||
collectCmd.AddCommand(&collectPWACmd.Command)
|
||||
rootCmd.AddCommand(collectCmd)
|
||||
_, err := executeCommand(rootCmd, "collect", "pwa")
|
||||
if err == nil {
|
||||
t.Fatalf("expected an error, but got none")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "uri is required") {
|
||||
t.Fatalf("unexpected error message: %v", err)
|
||||
}
|
||||
}
|
||||
func Test_NewCollectPWACmd(t *testing.T) {
|
||||
if NewCollectPWACmd() == nil {
|
||||
t.Errorf("NewCollectPWACmd is nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectPWA_Good(t *testing.T) {
|
||||
mockClient := &pwa.MockPWAClient{
|
||||
ManifestURL: "https://example.com/manifest.json",
|
||||
DN: datanode.New(),
|
||||
Err: nil,
|
||||
}
|
||||
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "pwa.dat")
|
||||
_, err := CollectPWA(mockClient, "https://example.com", path, "datanode", "none")
|
||||
if err != nil {
|
||||
t.Fatalf("CollectPWA failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectPWA_Bad(t *testing.T) {
|
||||
mockClient := &pwa.MockPWAClient{
|
||||
ManifestURL: "",
|
||||
DN: nil,
|
||||
Err: fmt.Errorf("pwa error"),
|
||||
}
|
||||
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "pwa.dat")
|
||||
_, err := CollectPWA(mockClient, "https://example.com", path, "datanode", "none")
|
||||
if err == nil {
|
||||
t.Fatalf("expected an error, but got none")
|
||||
}
|
||||
}
|
||||
65
cmd/compile.go
Normal file
65
cmd/compile.go
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
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)
|
||||
default:
|
||||
return fmt.Errorf("unknown instruction: %s", parts[0])
|
||||
}
|
||||
}
|
||||
|
||||
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.")
|
||||
}
|
||||
119
cmd/compile_test.go
Normal file
119
cmd/compile_test.go
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
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")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompileCmd_Bad_InvalidBorgfile(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
borgfilePath := filepath.Join(tempDir, "Borgfile")
|
||||
outputMatrixPath := filepath.Join(tempDir, "test.matrix")
|
||||
|
||||
// Create a dummy Borgfile with an invalid instruction.
|
||||
borgfileContent := "INVALID_INSTRUCTION"
|
||||
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.Fatal("compile command should have failed but did not")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompileCmd_Bad_MissingInputFile(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
borgfilePath := filepath.Join(tempDir, "Borgfile")
|
||||
outputMatrixPath := filepath.Join(tempDir, "test.matrix")
|
||||
|
||||
// Create a dummy Borgfile that references a non-existent file.
|
||||
borgfileContent := "ADD /non/existent/file /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.Fatal("compile command should have failed but did not")
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
91
cmd/run_test.go
Normal file
91
cmd/run_test.go
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
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.
|
||||
origExecCommand := execCommand
|
||||
execCommand = helperProcess
|
||||
t.Cleanup(func() {
|
||||
execCommand = origExecCommand
|
||||
})
|
||||
|
||||
// Run the run command.
|
||||
rootCmd := NewRootCmd()
|
||||
rootCmd.AddCommand(runCmd)
|
||||
_, err = executeCommand(rootCmd, "run", matrixPath)
|
||||
if err != nil {
|
||||
t.Fatalf("run command failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunCmd_Bad_MissingInputFile(t *testing.T) {
|
||||
// Run the run command with a non-existent file.
|
||||
rootCmd := NewRootCmd()
|
||||
rootCmd.AddCommand(runCmd)
|
||||
_, err := executeCommand(rootCmd, "run", "/non/existent/file.matrix")
|
||||
if err == nil {
|
||||
t.Fatal("run command should have failed but did not")
|
||||
}
|
||||
}
|
||||
18
cmd/serve.go
18
cmd/serve.go
|
|
@ -19,34 +19,30 @@ var serveCmd = &cobra.Command{
|
|||
Short: "Serve a packaged PWA file",
|
||||
Long: `Serves the contents of a packaged PWA file using a static file server.`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
dataFile := args[0]
|
||||
port, _ := cmd.Flags().GetString("port")
|
||||
|
||||
rawData, err := os.ReadFile(dataFile)
|
||||
if err != nil {
|
||||
fmt.Printf("Error reading data file: %v\n", err)
|
||||
return
|
||||
return fmt.Errorf("Error reading data file: %w", err)
|
||||
}
|
||||
|
||||
data, err := compress.Decompress(rawData)
|
||||
if err != nil {
|
||||
fmt.Printf("Error decompressing data: %v\n", err)
|
||||
return
|
||||
return fmt.Errorf("Error decompressing data: %w", err)
|
||||
}
|
||||
|
||||
var fs http.FileSystem
|
||||
if strings.HasSuffix(dataFile, ".matrix") {
|
||||
fs, err = tarfs.New(data)
|
||||
if err != nil {
|
||||
fmt.Printf("Error creating TarFS from matrix tarball: %v\n", err)
|
||||
return
|
||||
return fmt.Errorf("Error creating TarFS from matrix tarball: %w", err)
|
||||
}
|
||||
} else {
|
||||
dn, err := datanode.FromTar(data)
|
||||
if err != nil {
|
||||
fmt.Printf("Error creating DataNode from tarball: %v\n", err)
|
||||
return
|
||||
return fmt.Errorf("Error creating DataNode from tarball: %w", err)
|
||||
}
|
||||
fs = http.FS(dn)
|
||||
}
|
||||
|
|
@ -56,9 +52,9 @@ var serveCmd = &cobra.Command{
|
|||
fmt.Printf("Serving PWA on http://localhost:%s\n", port)
|
||||
err = http.ListenAndServe(":"+port, nil)
|
||||
if err != nil {
|
||||
fmt.Printf("Error starting server: %v\n", err)
|
||||
return
|
||||
return fmt.Errorf("Error starting server: %w", err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
7
examples/all/main.go
Normal file
7
examples/all/main.go
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
func main() {
|
||||
fmt.Println("This is a placeholder for the 'all' example.")
|
||||
}
|
||||
7
examples/collect_github_release/main.go
Normal file
7
examples/collect_github_release/main.go
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
func main() {
|
||||
fmt.Println("This is a placeholder for the 'collect github release' example.")
|
||||
}
|
||||
7
examples/collect_github_repo/main.go
Normal file
7
examples/collect_github_repo/main.go
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
func main() {
|
||||
fmt.Println("This is a placeholder for the 'collect github repo' example.")
|
||||
}
|
||||
7
examples/collect_github_repos/main.go
Normal file
7
examples/collect_github_repos/main.go
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
func main() {
|
||||
fmt.Println("This is a placeholder for the 'collect github repos' example.")
|
||||
}
|
||||
7
examples/collect_pwa/main.go
Normal file
7
examples/collect_pwa/main.go
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
func main() {
|
||||
fmt.Println("This is a placeholder for the 'collect pwa' example.")
|
||||
}
|
||||
7
examples/collect_website/main.go
Normal file
7
examples/collect_website/main.go
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
func main() {
|
||||
fmt.Println("This is a placeholder for the 'collect website' example.")
|
||||
}
|
||||
35
examples/create_matrix_programmatically/main.go
Normal file
35
examples/create_matrix_programmatically/main.go
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/Snider/Borg/pkg/datanode"
|
||||
"github.com/Snider/Borg/pkg/matrix"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Create a new DataNode to hold the root filesystem.
|
||||
dn := datanode.New()
|
||||
dn.AddData("hello.txt", []byte("Hello from within the matrix!"))
|
||||
|
||||
// Create a new TerminalIsolationMatrix from the DataNode.
|
||||
m, err := matrix.FromDataNode(dn)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create matrix: %v", err)
|
||||
}
|
||||
|
||||
// Serialize the matrix to a tarball.
|
||||
tarball, err := m.ToTar()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to serialize matrix to tar: %v", err)
|
||||
}
|
||||
|
||||
// Write the tarball to a file.
|
||||
err = os.WriteFile("programmatic.matrix", tarball, 0644)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to write matrix file: %v", err)
|
||||
}
|
||||
|
||||
log.Println("Successfully created programmatic.matrix")
|
||||
}
|
||||
75
examples/run_matrix_programmatically/main.go
Normal file
75
examples/run_matrix_programmatically/main.go
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Open the matrix file.
|
||||
matrixFile, err := os.Open("programmatic.matrix")
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to open matrix file (run create_matrix_programmatically first): %v", err)
|
||||
}
|
||||
defer matrixFile.Close()
|
||||
|
||||
// Create a temporary directory to unpack the matrix.
|
||||
tempDir, err := os.MkdirTemp("", "borg-run-example-*")
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create temporary directory: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
log.Printf("Unpacking matrix to %s", tempDir)
|
||||
|
||||
// Unpack the tar archive.
|
||||
tr := tar.NewReader(matrixFile)
|
||||
for {
|
||||
header, err := tr.Next()
|
||||
if err == io.EOF {
|
||||
break // End of archive
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to read tar header: %v", err)
|
||||
}
|
||||
|
||||
target := filepath.Join(tempDir, header.Name)
|
||||
|
||||
switch header.Typeflag {
|
||||
case tar.TypeDir:
|
||||
if err := os.MkdirAll(target, 0755); err != nil {
|
||||
log.Fatalf("Failed to create directory: %v", err)
|
||||
}
|
||||
case tar.TypeReg:
|
||||
outFile, err := os.Create(target)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create file: %v", err)
|
||||
}
|
||||
if _, err := io.Copy(outFile, tr); err != nil {
|
||||
log.Fatalf("Failed to write file content: %v", err)
|
||||
}
|
||||
outFile.Close()
|
||||
default:
|
||||
log.Printf("Skipping unsupported type: %c in %s", header.Typeflag, header.Name)
|
||||
}
|
||||
}
|
||||
|
||||
log.Println("Executing matrix with runc...")
|
||||
|
||||
// Execute the matrix using runc.
|
||||
cmd := exec.Command("runc", "run", "borg-container-example")
|
||||
cmd.Dir = tempDir
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdin = os.Stdin
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
log.Fatalf("Failed to run matrix: %v", err)
|
||||
}
|
||||
|
||||
log.Println("Matrix execution finished.")
|
||||
}
|
||||
7
examples/serve/main.go
Normal file
7
examples/serve/main.go
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
func main() {
|
||||
fmt.Println("This is a placeholder for the 'serve' example.")
|
||||
}
|
||||
|
|
@ -78,6 +78,14 @@ func (d *DataNode) ToTar() ([]byte, error) {
|
|||
// AddData adds a file to the DataNode.
|
||||
func (d *DataNode) AddData(name string, content []byte) {
|
||||
name = strings.TrimPrefix(name, "/")
|
||||
if name == "" {
|
||||
return
|
||||
}
|
||||
// Directories are implicit, so we don't store them.
|
||||
// A name ending in "/" is treated as a directory.
|
||||
if strings.HasSuffix(name, "/") {
|
||||
return
|
||||
}
|
||||
d.files[name] = &dataFile{
|
||||
name: name,
|
||||
content: content,
|
||||
|
|
@ -223,15 +231,37 @@ func (d *DataNode) Walk(root string, fn fs.WalkDirFunc, opts ...WalkOptions) err
|
|||
return fn(path, de, err)
|
||||
}
|
||||
if filter != nil && !filter(path, de) {
|
||||
if de.IsDir() {
|
||||
return fs.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Process the entry first.
|
||||
if err := fn(path, de, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if maxDepth > 0 {
|
||||
currentDepth := strings.Count(strings.TrimPrefix(path, root), "/")
|
||||
// Calculate depth relative to root
|
||||
cleanedPath := strings.TrimPrefix(path, root)
|
||||
cleanedPath = strings.TrimPrefix(cleanedPath, "/")
|
||||
|
||||
currentDepth := 0
|
||||
if path != root {
|
||||
if cleanedPath == "" {
|
||||
// This can happen if root is "bar" and path is "bar"
|
||||
currentDepth = 0
|
||||
} else {
|
||||
currentDepth = strings.Count(cleanedPath, "/") + 1
|
||||
}
|
||||
}
|
||||
|
||||
if de.IsDir() && currentDepth >= maxDepth {
|
||||
return fs.SkipDir
|
||||
}
|
||||
}
|
||||
return fn(path, de, nil)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
@ -180,9 +180,9 @@ const defaultConfigJSON = `{
|
|||
}`
|
||||
|
||||
// defaultConfig returns the default runc spec.
|
||||
func defaultConfig() (map[string]interface{}, error) {
|
||||
var defaultConfigVar = func() (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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ type TerminalIsolationMatrix struct {
|
|||
func New() (*TerminalIsolationMatrix, error) {
|
||||
// Use the default runc spec as a starting point.
|
||||
// This can be customized later.
|
||||
spec, err := defaultConfig()
|
||||
spec, err := defaultConfigVar()
|
||||
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 {
|
||||
|
|
|
|||
50
pkg/matrix/matrix_more_test.go
Normal file
50
pkg/matrix/matrix_more_test.go
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
package matrix
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/Snider/Borg/pkg/datanode"
|
||||
)
|
||||
|
||||
func TestNew_Error(t *testing.T) {
|
||||
origDefaultConfig := defaultConfigVar
|
||||
t.Cleanup(func() {
|
||||
defaultConfigVar = origDefaultConfig
|
||||
})
|
||||
|
||||
// Test error from defaultConfigVar
|
||||
defaultConfigVar = func() (map[string]interface{}, error) {
|
||||
return nil, errors.New("mock defaultConfig error")
|
||||
}
|
||||
_, err := New()
|
||||
if err == nil {
|
||||
t.Fatal("Expected error from defaultConfig, got nil")
|
||||
}
|
||||
|
||||
// Test error from json.Marshal
|
||||
defaultConfigVar = func() (map[string]interface{}, error) {
|
||||
return map[string]interface{}{"foo": make(chan int)}, nil
|
||||
}
|
||||
_, err = New()
|
||||
if err == nil {
|
||||
t.Fatal("Expected error from json.Marshal, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFromDataNode_Error(t *testing.T) {
|
||||
origDefaultConfig := defaultConfigVar
|
||||
t.Cleanup(func() {
|
||||
defaultConfigVar = origDefaultConfig
|
||||
})
|
||||
|
||||
defaultConfigVar = func() (map[string]interface{}, error) {
|
||||
return nil, errors.New("mock defaultConfig error")
|
||||
}
|
||||
|
||||
dn := datanode.New()
|
||||
_, err := FromDataNode(dn)
|
||||
if err == nil {
|
||||
t.Fatal("Expected error from FromDataNode, got nil")
|
||||
}
|
||||
}
|
||||
89
pkg/matrix/matrix_test.go
Normal file
89
pkg/matrix/matrix_test.go
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
package matrix
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/Snider/Borg/pkg/datanode"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
m, err := New()
|
||||
if err != nil {
|
||||
t.Fatalf("New() returned an error: %v", err)
|
||||
}
|
||||
if m == nil {
|
||||
t.Fatal("New() returned a nil matrix")
|
||||
}
|
||||
if m.Config == nil {
|
||||
t.Error("New() returned a matrix with a nil config")
|
||||
}
|
||||
if m.RootFS == nil {
|
||||
t.Error("New() returned a matrix with a nil RootFS")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFromDataNode(t *testing.T) {
|
||||
dn := datanode.New()
|
||||
dn.AddData("test.txt", []byte("hello world"))
|
||||
m, err := FromDataNode(dn)
|
||||
if err != nil {
|
||||
t.Fatalf("FromDataNode() returned an error: %v", err)
|
||||
}
|
||||
if m == nil {
|
||||
t.Fatal("FromDataNode() returned a nil matrix")
|
||||
}
|
||||
if m.RootFS != dn {
|
||||
t.Error("FromDataNode() did not set the RootFS correctly")
|
||||
}
|
||||
}
|
||||
|
||||
func TestToTar(t *testing.T) {
|
||||
m, err := New()
|
||||
if err != nil {
|
||||
t.Fatalf("New() returned an error: %v", err)
|
||||
}
|
||||
m.RootFS.AddData("test.txt", []byte("hello world"))
|
||||
tarball, err := m.ToTar()
|
||||
if err != nil {
|
||||
t.Fatalf("ToTar() returned an error: %v", err)
|
||||
}
|
||||
if tarball == nil {
|
||||
t.Fatal("ToTar() returned a nil tarball")
|
||||
}
|
||||
|
||||
tr := tar.NewReader(bytes.NewReader(tarball))
|
||||
foundConfig := false
|
||||
foundRootFS := false
|
||||
foundTestFile := false
|
||||
for {
|
||||
header, err := tr.Next()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
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")
|
||||
}
|
||||
}
|
||||
47
pkg/pwa/pwa_error_test.go
Normal file
47
pkg/pwa/pwa_error_test.go
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
package pwa
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/schollz/progressbar/v3"
|
||||
)
|
||||
|
||||
func TestDownloadAndPackagePWA_Error(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/manifest.json" {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(`{"start_url": "index.html"}`))
|
||||
} else {
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := newTestPWAClient()
|
||||
|
||||
// Test with a server that returns a 404 for the start_url
|
||||
bar := progressbar.New(1)
|
||||
_, err := client.DownloadAndPackagePWA(server.URL, server.URL+"/manifest.json", bar)
|
||||
if err == nil {
|
||||
t.Fatal("Expected an error when the start_url returns a 404, but got nil")
|
||||
}
|
||||
|
||||
// Test with a bad manifest URL
|
||||
_, err = client.DownloadAndPackagePWA(server.URL, "http://bad.url/manifest.json", bar)
|
||||
if err == nil {
|
||||
t.Fatal("Expected an error when the manifest URL is bad, but got nil")
|
||||
}
|
||||
|
||||
// Test with a manifest that is not valid JSON
|
||||
server2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintln(w, "this is not json")
|
||||
}))
|
||||
defer server2.Close()
|
||||
_, err = client.DownloadAndPackagePWA(server2.URL, server2.URL, bar)
|
||||
if err == nil {
|
||||
t.Fatal("Expected an error when the manifest is not valid JSON, but got nil")
|
||||
}
|
||||
}
|
||||
|
|
@ -23,6 +23,7 @@ type Downloader struct {
|
|||
maxDepth int
|
||||
progressBar *progressbar.ProgressBar
|
||||
client *http.Client
|
||||
errors []error
|
||||
}
|
||||
|
||||
// NewDownloader creates a new Downloader.
|
||||
|
|
@ -33,10 +34,11 @@ func NewDownloader(maxDepth int) *Downloader {
|
|||
// NewDownloaderWithClient creates a new Downloader with a custom http.Client.
|
||||
func NewDownloaderWithClient(maxDepth int, client *http.Client) *Downloader {
|
||||
return &Downloader{
|
||||
dn: datanode.New(),
|
||||
visited: make(map[string]bool),
|
||||
maxDepth: maxDepth,
|
||||
client: client,
|
||||
dn: datanode.New(),
|
||||
visited: make(map[string]bool),
|
||||
maxDepth: maxDepth,
|
||||
client: client,
|
||||
errors: make([]error, 0),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -52,6 +54,14 @@ func downloadAndPackageWebsite(startURL string, maxDepth int, bar *progressbar.P
|
|||
d.progressBar = bar
|
||||
d.crawl(startURL, 0)
|
||||
|
||||
if len(d.errors) > 0 {
|
||||
var errs []string
|
||||
for _, e := range d.errors {
|
||||
errs = append(errs, e.Error())
|
||||
}
|
||||
return nil, fmt.Errorf("failed to download website:\n%s", strings.Join(errs, "\n"))
|
||||
}
|
||||
|
||||
return d.dn, nil
|
||||
}
|
||||
|
||||
|
|
@ -66,23 +76,33 @@ func (d *Downloader) crawl(pageURL string, depth int) {
|
|||
|
||||
resp, err := d.client.Get(pageURL)
|
||||
if err != nil {
|
||||
fmt.Printf("Error getting %s: %v\n", pageURL, err)
|
||||
d.errors = append(d.errors, fmt.Errorf("Error getting %s: %w", pageURL, err))
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
d.errors = append(d.errors, fmt.Errorf("bad status for %s: %s", pageURL, resp.Status))
|
||||
return
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
fmt.Printf("Error reading body of %s: %v\n", pageURL, err)
|
||||
d.errors = append(d.errors, fmt.Errorf("Error reading body of %s: %w", pageURL, err))
|
||||
return
|
||||
}
|
||||
|
||||
relPath := d.getRelativePath(pageURL)
|
||||
d.dn.AddData(relPath, body)
|
||||
|
||||
// Don't try to parse non-html content
|
||||
if !strings.HasPrefix(resp.Header.Get("Content-Type"), "text/html") {
|
||||
return
|
||||
}
|
||||
|
||||
doc, err := html.Parse(strings.NewReader(string(body)))
|
||||
if err != nil {
|
||||
fmt.Printf("Error parsing HTML of %s: %v\n", pageURL, err)
|
||||
d.errors = append(d.errors, fmt.Errorf("Error parsing HTML of %s: %w", pageURL, err))
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -123,14 +143,19 @@ func (d *Downloader) downloadAsset(assetURL string) {
|
|||
|
||||
resp, err := d.client.Get(assetURL)
|
||||
if err != nil {
|
||||
fmt.Printf("Error getting asset %s: %v\n", assetURL, err)
|
||||
d.errors = append(d.errors, fmt.Errorf("Error getting asset %s: %w", assetURL, err))
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
d.errors = append(d.errors, fmt.Errorf("bad status for asset %s: %s", assetURL, resp.Status))
|
||||
return
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
fmt.Printf("Error reading body of asset %s: %v\n", assetURL, err)
|
||||
d.errors = append(d.errors, fmt.Errorf("Error reading body of asset %s: %w", assetURL, err))
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue