Git Product home page Git Product logo

camerondurham / codecanvas Goto Github PK

View Code? Open in Web Editor NEW
26.0 4.0 1.0 6.97 MB

Remote code executor server, frontend, and CLI to run untrusted code as a non-root user in a Docker container.

Home Page: https://u64.cam/codecanvas

Go 79.47% Makefile 2.19% Shell 1.91% Python 0.06% Dockerfile 1.91% HTML 1.43% JavaScript 8.32% CSS 1.58% Earthly 2.60% Rust 0.04% Nix 0.49%
go sandbox code-execution playground earthfile remote-execution docker executor flyio non-root

codecanvas's Introduction

codecanvas

codecov Go Version

Previously named "runner"

Demo

gif demo showing python3 nodejs and c++ running code

NOTE: please do not rely on this API. This is a hobby project for learning and despite best effort to make it definitely can still crash resilient, it does not handle lots of malicious input. If you do find bugs, please do report them directly or submit a quick issue!

Intro

The runner project is to create an interface for users to run their code remotely without having to have any compiler on their machine. It is a toy project meant for learning how to build a backend in Go and experimenting how to build a somewhat multi-tenant system to run other people's code.

High Level Architecture Diagram

runner project high level diagram

Project State

Sequence diagram with rough state of the project.

Mermaid Live Editor

sequenceDiagram
Client->>Server: submits code

loop Check
      Server->>Server: validate request format
end

Server-->>CodeRunner: call CodeRunner.Run()

loop TransformRequest
      CodeRunner->>CodeRunner: parse language
      CodeRunner->>CodeRunner: create language runtime props
end

CodeRunner-->>Controller: submit job to controller

loop FindRunner
    Controller->>Controller: find available runner
    Controller-->>Controller: lock runner


    loop RunnerExecutionFlow
        Controller->>Standard Shell: pre-run hook (compile)
        Standard Shell-->>Controller: 
        Controller->>Process: execute processor binary

        loop Process
            Process->>Process: set resource limits
            Process->>Process: set non-root uid and gid for user code
            Process->>Process: execute user code
        end

        Process-->>Controller: return exit code
        Controller->>Standard Shell: remove source code
        Standard Shell-->>Controller: return result
    end
    Controller-->>Controller: unlock runner
end




Controller-->>CodeRunner: return stdout, stderr, runtime errors

CodeRunner-->>Server: return CodeRunnerOutput
Server-->>Client: return server transformed response

Development

Repository Structure

These components live in the following paths:

  • browser front-end: web-frontend (thank you to @arekouzounian for this!)
  • command-line interface: cli/runner/ (another thank you to @arekouzounian for this!)
  • API Server: api/ (thank you to @filipgraniczny for the help!)
  • CodeRunner: engine/coderunner (thank you to @siwei-li for the help!)
  • Runner Containers: engine/runtime

Dev Environment

Editors:

Extensions setup docs:

Recommended extensions (VSCode):

Search for these extension ids in VSCode and feel free to add your personal favs:

  1. golang.go for running and debugging Go (see vscode-go debugging docs)
  2. eamodio.gitlens git lens (pro tip, enable editor heat map in upper right corner)
  3. ms-vscode-remote.remote-containers develop in containers with all dependencies pre-installed
  4. ms-vscode-remote.remote-wsl for Windows WSL users
  5. yzhang.markdown-all-in-one for writing docs

Docker:

We will likely end up using Docker and include instructions here. For now, you can install Docker Desktop if you like.

Using Dev Containers with VSCode (recommended)

To use a pre-built development container, you can use the VSCode and the dev container provided in .devcontainer/devcontainer.json. This approach will use a Docker container with Go, cobra, python3, and g++ pre-installed and ready to use.

Here is a waay to long video with ~5 mins showing setup and total 12 mins demoing using the container: runner devcontainer setup video

Steps:

  1. Verify that you have Docker running
  2. Open VSCode and install the Remote - Containers extension: ms-vscode-remote.remote-containers
  3. Run the dev container
    1. Open the Command Palette (cmd + shift + P on macOS, F1 or ctrl + shift + p on Windows/Linux)
    2. Run Remote-Containers: Open Folder in Container
    3. Select the runner repository folder
  4. Wait for the dev container to start up and open the VSCode Terminal as needed to run commands!

Also see Remote-Containers: open an existing folder in a container.

Development Workflow

This repository is primarily written in Go and the Makefile has a helper commands to make development easier and more consistent.

Note: before you start development, please run make install-hooks to install Git Hooks in your repository's .git/hooks directory. This will install a pre-commit hook that automatically formats your code with gofmt.

Using the Earthfile

Earthly is a Go CLI that works with Docker. It is build tool that lets you run continuous integration and deployment actions locally. The reason it is being used here is to make CI/CD for this repo easier.

For this repo in particular, we need some tests to run in a Dockerized Linux environment to be as close to our deployment image as possible. Earthly makes this really easy since it can can run tests as part of its build process.

Installing

For macOS users:

brew install earthly/earthly/earthly && earthly bootstrap

For other users: earthly.dev/get-earthly

Using

To use, just check the target you want to run in the Earthfile. It is effectively like a Makefile + Dockerfile and below are a few commands you may want to run during development.

# run all tests and lints, just like how they'll be run in CI when you open a PR
earthly +run-ci

# lint the sourcecode with the golangci lint tool
earthly +lint

# just test the go code with coverage
earthly +test-go

Using the Makefile

By now, you are probably familiar with Makefiles. If not, this wiki provides a great summary: cs104/wiki/makefile (written by Leif Wesche).

Here's a quick summary of what the targets will do:

# print out all the makefile targets
make

# create or create mocks for unit testing, helpful if you have
# modified any of the interfaces in a `types.go` file
make gen-mocks

# run the API server (blocking, you can't use the terminal anymore)
make run-api

# run all tests in the repository
make test

# run go fmt on the repository to format your code
make fmt

# install git-hooks to automatically format your code before you commit
make install-hooks

CLI Setup

CLI stands for command line interface.

Note: this step is not needed if you are using the dev container since cobra is pre-installed in the container.

Installing the cobra CLI to help with codegen

Install cobra dependencies: (required to generate new CLI commands)

go install github.com/spf13/cobra/[email protected]

Adding New Commands

Add new cobra command

# change directories into the CLI sourcecode
cd cli/runner

# add new subcommand
cobra add <CHILD_COMMAND> -p <PARENT_COMMAND>

# example:
cobra add childCommand -p 'parentCommand'

Other Resources

Running the Server

During CLI or even server development, you will likely want to run the server during testing.

In the root directory runner, you can run the API a couple ways:

# 1. use the Makefile
make run-api

# 2. just use the go command
go run api/main.go

Usually you'll want to run the server in the background to you can do other things with your terminal. However, you'd need to kill the process running on port 10100 once you're done. You can use the api/kill_server.sh script for this.

# 1. run the API in the background
go run api/main.go &

# 2. once you are done, use the script to shut down processes on port 10100
./api/kill_server.sh

You can also use the api/kill_server.sh script if you see this error:

error starting server: listen tcp :10100: bind: address already in use

Go Tips

Working with Go Modules

Go Module:

# you usually will not have to run this since we should already have a go.mod and go.sum file
go mod init github.com/<name>/<repo-name>

# add new library
go get <new dependency>

# organize modules and dependencies
go mod tidy

# remove dependency
go mod edit -dropreplace github.com/go-chi/chi

Testing

Unit Tests

What are unit tests and why do we use them?

Unit testing is used to help us make sure smaller "units" of the code work as expected and handle all expected error cases. This project will end up being pretty large and we want to use unit tests to verify individual components before piecing everything together.

In runner_test.go, we mock the response of the runtime to isolate what we are testing and produce consist results without actually calling our "real code" in the runtime module.

More about unit tests: Definition of a Unit Test.

How to generate mocks:

Install the Go CLI mockgen to create mocks from Go interfaces:

go install github.com/golang/mock/[email protected]

Using Mockgen to create new mocks for testing:

Basic command structure:

mockgen -source ./path/to/file/with/filename.go -destinaion ./path/to/write/mocks/filename.go InterfaceName

Example:

In engine/runtime/types.go there is the interface Runtime that we would like to mock for unit tests:

type Runtime interface {
  RunCmd(runprops *RunProps) (*RunOutput, error)
}

The command below will create a mock-able Runtime interface, helper functions to implement Runtime that you can call RunCmd on.

We can organize mocks in a submodule by making the engine/runtime/mocks directory and provide that and a filename to write the mocked classes.

mockgen -source ./engine/runtime/types.go -package=mocks -destination ./engine/runtime/mocks/Runtime.go Runtime

You can see an example here of how to actually use mocks in a unit test.

Note: The command above has been added to the Makefile. If you are creating mocks you want for a new file or interface, feel free to add those commands to the gen-mocks target so these are generated when you run make gen-mocks.

Integration Testing

Will add more about this later! Here's some reading from Martin Fowler for now!

For now the only sort of "end-to-end" integration test is here: runner/blob/main/engine/integ_test/integration_test.go

Documentation

When writing instructions for users and in the README, please follow syntax recommended by google developer docs

codecanvas's People

Contributors

arekouzounian avatar camerondurham avatar dependabot[bot] avatar filipgraniczny avatar siwei-li avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

Forkers

arekouzounian

codecanvas's Issues

CLI: create runner API client

This can be used to make request/response handling between client and server easier and allow us to document expected handling in our client types. With request/response handled in the client, the CLI logic can focus on reading files, forming requests and formatting response.

Some of this work is already done in CLI last issue, this will mainly be abstracting some logic into a client interface.

See TODOs in cli/runner/client/client.go and feel free to rename to make more sense in your use case.

Related page from Effective Go: https://go.dev/doc/effective_go#interfaces_and_types

TODO

  • implement NewClientFromConfig() helper function
  • refactor existing code into interface implementation struct for Run() and Languages() commands
  • use new client in CLI to make requests

CodeRunner: design solution for concurrent code runs

Details

Short Term Goal

Track max number of concurrent code-runs and the uid and gid allowed to be assigned to these code runner instances. The uid and gid are passed in statically here. Instead, these values should be tracked centrally by the engine so concurrent never are using the same UID or GID.

This could probably be done by using a set number of "workers" to handle requests, where each worker is given an allowed Uid, Gid and working directory. There can be a central map that stores the workers and when a request comes in we could do a dumb (for now) linear search over the list to find the first available worker. Workers would be responsible for setting if they are "ready" or not.

Long Term Goal

Design a solution to have N CodeRunners as "workers", running user submitted code. This limit will help protect the "host" system and the server from overloading while letting us execute code concurrently.

The process of a user submitting code should have this flow:

  1. client submits code to be executed
  2. server calls the controller with request
  3. controller checks for first available worker
    a. none found: return error/throttled response
    b. found: send to runner
  4. runner executes code
  5. runner sends response back to controller -> server -> client

This could also be approached by spawning as many threads up to limit N at one time and return 500 errors when at capacity.

Current Logic

We create temporary directories on-demand to store user code and use a static UID, GID. The goroutine handling a request will call CodeRunner, uses temporary directory to write user code to a file, compile and run it. This will likely be sufficient for now but we do not have any "levers" to control how many requests can be handled at a time. This will be important after load testing when we know the limits of our server.

Create deployment strategy

Deployment strategy for runner project.

The project can use a container for deployment. Will need to identify providers that make this easiest. ECS Fargate is likely a reliable, familiar option but should investigate https://fly.io vs cheaper VPS providers with a VM using Docker compose.

Should make sure we have a "kill switch" to turn off project if we get DDOS'd or something.

  • test if we can use containers for deployment
  • check out and test (if possible) container service providers (AWS ECS Fargate, fly.io, etc)
  • get domain, setup DNS
  • deploy project, network load balancer
  • write Ansible, Terraform, or AWS CDK for automated deployment (stretch)

Use `AsyncController` in server instead of default runtime

Will add more details to this issue but should be suitable for anyone to take up. This might be a good opportunity to get more familiar with the runtime part of the project.

The goal of this issue is to make sure that we use the "thread-safe" runtime to ensure that when concurrently running user requests we don't use the same UID and GID.

The coderunner should now use a Controller instead of the RuntimeAgent directly. We can make a coderunner struct (if there isn't one already) that will be initialized with the AsyncController when the server starts up. The server can have some default configuration as well that determines how many agents the server creates.

We can also make a more appropriate type to pass into SubmitRequest: https://github.com/camerondurham/runner/blob/e6362100fba794e9ea5626bf54da1f09592e7810/engine/controller/controller.go#L59

Instead of RunProps, the coderunner should not have to pass in any Uid or Gid since the Controller should handle the Uid/Gid since agents contain what values we should use. We can make some other wrapper type for the SubmitRequest like this:

type ControllerRequest struct {
    RunArgs: []string
    Timeout: int
    NProcs: int
}

TODO:

  • create new props to pass to controller.SubmitRequest instead of RunProps
  • remove Uid and Gid from RunProps
  • use AsyncController in the coderunner instead of directly using a new runtime each time

Resources: add container fundamentals wiki/tutorial

It would be nice to have a doc that defines the basics of containers and how to do some basic process isolation in Linux. This should eventually become a detailed doc of how we handle isolating potential malicious user code from the machine.

Document should include resources to good articles/DockerCon/KubeCon videos that are helpful.

  • document describes lower level container implementation
  • document links to learning resources
  • document includes how to use basic linux syscalls (i.e. unshare, pivot_root)

CLI: basic unit tests

Details

Create unit tests to verify CLI functionality for command handlers. If possible, add integration tests to test against CLI binary.

Motivation

The purpose of doing this is to verify CLI functionality and make sure this handles all the error/success cases we think it does. Unit tests like this can speed up development and iteration because we don't have to manually verify the CLI works like we want it to, all we have to do is run go test ./....

Part of this involves making the code more test-able by breaking up commands into smaller functions that we can easily verify. This also means clearly identifying and separating dependencies to allow easier mocking so when writing a test for the functions in cli/runner/run.go we are only testing the code in that file, not also testing the code in cli/runner/client/client.go.

Possible Implementation Steps

1. Make Requester Mocks

Make a directory such as cli/runner/client/mocks to contain mocks for the Requester interface. Then you can generate mocks of the Requester interface by using the mockgen CLI. See an example from the Makefile: https://github.com/camerondurham/runner/blob/2d02bbcc19294dead678d9aa7b1ed6c885018d2c/Makefile#L80

Now you can create a requester and mock its behavior when testing the CLI.

2. Refactor CLI functions so the Requester is passed into function

This step is needed so we can make the CLI functions use the mocked requester created in 1 instead of making a "real" one.

To do this, we can refactor the CLI into the CliClient by making a struct with the CLI's dependencies. An example of this structure is the runner server Client struct: https://github.com/camerondurham/runner/blob/2d02bbcc19294dead678d9aa7b1ed6c885018d2c/cli/runner/client/client.go#L28-L31

What this allows is the ability to inject the Runner Server Client mocks.

Since the CLI really only needs the Client, the CliClient can be something like this:

type CliClient struct {
    client Requester
}

Then we can separate the runner CLI specific logic into refactor the the CLI logic into pointer receivers:

func (cli *CliClient) run(lang string, filename string) (*api.RunResponse, error) {
   // check language and load file 
   ...
   return cli.client.Run(&RunRequest{...})
    
}

3. Implement the unit tests

Now you can create unit tests that will test the CliClient structs created in 2. It will probably look something like this

cli := &CliClient{
   client: mocks.NewMockRequester()
}

See this snippet for how to make specific responses for the generated mocks:

https://github.com/camerondurham/runner/blob/2d02bbcc19294dead678d9aa7b1ed6c885018d2c/engine/coderunner/runner_test.go#L20-L33

Subtasks

Project skeleton code and structure

Project repository with dev environment guide and basic project structure.

Engine, API server and CLI should be runnable and have at least one unit test.

  • README with dev environment instructions and PR process
  • Engine module boilerplate
    • runs input commands and returns stdout/stderr
    • basic integration test
  • CodeRunner module boilerplate
    • rough implementation of multiple language support
    • data structures setup to implement language specific features
    • unit test
  • API module boilerplate
    • GET /api/v1/languages endpoint hard-coded response
    • POST /api/v1/run endpoint hard-coded response
    • basic integration test
  • CLI module boilerplate
    • stub for languages subcommand
    • stub for run subcommand

Runtime: create interface and types for restricting jobs

This story is to add the data structures we will want to use to configure runner task/job resources. This config could be based on the OCI Runtime Spec for POSIX processes but open to different approaches.

The restrictions defined in this issue will be implemented in #38 with Golang wrappers around the mentioned setrlimit and getrlimit syscalls.

A sample configuration:

  • Rootfs string: path to "root" filesystem, where container should think "root" (/) directory is
  • [] RLimit, nprocs: the maximum number of processes the task can spawn
  • Hostname string: the hostname of the container (this isn't in the spec)

Linux RLIMIT

Linux uses RLIMIT to restrict resources that a process can create. We can use the setrlimit syscall to set these limits in another issue. RLIMIT's are listed in getrlimit(2) pages. We will be interested in:

  • RLIMIT_NPROC: number of processes process can spawn
  • RLIMIT_FSIZE: max file size process can create
  • RLIMIT_CPU: limit in seconds of CPU time the process can have

AC:

  • decide new runtime props props
    • array of RLIMITs
    • container root path
    • timeout
    • hostname
  • create new RunCmd function that will restrict the executed process, no need to implement restrictions yet

Runtime: automated end-to-end integration tests

Create end-to-end integration tests for limiting behavior. Should create runner struct and send code that will test the num procs limit, timeout limit and other limits as needed.

Requirements:

  1. verify runtime agent returns an error when program exceeds timeout limit
    1. and that execution does not exceed TimeoutLimit + epsilon
  2. verify runtime agent returns an error when program exceeds max num procs limit
    1. (note: this will be hard to verify at an OS level, at least returning an error should be okay for now)
  3. verify runtime agent returns an error when program exceeds max file size creation limit

Add `RLIMIT_CPU`

Will add details later but this RLIMIT needs to be added.

https://man7.org/linux/man-pages/man2/getrlimit.2.html


       RLIMIT_CPU
              This is a limit, in seconds, on the amount of CPU time
              that the process can consume.  When the process reaches
              the soft limit, it is sent a SIGXCPU signal.  The default
              action for this signal is to terminate the process.
              However, the signal can be caught, and the handler can
              return control to the main program.  If the process
              continues to consume CPU time, it will be sent SIGXCPU
              once per second until the hard limit is reached, at which
              time it is sent SIGKILL.  (This latter point describes
              Linux behavior.  Implementations vary in how they treat
              processes which continue to consume CPU time after
              reaching the soft limit.  Portable applications that need
              to catch this signal should perform an orderly termination
              upon first receipt of SIGXCPU.)

Create users/groups on container startup

Create named groups and users on container startup:

# the simplest solution from the command line is

# make group with gid 1234 named runner1
groupadd -g 1234 runner1

# make user assigned to group runner1 with uid 1234, "home" directory /tmp/runner1, named runner1
useradd -g runner1 -u 1234 -d /tmp/runner1 runner1
  • install python3 and bash in dev container (can just use Debian or find equivalent in Alpine Packages)
  • create user directory for single runner (we'll add more for other users later): /tmp/runner1
  • make simple entrypoint script or setup users on server startup
  • (stretch) add this functionality to a dedicated server struct or add to CodeRunner to run when we initialize the server

Frontend: submit form, async request/response handling

Make backend request to server with form values and display in the output box.

Should handle server timeout if needed. Can use mock server for basic development but should ideally test against actual running server.

Please submit screenshots of work when creating CR.

CLI: make requests to mock server /language and /run endpoints

CLI makes valid requests to mock server endpoints created in #6.

Resources for working with the CLI:

Basic requirements:

  • CLI makes GET request to /api/v1/languages endpoint, displays a formatted response in terminal
  • CLI makes POST request to /api/v1/run endpoint and displays a formatted response in terminal

Nice to have:
These can be implemented in another story

  • CLI loads source code file from user
  • CLI sends source code string in POST /api/v1/run request body

Server: validate client request

Validate client request has non-empty POST Body with all required fields.

So far this just includes language and source.

See api.md in docs branch for current API spec.

  • validate client request has non-empty Body with required fields
  • unit test for validation logic

Server (testing): create synthetic load generator

Create a tool to generate and run synthetic load on the coderunner server. This is needed to test server performance and identify any scaling issues and determine how the application performs under high traffic.

Requirements:

  • makes requests with different types of languages and source code input
  • tool can be configured to make X requests per second

Nice to have:

  • randomly selects from at least 5 examples of each language
  • use GitHub Copilot or similar tool to generate language specific examples

Server: Plug in CodeRunner API into server

Basic server wrapper around the CodeRunner API. Should respond to POST/GET request that provides user input and return the Stdout result.

Not expected to handle remote shell vulnerability, server crashes, etc but will be useful for initial testing.

Resources:

  • examples on how to use CodeRunner module: engine/main.go

  • where to implement server: server/main.go

  • example of unit tests using mocks: engine/coderunner/runner_test.go

  • server endpoint: GET /api/v1/languages returns list of supported languages, retrieved from the CodeRunner module (engine/coderunner/types.go)

  • server endpoint: POST /api/v1/run lets user submit code to run and returns output

  • unit test for server request handlers

Runtime: command should be able to spawn a limited # of processes

The runtime should limit how many processes that can be created when the RunCmd function is used. Currently, the only limitation put on the code is how long it runs.

The Linux syscall setrlimit will be useful, exposed in os.syscall package with Setrlimit. Linux utility prlimit (used to get and set resource limits for a process) will likely be most helpful in debugging since it is a wrapper for setrlimit. NOTE this will require being run in a Linux box or at least a Linux Docker container.

Example:

runner@runner-vm:~$ prlimit --pid $$ --nproc=10:12
runner@runner-vm:~$ ps
    PID TTY          TIME CMD
  50411 pts/0    00:00:00 bash
  50438 pts/0    00:00:00 ps
...
runner@runner-vm:~$ sleep 100 &
[6] 50444
runner@runner-vm:~$ sleep 100 &
-bash: fork: retry: Resource temporarily unavailable

More Resources

RLIMITs we care about described here: getrlimit manpage
Process Resource Limit: prlimit syscall
Go wrapper for syscall: Prlimit
Golang package/wrapper for these syscalls: golang.org/x/sys/unix

Constants where RLIMITs are defined: pkg-constants

AC:

  • runtime can set hard limits on how many processes can be spawned from a RunCmd
  • unit tests verify processes are killed when over nproc limit
  • fork new child process before applying limits

Repo: fix failing go lints

Go lint failing from not checking returned error from json encoding.

This was my code and just making this issue so I remember to fix it.

Repo: Docker setup and pre-merge workflow checks for better CI

General repo improvements for faster development.

Merge with confidence! Let Github Actions run tests for you.

Should check the following:

  • build server and CLI binaries successfully
  • passes go lints/vets
  • code coverage test (stretch)

Docker setup for local testing and easier deployment when we decide to do so.

  • add Dockerfiles for running tests
  • Docker compose for running mock server/real server
  • add targets to Makefile for running containers
  • run go ci lint tool

Git Hooks:

  • run go fmt tool on repository before commits

Runtime: improve resource limiting API

This issue is to improve the resource limiting API by removing the process helper CLI or using another library to limit host resources.

An "external" process may not be needed and hopefully can find a way around this.

Tasks:

  • test if limiting behavior can be accomplished in same process
  • create new issue to try limiting using cgroups instead of rlimits (see containerd/cgroups)

Related Background

The prlimit/setrlimit mechanism being used will apply limits on the process if the user is not root and these limits will be for that user. Will need to manually test this implementation, this is possibly sufficient for now but using separate non-root users for each process is needed next.

prlimit --nproc a.k.a. RLIMIT_NPROC a.k.a. ulimit -u is the maximum number of processes¹ for the user as a whole. If you already have 20 processes running, and you set the limit to 20, you can't create any new process. What matters is how many processes are running as your user, it doesn't matter who their parent is or what their limit setting is.
[source]

See notes in this issue for more details: #42 (comment)

Originally posted by @camerondurham in #38 (comment)

Runtime: experiment using containerd runtime or containerd/cgroups vs syscalls to isolate user code

Look into using containerd/containerd as a container runtime to run processes or using containerd/cgroups to enforce limits instead of Linux syscall setrlimit : https://github.com/camerondurham/runner/blob/9cc6b646cb85552122482783d44008383ea14966/engine/runtime/limits.go#L33

An example of using cgroups instead of resource limits could be setting the pids cgroup value to the proc limit (see the containerd/cgroups repo for what this library can do for cgroups, specifically: https://github.com/containerd/cgroups#create-a-new-cgroup).

Related links:

Note:

This will likely require setting up a custom Linux VM as these tools cannot be tested in a container. We'd have to adjust the project's deployment strategy accordingly to pursue these alternate libraries/runtimes.

This does not have to be limited to containerd. We can explore other container runtimes besides this, including Docker itself. Ideally using containerd would be preferred since it is a lower level runtime that can expose more customization for us. Docker is really meant for users and dev experience and has many extra features we don't need such as image building included in the Docker daemon.

AC:

If containerd is a good fit:

  • determine dependencies required to run a containerd daemon
  • create Ansible/Vagrant or some automated configuration to run containerd
  • write module to start daemon and execute code in containers

If containerd is not a good fit:

  • decide on syscalls and security strategy that our runtime should use

Engine: runs basic commands

Engine can run basic commands from caller.

Example:

resp, err = engine.RunCommand(CommandProps{command: "echo hello world"})
fmt.Println(resp.CommandStdout)
  • create engine module with RunCommand function
    • engine runs arbitrary command input from user
    • engine returns Stdout result from command
    • engine returns Stderr result from command
  • unit test RunCommand and/or helpers

CodeRunner: multi-language support

Design/Refactor CodeRunner module to support running compiled and interpreted languages.

Currently, to support compiled languages, the compile and run step have to be executed "together" (link). This issue is to create a PreRunHook interface that implements any steps required before running the code. An example of how this could be used is writing the sourcecode to file for Python or writing to file and compiling to binary for C++.

Requirements:

  • "easy" to add another language (easy can be defined many ways, let's say adding support for Node shouldn't take more than a week?)
  • supports compiled and interpreted code execution

Example interface:

type CodeRunner interface {
    PreRunHook(lang Language, filename string) (RunProps, error)
    Run(props RunProps) (RunResponse, error)
    PostRunHook(filepath string) (error)
}

Currently implementation is here.

Example execution structure:

func (r *CodeRunner) Run(req *RunnerProps) (*RunnerOutput, error) {
    // error handling ...

    // handles writing to filename.extension and compiling code if needed
    runProps, err = r.runner.PreRunHook(r.language, runDirectoryTmp)

    // error handling
    resp, err := Run(runProps)
    
    // cleanup
    // ...
    // return output
}
  • CodeRunner function accepts variable language and code snippet

    • CodeRunner can internally handles compiling step if needed
    • CodeRunner may temporarily store user code in filesystem
    • CodeRunner deletes user code after running
    • Unit test for multi-language feature
  • End to End test for basic code snippet

Add more language support

Ideas on more languages to add:

  • Go (very easy, we already have Go installed in the image)
  • C (should be relatively easy, image already requires uses gcc)
  • Ruby (honestly who uses this?)
  • Rust
  • Zig

See #82 for what files should be modified. Calling out in particular:

Backend

  1. Server Dockerfile (for adding any dependencies required for the server image): https://github.com/camerondurham/runner/pull/82/files#diff-e3dc0276ffd991d263bbb04bce5d89d0773bb4069c96ef46282902681ec64ba3
  2. language.go (for adding language config, how to compile (if it's compiled of course) and run): https://github.com/camerondurham/runner/pull/82/files#diff-3c34a65e1f5cf2827cbca5707c44a4301816d2c713a80ab9e868a6e221db08a3
    a. need to add language config
    b. add language to SupportedLanguages, SupportedLanguagesSet so server accepts new lang requests
  3. Add new test cases in language_test.go as well

Frontend

  1. set-code.js (for adding language starter code to the dropdown): https://github.com/camerondurham/runner/pull/82/files#diff-1d55dc40f31e290098a80b8785b23ffa804a4fbd7cc1f81aa95f08207cb6ff6e

CodeRunner/Runtime: add failure cases to test suite

This may be blocked by #15

Create test suite of "malicious" code to understand limits of the CodeRunner. This will be helpful for benchmarking and understanding how much progress we've made toward the Code Isolation Milestone.

These test cases can be wrapped in an environment variable check so they do not fail the build and just used in a temporary test container. Example:

func Test_MaliciousCode(t *testing.T) {
  if os.Getenv("TEST_MC") != "" {
    // test stuff that may intentionally break
  }
  // rest of the test
}
  • add set of test cases for the runtime that run separately from unit tests (ideally in Docker)
  • test failure modes
    • throw an unexpected exception
    • spawn many processes
    • "escape" the current directory
    • spawn a shell
    • ... (think of other ideas)

Server: Improve mock server for CLI/frontend development

Create mock server for API endpoints described in design doc: Runner Project Details

This functionality currently exists. However it would be nice to have a separate mock server
for specifically testing frontend/CLI and so changes can be made on server without interfering with other development.

Acceptance Criteria:

  • Mock GET /api/v1/languages (added in #11, can copy into mock server)
  • Mock POST /api/v1/run (added in #11, can copy into mock server)
  • Mock POST /api/v1/run with random response (stretch)
    • Return mock "syntax error" like response
    • Return mock "runtime error" like response
    • Return mock successful response (prints out some message to stdout/stderr, no errors)
  • update Makefile commands to support make run-mock-api and run the corresponding mock server

Frontend: minimal text input and language selector

Minimal frontend with a text input form, language selector, "Run Code" button, and ideally a space to display user output.

Should submit form selections but backend request/response handling can be handled in a separate PR.

Please submit screenshots with the CR. Only working to target Google Chrome on a ~1920x1080 screen. Any other screen is out of scope or stretch if you really want to make it work.

Server: add more unit tests for expected server functionality

Some basic server unit tests exist here already: https://github.com/camerondurham/runner/blob/main/server/main_test.go

We don't need all of the tests below and can have different/more tests. This is just some basic test ideas to improve confidence in code changes that we're not breaking the server functionality.

Add demo to README

Demos

Add these gifs to README

  1. screen recording of writing/editing code
  2. ascicinema recording of using CLI

Use framework for web frontend

Use (any) framework for frontend. It can be Vue, React, Alpine.js, whichever is up to whomever works on this can decide. Would be nice if we could use Typescript (Vue, React) but not a requirement if it makes development too difficult.

The current frontend uses webpack for building but this can be replaced by any framework packaging tools. create-react-app, create-vue comes with its own autogenerated webpack config and vue likely does as well.

Nice to have:
Feel free to take artistic liberty with the frontend but would be cool if we could use existing coding playgrounds as design examples:

Frontend: research or proof of concept for basic in-browser editor

It would be nice to use a simple editor experience for a runner webapp/webpage.

This story is to look into existing libraries we could use for the editor and (if possible) create a minimal proof of concept where users can write code in the browser.

For example, the Monaco Editor from VSCode is an NPM library we could use.

Minimal requirements:

  • syntax highlighting based on language

  • frontend able to send input and log to console (show we can easily make a web request to backend)

  • (timebox ~1-2 hrs) looking into existing libraries we can use for editor, syntax highlighting

  • create mockup, single webpage

Engine: implement timeout with Go instead of external program

Currently, the runner uses the Linux timeout (from the coreutils homebrew formula on macOS) command to execute commands with a timeout.

This is to improve execution by using multiple channels and select to implement the timeout in Go. See gobyexample/timeouts for an example.

Acceptance Criteria (in progress):

  • run command with configurable X second timeout
    • look at other options to implement timeout in Go
    • explore if we can use select with exec.Command instead of the timeout external process
    • test functionality in unit tests

Runtime: basic process isolation

The following Linux utilities may be helpful in implementing this:

The solution to implementing these limits is still not certain. For now, we can attempt to use prlimit syscall to limit process resources. Our configuration for runner tasks/jobs should be based on the OCI Runtime Spec, specifically , the configuration for POSIX processes.

Acceptance Criteria (in progress):

  • run command with configurable X second timeout (moved to #17)
  • #38

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.