diff --git a/configs/boxes.json b/configs/boxes.json deleted file mode 100644 index ed0eb66..0000000 --- a/configs/boxes.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "alpine": { - "image": "codapi/alpine" - } -} diff --git a/configs/boxes/alpine.json b/configs/boxes/alpine.json new file mode 100644 index 0000000..972c82e --- /dev/null +++ b/configs/boxes/alpine.json @@ -0,0 +1,3 @@ +{ + "image": "codapi/alpine" +} diff --git a/docs/add-sandbox.md b/docs/add-sandbox.md index 9811037..65b6616 100644 --- a/docs/add-sandbox.md +++ b/docs/add-sandbox.md @@ -45,14 +45,11 @@ Build the image: docker build --file images/python/Dockerfile --tag codapi/python:latest images/python/ ``` -And register the image as a Codapi _box_ in `configs/boxes.json`: +Then register the image as a Codapi _box_. To do this, we create `configs/boxes/python.json`: ```js { - // ... - "python": { - "image": "codapi/python" - } + "image": "codapi/python" } ``` diff --git a/docs/install.md b/docs/install.md index 5064a23..2c27d71 100644 --- a/docs/install.md +++ b/docs/install.md @@ -2,7 +2,7 @@ Make sure you install Codapi on a separate machine — this is a must for security reasons. Do not store any sensitive data or credentials on this machine. This way, even if someone runs malicious code that somehow escapes the isolated environment, they won't have access to your other machines and data. -Steps for Debian (11/12) or Ubuntu (20.04/22.04). +Steps for Debian (11+) or Ubuntu (20.04+). 1. Install necessary packages (as root): diff --git a/images/alpine/Dockerfile b/images/alpine/Dockerfile index e12de74..aab8fba 100644 --- a/images/alpine/Dockerfile +++ b/images/alpine/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.18 +FROM alpine:3.20 RUN adduser --home /sandbox --disabled-password sandbox diff --git a/internal/config/config.go b/internal/config/config.go index 6dad01d..8e69e6a 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -55,6 +55,7 @@ func (cfg *Config) ToJSON() string { // A sandbox command can contain multiple steps, each of which runs in a separate box. // So the relation sandbox -> box is 1 -> 1+. type Box struct { + Name string `json:"name"` Image string `json:"image"` Runtime string `json:"runtime"` Host diff --git a/internal/config/load.go b/internal/config/load.go index ee5f76a..5c70c48 100644 --- a/internal/config/load.go +++ b/internal/config/load.go @@ -11,7 +11,7 @@ import ( const ( configFilename = "config.json" - boxesFilename = "boxes.json" + boxesDirname = "boxes" commandsDirname = "commands" ) @@ -22,7 +22,7 @@ func Read(path string) (*Config, error) { return nil, err } - cfg, err = ReadBoxes(cfg, filepath.Join(path, boxesFilename)) + cfg, err = ReadBoxes(cfg, filepath.Join(path, boxesDirname)) if err != nil { return nil, err } @@ -51,8 +51,57 @@ func ReadConfig(path string) (*Config, error) { return cfg, err } -// ReadBoxes reads boxes config from a JSON file. +// ReadBoxes reads boxes config from the boxes dir +// or from the boxes.json file if the boxes dir does not exist. func ReadBoxes(cfg *Config, path string) (*Config, error) { + var boxes map[string]*Box + var err error + + if fileio.Exists(path) { + // prefer the boxes dir + boxes, err = readBoxesDir(path) + } else { + // fallback to boxes.json + boxes, err = readBoxesFile(path + ".json") + } + if err != nil { + return nil, err + } + + for _, box := range boxes { + setBoxDefaults(box, cfg.Box) + } + + cfg.Boxes = boxes + return cfg, nil + +} + +// readBoxesDir reads boxes config from the boxes dir. +func readBoxesDir(path string) (map[string]*Box, error) { + fnames, err := filepath.Glob(filepath.Join(path, "*.json")) + if err != nil { + return nil, err + } + + boxes := make(map[string]*Box, len(fnames)) + for _, fname := range fnames { + box, err := fileio.ReadJson[Box](fname) + if err != nil { + return nil, err + } + if box.Name == "" { + // use the filename as the box name if it's not set + box.Name = strings.TrimSuffix(filepath.Base(fname), ".json") + } + boxes[box.Name] = &box + } + + return boxes, err +} + +// readBoxesFile reads boxes config from the boxes.json file. +func readBoxesFile(path string) (map[string]*Box, error) { data, err := os.ReadFile(path) if err != nil { return nil, err @@ -64,12 +113,7 @@ func ReadBoxes(cfg *Config, path string) (*Config, error) { return nil, err } - for _, box := range boxes { - setBoxDefaults(box, cfg.Box) - } - - cfg.Boxes = boxes - return cfg, err + return boxes, err } // ReadCommands reads commands config from a JSON file. diff --git a/internal/config/load_test.go b/internal/config/load_test.go index 84ab62c..28f077f 100644 --- a/internal/config/load_test.go +++ b/internal/config/load_test.go @@ -22,6 +22,19 @@ func TestRead(t *testing.T) { if cfg.Step.User != "sandbox" { t.Errorf("Step.User: expected sandbox, got %s", cfg.Step.User) } + + // alpine box + if _, ok := cfg.Boxes["custom-alpine"]; !ok { + t.Error("Boxes: missing my/alpine box") + } + if cfg.Boxes["custom-alpine"].Image != "custom/alpine" { + t.Errorf( + "Boxes[custom-alpine]: expected custom/alpine image, got %s", + cfg.Boxes["custom-alpine"].Image, + ) + } + + // python box if _, ok := cfg.Boxes["python"]; !ok { t.Error("Boxes: missing python box") } diff --git a/internal/config/testdata/boxes/alpine.json b/internal/config/testdata/boxes/alpine.json new file mode 100644 index 0000000..da1f369 --- /dev/null +++ b/internal/config/testdata/boxes/alpine.json @@ -0,0 +1,4 @@ +{ + "name": "custom-alpine", + "image": "custom/alpine" +} diff --git a/internal/config/testdata/boxes/python.json b/internal/config/testdata/boxes/python.json new file mode 100644 index 0000000..ea4fb17 --- /dev/null +++ b/internal/config/testdata/boxes/python.json @@ -0,0 +1,3 @@ +{ + "image": "codapi/python" +}