feat: initial public version
This commit is contained in:
165
config/config.go
Normal file
165
config/config.go
Normal file
@@ -0,0 +1,165 @@
|
||||
// Package config reads application config.
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// A Config describes application cofig.
|
||||
type Config struct {
|
||||
PoolSize int `json:"pool_size"`
|
||||
Verbose bool `json:"verbose"`
|
||||
Box *Box `json:"box"`
|
||||
Step *Step `json:"step"`
|
||||
HTTP *HTTP `json:"http"`
|
||||
|
||||
// These are the available containers ("boxes").
|
||||
Boxes map[string]*Box `json:"boxes"`
|
||||
|
||||
// These are the "sandboxes". Each sandbox can contain
|
||||
// multiple commands, and each command can contain
|
||||
// multiple steps. Each step is executed in a specific box.
|
||||
Commands map[string]SandboxCommands `json:"commands"`
|
||||
}
|
||||
|
||||
// BoxNames returns configured box names.
|
||||
func (cfg *Config) BoxNames() []string {
|
||||
names := make([]string, 0, len(cfg.Boxes))
|
||||
for name := range cfg.Boxes {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
return names
|
||||
}
|
||||
|
||||
// CommandNames returns configured command names.
|
||||
func (cfg *Config) CommandNames() []string {
|
||||
names := make([]string, 0, len(cfg.Commands))
|
||||
for name := range cfg.Commands {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
return names
|
||||
}
|
||||
|
||||
// ToJSON returns JSON-encoded config with indentation.
|
||||
func (cfg *Config) ToJSON() string {
|
||||
data, _ := json.MarshalIndent(cfg, "", " ")
|
||||
return string(data)
|
||||
}
|
||||
|
||||
// A Box describes a specific container.
|
||||
// There is an important difference between a "sandbox" and a "box".
|
||||
// A box is a single container. A sandbox is an environment in which we run commands.
|
||||
// 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 {
|
||||
Image string `json:"image"`
|
||||
Runtime string `json:"runtime"`
|
||||
Host
|
||||
|
||||
Files []string `json:"files"`
|
||||
}
|
||||
|
||||
// A Host describes container Host attributes.
|
||||
type Host struct {
|
||||
CPU int `json:"cpu"`
|
||||
Memory int `json:"memory"`
|
||||
Storage string `json:"storage"`
|
||||
Network string `json:"network"`
|
||||
Writable bool `json:"writable"`
|
||||
Volume string `json:"volume"`
|
||||
Tmpfs []string `json:"tmpfs"`
|
||||
CapAdd []string `json:"cap_add"`
|
||||
CapDrop []string `json:"cap_drop"`
|
||||
Ulimit []string `json:"ulimit"`
|
||||
// do not use the ulimit nproc because it is
|
||||
// a per-user setting, not a per-container setting
|
||||
NProc int `json:"nproc"`
|
||||
}
|
||||
|
||||
// SandboxCommands describes all commands available for a sandbox.
|
||||
// command name : command
|
||||
type SandboxCommands map[string]*Command
|
||||
|
||||
// A Command describes a specific set of actions to take
|
||||
// when executing a command in a sandbox.
|
||||
type Command struct {
|
||||
Engine string `json:"engine"`
|
||||
Entry string `json:"entry"`
|
||||
Before *Step `json:"before"`
|
||||
Steps []*Step `json:"steps"`
|
||||
After *Step `json:"after"`
|
||||
}
|
||||
|
||||
// A Step describes a single step of a command.
|
||||
type Step struct {
|
||||
Box string `json:"box"`
|
||||
User string `json:"user"`
|
||||
Action string `json:"action"`
|
||||
Stdin bool `json:"stdin"`
|
||||
Command []string `json:"command"`
|
||||
Timeout int `json:"timeout"`
|
||||
NOutput int `json:"noutput"`
|
||||
}
|
||||
|
||||
// An HTTP describes HTTP engine settings.
|
||||
type HTTP struct {
|
||||
Hosts map[string]string `json:"hosts"`
|
||||
}
|
||||
|
||||
// setBoxDefaults sets default box properties
|
||||
// instead of zero values.
|
||||
func setBoxDefaults(box, defs *Box) {
|
||||
if box.Runtime == "" {
|
||||
box.Runtime = defs.Runtime
|
||||
}
|
||||
if box.CPU == 0 {
|
||||
box.CPU = defs.CPU
|
||||
}
|
||||
if box.Memory == 0 {
|
||||
box.Memory = defs.Memory
|
||||
}
|
||||
if box.Storage == "" {
|
||||
box.Storage = defs.Storage
|
||||
}
|
||||
if box.Network == "" {
|
||||
box.Network = defs.Network
|
||||
}
|
||||
if box.Volume == "" {
|
||||
box.Volume = defs.Volume
|
||||
}
|
||||
if box.Tmpfs == nil {
|
||||
box.Tmpfs = defs.Tmpfs
|
||||
}
|
||||
if box.CapAdd == nil {
|
||||
box.CapAdd = defs.CapAdd
|
||||
}
|
||||
if box.CapDrop == nil {
|
||||
box.CapDrop = defs.CapDrop
|
||||
}
|
||||
if box.Ulimit == nil {
|
||||
box.Ulimit = defs.Ulimit
|
||||
}
|
||||
if box.NProc == 0 {
|
||||
box.NProc = defs.NProc
|
||||
}
|
||||
}
|
||||
|
||||
// setStepDefaults sets default command step
|
||||
// properties instead of zero values.
|
||||
func setStepDefaults(step, defs *Step) {
|
||||
if step.User == "" {
|
||||
step.User = defs.User
|
||||
}
|
||||
if step.Action == "" {
|
||||
step.Action = defs.Action
|
||||
}
|
||||
if step.Timeout == 0 {
|
||||
step.Timeout = defs.Timeout
|
||||
}
|
||||
if step.NOutput == 0 {
|
||||
step.NOutput = defs.NOutput
|
||||
}
|
||||
}
|
||||
157
config/config_test.go
Normal file
157
config/config_test.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestConfig_BoxNames(t *testing.T) {
|
||||
cfg := &Config{
|
||||
Boxes: map[string]*Box{
|
||||
"go": {},
|
||||
"python": {},
|
||||
},
|
||||
}
|
||||
|
||||
want := []string{"go", "python"}
|
||||
got := cfg.BoxNames()
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("BoxNames: expected %v, got %v", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfig_CommandNames(t *testing.T) {
|
||||
cfg := &Config{
|
||||
Commands: map[string]SandboxCommands{
|
||||
"go": map[string]*Command{
|
||||
"run": {},
|
||||
},
|
||||
"python": map[string]*Command{
|
||||
"run": {},
|
||||
"test": {},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
want := []string{"go", "python"}
|
||||
got := cfg.CommandNames()
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("CommandNames: expected %v, got %v", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfig_ToJSON(t *testing.T) {
|
||||
cfg := &Config{
|
||||
PoolSize: 8,
|
||||
Boxes: map[string]*Box{
|
||||
"go": {},
|
||||
"python": {},
|
||||
},
|
||||
Commands: map[string]SandboxCommands{
|
||||
"go": map[string]*Command{
|
||||
"run": {},
|
||||
},
|
||||
"python": map[string]*Command{
|
||||
"run": {},
|
||||
"test": {},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
got := cfg.ToJSON()
|
||||
if !strings.Contains(got, `"pool_size": 8`) {
|
||||
t.Error("ToJSON: expected pool_size = 8")
|
||||
}
|
||||
}
|
||||
|
||||
func Test_setBoxDefaults(t *testing.T) {
|
||||
box := &Box{}
|
||||
defs := &Box{
|
||||
Image: "codapi/python",
|
||||
Runtime: "runc",
|
||||
Host: Host{
|
||||
CPU: 1, Memory: 64, Storage: "16m",
|
||||
Network: "none", Writable: true,
|
||||
Volume: "%s:/sandbox:ro",
|
||||
Tmpfs: []string{"/tmp:rw,size=16m"},
|
||||
CapAdd: []string{"all"},
|
||||
CapDrop: []string{"none"},
|
||||
Ulimit: []string{"nofile=96"},
|
||||
NProc: 96,
|
||||
},
|
||||
Files: []string{"config.py"},
|
||||
}
|
||||
setBoxDefaults(box, defs)
|
||||
if box.Image != "" {
|
||||
t.Error("Image: should not set default value")
|
||||
}
|
||||
if box.Runtime != defs.Runtime {
|
||||
t.Errorf("Runtime: expected %s, got %s", defs.Runtime, box.Runtime)
|
||||
}
|
||||
if box.CPU != defs.CPU {
|
||||
t.Errorf("CPU: expected %d, got %d", defs.CPU, box.CPU)
|
||||
}
|
||||
if box.Memory != defs.Memory {
|
||||
t.Errorf("Memory: expected %d, got %d", defs.Memory, box.Memory)
|
||||
}
|
||||
if box.Storage != defs.Storage {
|
||||
t.Errorf("Storage: expected %s, got %s", defs.Storage, box.Storage)
|
||||
}
|
||||
if box.Network != defs.Network {
|
||||
t.Errorf("Network: expected %s, got %s", defs.Network, box.Network)
|
||||
}
|
||||
if box.Volume != defs.Volume {
|
||||
t.Errorf("Volume: expected %s, got %s", defs.Volume, box.Volume)
|
||||
}
|
||||
if !reflect.DeepEqual(box.Tmpfs, defs.Tmpfs) {
|
||||
t.Errorf("Tmpfs: expected %v, got %v", defs.Tmpfs, box.Tmpfs)
|
||||
}
|
||||
if !reflect.DeepEqual(box.CapAdd, defs.CapAdd) {
|
||||
t.Errorf("CapAdd: expected %v, got %v", defs.CapAdd, box.CapAdd)
|
||||
}
|
||||
if !reflect.DeepEqual(box.CapDrop, defs.CapDrop) {
|
||||
t.Errorf("CapDrop: expected %v, got %v", defs.CapDrop, box.CapDrop)
|
||||
}
|
||||
if !reflect.DeepEqual(box.Ulimit, defs.Ulimit) {
|
||||
t.Errorf("Ulimit: expected %v, got %v", defs.Ulimit, box.Ulimit)
|
||||
}
|
||||
if box.NProc != defs.NProc {
|
||||
t.Errorf("NProc: expected %d, got %d", defs.NProc, box.NProc)
|
||||
}
|
||||
if len(box.Files) != 0 {
|
||||
t.Error("Files: should not set default value")
|
||||
}
|
||||
}
|
||||
|
||||
func Test_setStepDefaults(t *testing.T) {
|
||||
step := &Step{}
|
||||
defs := &Step{
|
||||
Box: "python",
|
||||
User: "sandbox",
|
||||
Action: "run",
|
||||
Command: []string{"python", "main.py"},
|
||||
Timeout: 3,
|
||||
NOutput: 4096,
|
||||
}
|
||||
|
||||
setStepDefaults(step, defs)
|
||||
if step.Box != "" {
|
||||
t.Error("Box: should not set default value")
|
||||
}
|
||||
if step.User != defs.User {
|
||||
t.Errorf("User: expected %s, got %s", defs.User, step.User)
|
||||
}
|
||||
if step.Action != defs.Action {
|
||||
t.Errorf("Action: expected %s, got %s", defs.Action, step.Action)
|
||||
}
|
||||
if len(step.Command) != 0 {
|
||||
t.Error("Command: should not set default value")
|
||||
}
|
||||
if step.Timeout != defs.Timeout {
|
||||
t.Errorf("Timeout: expected %d, got %d", defs.Timeout, step.Timeout)
|
||||
}
|
||||
if step.NOutput != defs.NOutput {
|
||||
t.Errorf("NOutput: expected %d, got %d", defs.NOutput, step.NOutput)
|
||||
}
|
||||
}
|
||||
94
config/load.go
Normal file
94
config/load.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Read reads application config from JSON files.
|
||||
func Read(cfgPath, boxPath, cmdPath string) (*Config, error) {
|
||||
cfg, err := ReadConfig(cfgPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg, err = ReadBoxes(cfg, boxPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg, err = ReadCommands(cfg, cmdPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
// ReadConfig reads application config from a JSON file.
|
||||
func ReadConfig(path string) (*Config, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg := &Config{}
|
||||
err = json.Unmarshal(data, cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
// ReadBoxes reads boxes config from a JSON file.
|
||||
func ReadBoxes(cfg *Config, path string) (*Config, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
boxes := make(map[string]*Box)
|
||||
err = json.Unmarshal(data, &boxes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, box := range boxes {
|
||||
setBoxDefaults(box, cfg.Box)
|
||||
}
|
||||
|
||||
cfg.Boxes = boxes
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
// ReadCommands reads commands config from a JSON file.
|
||||
func ReadCommands(cfg *Config, path string) (*Config, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
commands := make(map[string]SandboxCommands)
|
||||
err = json.Unmarshal(data, &commands)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, playCmds := range commands {
|
||||
for _, cmd := range playCmds {
|
||||
if cmd.Before != nil {
|
||||
setStepDefaults(cmd.Before, cfg.Step)
|
||||
}
|
||||
for _, step := range cmd.Steps {
|
||||
setStepDefaults(step, cfg.Step)
|
||||
}
|
||||
if cmd.After != nil {
|
||||
setStepDefaults(cmd.After, cfg.Step)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cfg.Commands = commands
|
||||
return cfg, err
|
||||
}
|
||||
38
config/load_test.go
Normal file
38
config/load_test.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRead(t *testing.T) {
|
||||
cfgPath := filepath.Join("testdata", "config.json")
|
||||
boxPath := filepath.Join("testdata", "boxes.json")
|
||||
cmdPath := filepath.Join("testdata", "commands.json")
|
||||
cfg, err := Read(cfgPath, boxPath, cmdPath)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if cfg.PoolSize != 8 {
|
||||
t.Errorf("PoolSize: expected 8, got %d", cfg.PoolSize)
|
||||
}
|
||||
if !cfg.Verbose {
|
||||
t.Error("Verbose: expected true")
|
||||
}
|
||||
if cfg.Box.Memory != 64 {
|
||||
t.Errorf("Box.Memory: expected 64, got %d", cfg.Box.Memory)
|
||||
}
|
||||
if cfg.Step.User != "sandbox" {
|
||||
t.Errorf("Step.User: expected sandbox, got %s", cfg.Step.User)
|
||||
}
|
||||
if _, ok := cfg.Boxes["python"]; !ok {
|
||||
t.Error("Boxes: missing python box")
|
||||
}
|
||||
if _, ok := cfg.Commands["python"]; !ok {
|
||||
t.Error("Commands: missing python sandbox")
|
||||
}
|
||||
if _, ok := cfg.Commands["python"]["run"]; !ok {
|
||||
t.Error("Commands[python]: missing run command")
|
||||
}
|
||||
}
|
||||
5
config/testdata/boxes.json
vendored
Normal file
5
config/testdata/boxes.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"python": {
|
||||
"image": "codapi/python"
|
||||
}
|
||||
}
|
||||
25
config/testdata/commands.json
vendored
Normal file
25
config/testdata/commands.json
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"python": {
|
||||
"run": {
|
||||
"engine": "docker",
|
||||
"entry": "main.py",
|
||||
"steps": [
|
||||
{
|
||||
"box": "python",
|
||||
"command": ["python", "main.py"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"test": {
|
||||
"engine": "docker",
|
||||
"entry": "test_main.py",
|
||||
"steps": [
|
||||
{
|
||||
"box": "python",
|
||||
"command": ["python", "-m", "unittest"],
|
||||
"noutput": 8192
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
10
config/testdata/config.json
vendored
Normal file
10
config/testdata/config.json
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"pool_size": 8,
|
||||
"verbose": true,
|
||||
"box": {
|
||||
"memory": 64
|
||||
},
|
||||
"step": {
|
||||
"user": "sandbox"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user