runbox 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +148 -0
- data/README.md +285 -0
- data/lib/runbox/client.rb +158 -0
- data/lib/runbox/configuration.rb +20 -0
- data/lib/runbox/error.rb +34 -0
- data/lib/runbox/result.rb +43 -0
- data/lib/runbox/setup_result.rb +49 -0
- data/lib/runbox/version.rb +5 -0
- data/lib/runbox.rb +27 -0
- metadata +123 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 662927e34aedb615bcd9f472fd2d2841c75c0e4bc1cca64ef4122d46402a7acb
|
|
4
|
+
data.tar.gz: a2d0d12e759f2417194f22c8b4a6747c6a45bd66e1c0c50d629024924738f92e
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 77ef8ccb39538cd98d3d5f95c62dc5f615e2204100c3a098bc896f714d70cae82b77282ab6ae329eb868bd311797ba1dbd61c7aa012cb3bc9594588a47c7a4df
|
|
7
|
+
data.tar.gz: ddb7f6e43783578a936cffb783965b4da7b48d2b5860517a93b5d9d633c54827d917aaeb9888996a02474fe58d227545ff414517bab3af0231a40db7512d1d9b
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [1.2.1] - 2025-12-17
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- **Shell image updated**: Added bats-core (Bash Automated Testing System) to the shell execution environment for bash script testing support.
|
|
13
|
+
|
|
14
|
+
## [1.2.0] - 2025-12-11
|
|
15
|
+
|
|
16
|
+
### Breaking Changes
|
|
17
|
+
|
|
18
|
+
- **Renamed `entrypoint` to `run_command`** in `run()` method.
|
|
19
|
+
- The `entrypoint` parameter (file path) is removed.
|
|
20
|
+
- New `run_command` parameter accepts a full shell command string.
|
|
21
|
+
- Example: `run_command: "python main.py"` instead of `entrypoint: "main.py"`.
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- **`run_command` support**: Execute arbitrary shell commands (e.g. `pytest tests/`, `bundle exec rails test`).
|
|
26
|
+
|
|
27
|
+
### Migration Guide
|
|
28
|
+
|
|
29
|
+
**Before:**
|
|
30
|
+
```ruby
|
|
31
|
+
client.run(
|
|
32
|
+
entrypoint: "main.py",
|
|
33
|
+
# ...
|
|
34
|
+
)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**After:**
|
|
38
|
+
```ruby
|
|
39
|
+
client.run(
|
|
40
|
+
run_command: "python main.py",
|
|
41
|
+
# ...
|
|
42
|
+
)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## [1.1.0] - 2025-12-10
|
|
46
|
+
|
|
47
|
+
### Added
|
|
48
|
+
|
|
49
|
+
- **`new_dependencies` parameter** in `run()` method: Install dependencies on-the-fly before code execution
|
|
50
|
+
- Python: `["requests==2.31.0", "pytest"]`
|
|
51
|
+
- Ruby: `["rake", "rspec"]`
|
|
52
|
+
- Shell: `["curl", "jq", "git"]` (uses apk)
|
|
53
|
+
- **`packages` field** in `Result`: Returns updated package list when dependencies are installed
|
|
54
|
+
- Only included when `new_dependencies` were provided
|
|
55
|
+
- Hash of package names to versions
|
|
56
|
+
|
|
57
|
+
### Example
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
# Install dependencies and run code
|
|
61
|
+
result = client.run(
|
|
62
|
+
container_id: container_id,
|
|
63
|
+
files: [{ path: "main.py", content: "import requests; print(requests.__version__)" }],
|
|
64
|
+
entrypoint: "main.py",
|
|
65
|
+
new_dependencies: ["requests==2.31.0"]
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
puts result.packages # {"pip"=>"23.0.1", "requests"=>"2.31.0", ...}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## [1.0.0] - 2025-12-10
|
|
72
|
+
|
|
73
|
+
### Breaking Changes
|
|
74
|
+
|
|
75
|
+
- **New API workflow**: The client now uses a two-step `setup()` + `run()` workflow instead of a single `run()` call
|
|
76
|
+
- `run()` method signature changed:
|
|
77
|
+
- Now requires `container_id` (from `setup()` response) instead of `identifier` and `language`
|
|
78
|
+
- Removed `memory` and `network_allow` parameters (these are now in `setup()`)
|
|
79
|
+
- `Result` object no longer includes `container_id` and `cached` fields (these are now in `SetupResult`)
|
|
80
|
+
|
|
81
|
+
### Added
|
|
82
|
+
|
|
83
|
+
- **`setup()` method**: Creates or reuses a container and returns environment information
|
|
84
|
+
- Returns `SetupResult` with `container_id`, `cached?`, and `environment_snapshot`
|
|
85
|
+
- `environment_snapshot` includes OS name/version, runtime name/version, and installed packages
|
|
86
|
+
- **`SetupResult` class**: New result object for `setup()` responses
|
|
87
|
+
- **`EnvironmentSnapshot` class**: Contains detailed environment information
|
|
88
|
+
- `os_name`, `os_version`: Operating system details
|
|
89
|
+
- `runtime_name`, `runtime_version`: Runtime details (e.g., Python 3.11.6)
|
|
90
|
+
- `packages`: Hash of installed package names and versions
|
|
91
|
+
- **`NotFoundError`**: New error class for 404 responses (e.g., container not found)
|
|
92
|
+
|
|
93
|
+
### Changed
|
|
94
|
+
|
|
95
|
+
- Renamed `ExecutionError` to `RunError` to match new terminology
|
|
96
|
+
- Updated error handling to include 404 Not Found responses
|
|
97
|
+
- `run()` method is now focused solely on code execution in pre-setup containers
|
|
98
|
+
|
|
99
|
+
### Migration Guide
|
|
100
|
+
|
|
101
|
+
**Before (v0.1.0):**
|
|
102
|
+
```ruby
|
|
103
|
+
result = client.run(
|
|
104
|
+
identifier: "session-123",
|
|
105
|
+
language: "python",
|
|
106
|
+
files: [{ path: "main.py", content: "print('hi')" }],
|
|
107
|
+
entrypoint: "main.py"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
puts result.container_id # "runbox-session-123-python"
|
|
111
|
+
puts result.cached? # true/false
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**After (v1.0.0):**
|
|
115
|
+
```ruby
|
|
116
|
+
# Step 1: Setup container
|
|
117
|
+
setup = client.setup(
|
|
118
|
+
identifier: "session-123",
|
|
119
|
+
language: "python"
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
puts setup.container_id # "runbox-session-123-python"
|
|
123
|
+
puts setup.cached? # true/false
|
|
124
|
+
puts setup.environment_snapshot.runtime_version # "3.11.6"
|
|
125
|
+
|
|
126
|
+
# Step 2: Run code
|
|
127
|
+
result = client.run(
|
|
128
|
+
container_id: setup.container_id,
|
|
129
|
+
files: [{ path: "main.py", content: "print('hi')" }],
|
|
130
|
+
entrypoint: "main.py"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# Note: result no longer has container_id or cached fields
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## [0.1.0] - 2024-12-01
|
|
137
|
+
|
|
138
|
+
### Added
|
|
139
|
+
|
|
140
|
+
- Initial release
|
|
141
|
+
- `run()` method for executing code in isolated containers
|
|
142
|
+
- Support for Python, Ruby, and Shell languages
|
|
143
|
+
- Container reuse via identifiers
|
|
144
|
+
- Environment variables, timeouts, memory limits, network allowlisting
|
|
145
|
+
- `delete_containers()` method for cleanup
|
|
146
|
+
- `health()` method for health checks
|
|
147
|
+
- Comprehensive error handling
|
|
148
|
+
- Configuration via environment variables or config block
|
data/README.md
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
## README.md
|
|
2
|
+
|
|
3
|
+
# Runbox Ruby Client
|
|
4
|
+
|
|
5
|
+
Official Ruby client for [Runbox](https://github.com/anywaye/runbox) - a fast, secure API for running code in isolated containers.
|
|
6
|
+
|
|
7
|
+
[](https://badge.fury.io/rb/runbox)
|
|
8
|
+
[](https://github.com/anywaye/runbox-rb/actions/workflows/ci.yml)
|
|
9
|
+
[](https://opensource.org/licenses/MIT)
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
Add to your Gemfile:
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
gem "runbox"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Or install directly:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
gem install runbox
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quick Start
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
require "runbox"
|
|
29
|
+
|
|
30
|
+
# Configure (or use environment variables)
|
|
31
|
+
Runbox.configure do |config|
|
|
32
|
+
config.url = "http://localhost:8080"
|
|
33
|
+
config.api_key = "your-api-key"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Create a client
|
|
37
|
+
client = Runbox::Client.new
|
|
38
|
+
|
|
39
|
+
# Step 1: Set up a container and get environment info
|
|
40
|
+
setup = client.setup(
|
|
41
|
+
identifier: "my-session",
|
|
42
|
+
language: "python"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
puts setup.container_id # => "runbox-my-session-python"
|
|
46
|
+
puts setup.environment_snapshot.runtime_version # => "3.11.6"
|
|
47
|
+
puts setup.environment_snapshot.packages["requests"] # => "2.31.0"
|
|
48
|
+
|
|
49
|
+
# Step 2: Run code in the container
|
|
50
|
+
result = client.run(
|
|
51
|
+
container_id: setup.container_id,
|
|
52
|
+
files: [{ path: "main.py", content: "print('Hello!')" }],
|
|
53
|
+
run_command: "python main.py"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
puts result.stdout # => "Hello!\n"
|
|
57
|
+
puts result.success? # => true
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Configuration
|
|
61
|
+
|
|
62
|
+
### Using Environment Variables
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
export RUNBOX_URL=http://localhost:8080
|
|
66
|
+
export RUNBOX_API_KEY=your-api-key
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Using Configuration Block
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
Runbox.configure do |config|
|
|
73
|
+
config.url = "http://localhost:8080"
|
|
74
|
+
config.api_key = "your-api-key"
|
|
75
|
+
config.timeout = 60 # HTTP timeout
|
|
76
|
+
end
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Per-Client Configuration
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
client = Runbox::Client.new(
|
|
83
|
+
url: "http://localhost:8080",
|
|
84
|
+
api_key: "your-api-key"
|
|
85
|
+
)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Usage
|
|
89
|
+
|
|
90
|
+
### Setting Up a Container
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
setup = client.setup(
|
|
94
|
+
identifier: "session-123",
|
|
95
|
+
language: "python",
|
|
96
|
+
env: { "API_KEY" => "secret" }, # Optional: set environment variables
|
|
97
|
+
memory: "512m", # Optional: memory limit
|
|
98
|
+
network_allow: ["api.stripe.com"] # Optional: allowed network destinations
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Access environment information
|
|
102
|
+
puts setup.container_id # => "runbox-session-123-python"
|
|
103
|
+
puts setup.cached? # => false (true if container was reused)
|
|
104
|
+
|
|
105
|
+
env = setup.environment_snapshot
|
|
106
|
+
puts env.os_name # => "debian"
|
|
107
|
+
puts env.os_version # => "12"
|
|
108
|
+
puts env.runtime_name # => "python"
|
|
109
|
+
puts env.runtime_version # => "3.11.6"
|
|
110
|
+
puts env.packages # => {"pip" => "23.0.1", "requests" => "2.31.0", ...}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Running Code
|
|
114
|
+
|
|
115
|
+
```ruby
|
|
116
|
+
result = client.run(
|
|
117
|
+
container_id: "runbox-session-123-python",
|
|
118
|
+
files: [{ path: "main.py", content: "print('Hello!')" }],
|
|
119
|
+
run_command: "python main.py",
|
|
120
|
+
env: { "DEBUG" => "true" }, # Optional: runtime environment variables
|
|
121
|
+
timeout: 30 # Optional: execution timeout in seconds
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
puts result.success? # => true
|
|
125
|
+
puts result.exit_code # => 0
|
|
126
|
+
puts result.stdout # => "Hello!\n"
|
|
127
|
+
puts result.stderr # => ""
|
|
128
|
+
puts result.execution_time_ms # => 42
|
|
129
|
+
puts result.timeout? # => false
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Installing Dependencies On-The-Fly
|
|
133
|
+
|
|
134
|
+
Install new dependencies before running code:
|
|
135
|
+
|
|
136
|
+
```ruby
|
|
137
|
+
# Python example
|
|
138
|
+
result = client.run(
|
|
139
|
+
container_id: container_id,
|
|
140
|
+
files: [{ path: "main.py", content: "import requests; print(requests.__version__)" }],
|
|
141
|
+
run_command: "python main.py",
|
|
142
|
+
new_dependencies: ["requests==2.31.0", "pytest"]
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
puts result.packages # => {"pip"=>"23.0.1", "requests"=>"2.31.0", "pytest"=>"7.4.0", ...}
|
|
146
|
+
|
|
147
|
+
# Ruby example
|
|
148
|
+
result = client.run(
|
|
149
|
+
container_id: container_id,
|
|
150
|
+
files: [{ path: "main.rb", content: "require 'rake'; puts Rake::VERSION" }],
|
|
151
|
+
run_command: "ruby main.rb",
|
|
152
|
+
new_dependencies: ["rake", "rspec"]
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# Shell example (uses apk)
|
|
156
|
+
result = client.run(
|
|
157
|
+
container_id: container_id,
|
|
158
|
+
files: [{ path: "script.sh", content: "#!/bin/sh\ncurl --version" }],
|
|
159
|
+
run_command: "sh script.sh",
|
|
160
|
+
new_dependencies: ["curl", "jq", "git"]
|
|
161
|
+
)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
**Note:** The `packages` field is only included in the result when `new_dependencies` are provided.
|
|
165
|
+
|
|
166
|
+
### Cleanup Containers
|
|
167
|
+
|
|
168
|
+
```ruby
|
|
169
|
+
deleted = client.delete_containers("session-123")
|
|
170
|
+
puts deleted # => ["runbox-session-123-python"]
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Check Health
|
|
174
|
+
|
|
175
|
+
```ruby
|
|
176
|
+
health = client.health
|
|
177
|
+
puts health[:status] # => "healthy"
|
|
178
|
+
puts health[:version] # => "1.0.0"
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## API Reference
|
|
182
|
+
|
|
183
|
+
### `client.setup(identifier:, language:, **options)`
|
|
184
|
+
|
|
185
|
+
Set up a container and get environment information.
|
|
186
|
+
|
|
187
|
+
**Parameters:**
|
|
188
|
+
- `identifier` (String, required): Unique identifier for container reuse
|
|
189
|
+
- `language` (String, required): Programming language (`"python"`, `"ruby"`, `"shell"`)
|
|
190
|
+
- `env` (Hash, optional): Environment variables to set
|
|
191
|
+
- `timeout` (Integer, optional): Default timeout in seconds
|
|
192
|
+
- `memory` (String, optional): Memory limit (e.g., `"256m"`, `"1g"`)
|
|
193
|
+
- `network_allow` (Array<String>, optional): Allowed network destinations
|
|
194
|
+
|
|
195
|
+
**Returns:** `SetupResult` with:
|
|
196
|
+
- `container_id`: Container ID to use in `run()` calls
|
|
197
|
+
- `cached?`: Whether container was reused
|
|
198
|
+
- `environment_snapshot`: Environment information (OS, runtime, packages)
|
|
199
|
+
|
|
200
|
+
### `client.run(container_id:, files:, run_command:, **options)`
|
|
201
|
+
|
|
202
|
+
Run code in a container that was set up via `setup()`.
|
|
203
|
+
|
|
204
|
+
**Parameters:**
|
|
205
|
+
- `container_id` (String, required): Container ID from `setup()` response
|
|
206
|
+
- `files` (Array<Hash>, required): Files to write before running
|
|
207
|
+
- `run_command` (String, required): Command to run
|
|
208
|
+
- `env` (Hash, optional): Runtime environment variables
|
|
209
|
+
- `timeout` (Integer, optional): Execution timeout in seconds
|
|
210
|
+
|
|
211
|
+
**Returns:** `Result` with:
|
|
212
|
+
- `success?`: Whether run succeeded (exit code 0)
|
|
213
|
+
- `exit_code`: Process exit code
|
|
214
|
+
- `stdout`: Standard output
|
|
215
|
+
- `stderr`: Standard error
|
|
216
|
+
- `execution_time_ms`: Execution time in milliseconds
|
|
217
|
+
- `timeout?`: Whether run timed out
|
|
218
|
+
|
|
219
|
+
## Error Handling
|
|
220
|
+
|
|
221
|
+
```ruby
|
|
222
|
+
begin
|
|
223
|
+
setup = client.setup(identifier: "test", language: "python")
|
|
224
|
+
result = client.run(
|
|
225
|
+
container_id: setup.container_id,
|
|
226
|
+
files: [{ path: "main.py", content: "print('hi')" }],
|
|
227
|
+
run_command: "python main.py"
|
|
228
|
+
)
|
|
229
|
+
rescue Runbox::AuthenticationError => e
|
|
230
|
+
puts "Invalid API key"
|
|
231
|
+
rescue Runbox::NotFoundError => e
|
|
232
|
+
puts "Container not found (did you call setup first?)"
|
|
233
|
+
rescue Runbox::ValidationError => e
|
|
234
|
+
puts "Invalid request: #{e.message}"
|
|
235
|
+
rescue Runbox::RunError => e
|
|
236
|
+
puts "Run failed: #{e.message}"
|
|
237
|
+
rescue Runbox::ConnectionError => e
|
|
238
|
+
puts "Could not connect to Runbox"
|
|
239
|
+
end
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## Development
|
|
243
|
+
|
|
244
|
+
### Running Tests
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
bundle install
|
|
248
|
+
bundle exec rake test
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Running Integration Tests
|
|
252
|
+
|
|
253
|
+
Integration tests require a running Runbox server:
|
|
254
|
+
|
|
255
|
+
```bash
|
|
256
|
+
# Start Runbox server (in another terminal)
|
|
257
|
+
cd ../runbox
|
|
258
|
+
docker-compose up
|
|
259
|
+
|
|
260
|
+
# Run integration tests
|
|
261
|
+
export RUNBOX_URL=http://localhost:8080
|
|
262
|
+
export RUNBOX_API_KEY=your-api-key
|
|
263
|
+
ruby examples/integration_test.rb
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### Linting
|
|
267
|
+
|
|
268
|
+
```bash
|
|
269
|
+
bundle exec rubocop
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### CI/CD
|
|
273
|
+
|
|
274
|
+
- **CI**: Runs on every push and PR
|
|
275
|
+
- Tests on Ruby 3.1, 3.2, 3.3
|
|
276
|
+
- Linting with RuboCop
|
|
277
|
+
- Integration tests against live Runbox server
|
|
278
|
+
|
|
279
|
+
- **CD**: Publishes to RubyGems on version tags
|
|
280
|
+
- Create a tag: `git tag v1.0.0 && git push --tags`
|
|
281
|
+
- Requires `RUBYGEMS_API_KEY` secret in GitHub
|
|
282
|
+
|
|
283
|
+
## License
|
|
284
|
+
|
|
285
|
+
MIT License - see [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "faraday"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module Runbox
|
|
7
|
+
class Client
|
|
8
|
+
attr_reader :url, :api_key
|
|
9
|
+
|
|
10
|
+
def initialize(url: nil, api_key: nil, timeout: nil, open_timeout: nil)
|
|
11
|
+
config = Runbox.configuration
|
|
12
|
+
@url = url || config.url
|
|
13
|
+
@api_key = api_key || config.api_key
|
|
14
|
+
@timeout = timeout || config.timeout
|
|
15
|
+
@open_timeout = open_timeout || config.open_timeout
|
|
16
|
+
|
|
17
|
+
validate_configuration!
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Set up a container and get environment information
|
|
21
|
+
#
|
|
22
|
+
# @param identifier [String] Unique identifier for container reuse
|
|
23
|
+
# @param language [String] Programming language (python, ruby, shell)
|
|
24
|
+
# @param env [Hash] Environment variables to set
|
|
25
|
+
# @param timeout [Integer] Default timeout in seconds
|
|
26
|
+
# @param memory [String] Memory limit (e.g., "256m")
|
|
27
|
+
# @param network_allow [Array<String>] Allowed network destinations
|
|
28
|
+
# @return [SetupResult] Setup result with container_id and environment_snapshot
|
|
29
|
+
def setup(identifier:, language:, env: {}, timeout: nil, memory: nil, network_allow: nil)
|
|
30
|
+
payload = {
|
|
31
|
+
identifier: identifier,
|
|
32
|
+
language: language,
|
|
33
|
+
env: env
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
payload[:timeout] = timeout if timeout
|
|
37
|
+
payload[:memory] = memory if memory
|
|
38
|
+
payload[:network_allow] = network_allow if network_allow
|
|
39
|
+
|
|
40
|
+
response = post("/v1/setup", payload)
|
|
41
|
+
SetupResult.new(response)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Run code in a container that was set up via setup()
|
|
45
|
+
#
|
|
46
|
+
# @param container_id [String] Container ID from setup response
|
|
47
|
+
# @param files [Array<Hash>] Files to write before running
|
|
48
|
+
# @param run_command [String] Command to execute
|
|
49
|
+
# @param env [Hash] Runtime environment variables
|
|
50
|
+
# @param timeout [Integer] Timeout in seconds
|
|
51
|
+
# @param new_dependencies [Array<String>] New dependencies to install before running
|
|
52
|
+
# @return [Result] Run result with stdout, stderr, exit_code, and optional packages
|
|
53
|
+
def run(container_id:, files:, run_command:, env: {}, timeout: nil, new_dependencies: nil)
|
|
54
|
+
payload = {
|
|
55
|
+
container_id: container_id,
|
|
56
|
+
files: normalize_files(files),
|
|
57
|
+
run_command: run_command,
|
|
58
|
+
env: env
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
payload[:timeout] = timeout if timeout
|
|
62
|
+
payload[:new_dependencies] = new_dependencies if new_dependencies
|
|
63
|
+
|
|
64
|
+
response = post("/v1/run", payload)
|
|
65
|
+
Result.new(response)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def delete_containers(identifier)
|
|
69
|
+
response = delete("/v1/containers/#{identifier}")
|
|
70
|
+
response["deleted"] || []
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def health
|
|
74
|
+
response = get("/v1/health", auth: false)
|
|
75
|
+
{
|
|
76
|
+
status: response["status"],
|
|
77
|
+
version: response["version"]
|
|
78
|
+
}
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
def validate_configuration!
|
|
84
|
+
raise ConfigurationError, "URL is required" if @url.nil? || @url.empty?
|
|
85
|
+
raise ConfigurationError, "API key is required" if @api_key.nil? || @api_key.empty?
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def normalize_files(files)
|
|
89
|
+
files.map do |file|
|
|
90
|
+
if file.is_a?(Hash)
|
|
91
|
+
{ path: file[:path] || file["path"], content: file[:content] || file["content"] }
|
|
92
|
+
else
|
|
93
|
+
file
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def connection
|
|
99
|
+
@connection ||= Faraday.new(url: @url) do |f|
|
|
100
|
+
f.request :json
|
|
101
|
+
f.response :json
|
|
102
|
+
f.options.timeout = @timeout
|
|
103
|
+
f.options.open_timeout = @open_timeout
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def get(path, auth: true)
|
|
108
|
+
response = connection.get(path) do |req|
|
|
109
|
+
req.headers["Authorization"] = "Bearer #{@api_key}" if auth
|
|
110
|
+
end
|
|
111
|
+
handle_response(response)
|
|
112
|
+
rescue Faraday::ConnectionFailed, Faraday::TimeoutError => e
|
|
113
|
+
raise ConnectionError, "Failed to connect to Runbox: #{e.message}"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def post(path, body)
|
|
117
|
+
response = connection.post(path) do |req|
|
|
118
|
+
req.headers["Authorization"] = "Bearer #{@api_key}"
|
|
119
|
+
req.body = body
|
|
120
|
+
end
|
|
121
|
+
handle_response(response)
|
|
122
|
+
rescue Faraday::ConnectionFailed, Faraday::TimeoutError => e
|
|
123
|
+
raise ConnectionError, "Failed to connect to Runbox: #{e.message}"
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def delete(path)
|
|
127
|
+
response = connection.delete(path) do |req|
|
|
128
|
+
req.headers["Authorization"] = "Bearer #{@api_key}"
|
|
129
|
+
end
|
|
130
|
+
handle_response(response)
|
|
131
|
+
rescue Faraday::ConnectionFailed, Faraday::TimeoutError => e
|
|
132
|
+
raise ConnectionError, "Failed to connect to Runbox: #{e.message}"
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def handle_response(response)
|
|
136
|
+
case response.status
|
|
137
|
+
when 200..299
|
|
138
|
+
response.body
|
|
139
|
+
when 401
|
|
140
|
+
raise AuthenticationError, "Invalid API key"
|
|
141
|
+
when 404
|
|
142
|
+
body = response.body || {}
|
|
143
|
+
raise NotFoundError, body["detail"] || "Container not found"
|
|
144
|
+
when 400
|
|
145
|
+
body = response.body || {}
|
|
146
|
+
raise ValidationError.new(body["detail"] || "Validation error", details: body)
|
|
147
|
+
when 422
|
|
148
|
+
body = response.body || {}
|
|
149
|
+
raise ValidationError.new(body["detail"] || "Validation error", details: body)
|
|
150
|
+
when 500..599
|
|
151
|
+
body = response.body || {}
|
|
152
|
+
raise RunError, body["detail"] || "Run failed"
|
|
153
|
+
else
|
|
154
|
+
raise Error, "Unexpected response: #{response.status}"
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Runbox
|
|
4
|
+
class Configuration
|
|
5
|
+
attr_accessor :url, :api_key, :timeout, :open_timeout
|
|
6
|
+
|
|
7
|
+
def initialize
|
|
8
|
+
@url = ENV.fetch("RUNBOX_URL", "http://localhost:8080")
|
|
9
|
+
@api_key = ENV.fetch("RUNBOX_API_KEY", nil)
|
|
10
|
+
@timeout = 120 # HTTP timeout (longer than execution timeout)
|
|
11
|
+
@open_timeout = 10
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def validate!
|
|
15
|
+
raise ConfigurationError, "API key is required" if api_key.nil? || api_key.empty?
|
|
16
|
+
raise ConfigurationError, "URL is required" if url.nil? || url.empty?
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
data/lib/runbox/error.rb
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Runbox
|
|
4
|
+
# Base error class
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
|
|
7
|
+
# Configuration errors
|
|
8
|
+
class ConfigurationError < Error; end
|
|
9
|
+
|
|
10
|
+
# Connection errors
|
|
11
|
+
class ConnectionError < Error; end
|
|
12
|
+
|
|
13
|
+
# Authentication errors (401)
|
|
14
|
+
class AuthenticationError < Error; end
|
|
15
|
+
|
|
16
|
+
# Not found errors (404)
|
|
17
|
+
class NotFoundError < Error; end
|
|
18
|
+
|
|
19
|
+
# Validation errors (400)
|
|
20
|
+
class ValidationError < Error
|
|
21
|
+
attr_reader :details
|
|
22
|
+
|
|
23
|
+
def initialize(message, details: nil)
|
|
24
|
+
super(message)
|
|
25
|
+
@details = details
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Run errors (500)
|
|
30
|
+
class RunError < Error; end
|
|
31
|
+
|
|
32
|
+
# Timeout errors
|
|
33
|
+
class TimeoutError < Error; end
|
|
34
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Runbox
|
|
4
|
+
class Result
|
|
5
|
+
attr_reader :success, :exit_code, :stdout, :stderr,
|
|
6
|
+
:execution_time_ms, :timeout_exceeded, :packages
|
|
7
|
+
|
|
8
|
+
def initialize(data)
|
|
9
|
+
@success = data["success"] || data[:success]
|
|
10
|
+
@exit_code = data["exit_code"] || data[:exit_code]
|
|
11
|
+
@stdout = data["stdout"] || data[:stdout] || ""
|
|
12
|
+
@stderr = data["stderr"] || data[:stderr] || ""
|
|
13
|
+
@execution_time_ms = data["execution_time_ms"] || data[:execution_time_ms]
|
|
14
|
+
@timeout_exceeded = data["timeout_exceeded"] || data[:timeout_exceeded] || false
|
|
15
|
+
@packages = data["packages"] || data[:packages]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def success?
|
|
19
|
+
@success == true
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def failed?
|
|
23
|
+
!success?
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def timeout?
|
|
27
|
+
@timeout_exceeded == true
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def to_h
|
|
31
|
+
hash = {
|
|
32
|
+
success: @success,
|
|
33
|
+
exit_code: @exit_code,
|
|
34
|
+
stdout: @stdout,
|
|
35
|
+
stderr: @stderr,
|
|
36
|
+
execution_time_ms: @execution_time_ms,
|
|
37
|
+
timeout_exceeded: @timeout_exceeded
|
|
38
|
+
}
|
|
39
|
+
hash[:packages] = @packages if @packages
|
|
40
|
+
hash
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Runbox
|
|
4
|
+
class SetupResult
|
|
5
|
+
attr_reader :container_id, :cached, :environment_snapshot
|
|
6
|
+
|
|
7
|
+
def initialize(data)
|
|
8
|
+
@container_id = data["container_id"] || data[:container_id]
|
|
9
|
+
@cached = data["cached"] || data[:cached] || false
|
|
10
|
+
@environment_snapshot = EnvironmentSnapshot.new(
|
|
11
|
+
data["environment_snapshot"] || data[:environment_snapshot] || {}
|
|
12
|
+
)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def cached?
|
|
16
|
+
@cached == true
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def to_h
|
|
20
|
+
{
|
|
21
|
+
container_id: @container_id,
|
|
22
|
+
cached: @cached,
|
|
23
|
+
environment_snapshot: @environment_snapshot.to_h
|
|
24
|
+
}
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
class EnvironmentSnapshot
|
|
29
|
+
attr_reader :os_name, :os_version, :runtime_name, :runtime_version, :packages
|
|
30
|
+
|
|
31
|
+
def initialize(data)
|
|
32
|
+
@os_name = data["os_name"] || data[:os_name]
|
|
33
|
+
@os_version = data["os_version"] || data[:os_version]
|
|
34
|
+
@runtime_name = data["runtime_name"] || data[:runtime_name]
|
|
35
|
+
@runtime_version = data["runtime_version"] || data[:runtime_version]
|
|
36
|
+
@packages = data["packages"] || data[:packages] || {}
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def to_h
|
|
40
|
+
{
|
|
41
|
+
os_name: @os_name,
|
|
42
|
+
os_version: @os_version,
|
|
43
|
+
runtime_name: @runtime_name,
|
|
44
|
+
runtime_version: @runtime_version,
|
|
45
|
+
packages: @packages
|
|
46
|
+
}
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
data/lib/runbox.rb
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "runbox/version"
|
|
4
|
+
require_relative "runbox/configuration"
|
|
5
|
+
require_relative "runbox/error"
|
|
6
|
+
require_relative "runbox/result"
|
|
7
|
+
require_relative "runbox/setup_result"
|
|
8
|
+
require_relative "runbox/client"
|
|
9
|
+
|
|
10
|
+
module Runbox
|
|
11
|
+
class << self
|
|
12
|
+
attr_writer :configuration
|
|
13
|
+
|
|
14
|
+
def configuration
|
|
15
|
+
@configuration ||= Configuration.new
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def configure
|
|
19
|
+
yield(configuration)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def reset_configuration!
|
|
23
|
+
@configuration = Configuration.new
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
metadata
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: runbox
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.2.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Anywaye
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: faraday
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '2.9'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '2.9'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: minitest
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '5.21'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '5.21'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rake
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '13.0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '13.0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: rubocop
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '1.60'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '1.60'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: webmock
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '3.19'
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '3.19'
|
|
82
|
+
description: Official Ruby client for Runbox, a fast and secure API for running code
|
|
83
|
+
in isolated containers.
|
|
84
|
+
email:
|
|
85
|
+
- hello@anywaye.com
|
|
86
|
+
executables: []
|
|
87
|
+
extensions: []
|
|
88
|
+
extra_rdoc_files: []
|
|
89
|
+
files:
|
|
90
|
+
- CHANGELOG.md
|
|
91
|
+
- README.md
|
|
92
|
+
- lib/runbox.rb
|
|
93
|
+
- lib/runbox/client.rb
|
|
94
|
+
- lib/runbox/configuration.rb
|
|
95
|
+
- lib/runbox/error.rb
|
|
96
|
+
- lib/runbox/result.rb
|
|
97
|
+
- lib/runbox/setup_result.rb
|
|
98
|
+
- lib/runbox/version.rb
|
|
99
|
+
homepage: https://github.com/anywaye/runbox-rb
|
|
100
|
+
licenses:
|
|
101
|
+
- MIT
|
|
102
|
+
metadata:
|
|
103
|
+
homepage_uri: https://github.com/anywaye/runbox-rb
|
|
104
|
+
source_code_uri: https://github.com/anywaye/runbox-rb
|
|
105
|
+
changelog_uri: https://github.com/anywaye/runbox-rb/blob/main/CHANGELOG.md
|
|
106
|
+
rdoc_options: []
|
|
107
|
+
require_paths:
|
|
108
|
+
- lib
|
|
109
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
110
|
+
requirements:
|
|
111
|
+
- - ">="
|
|
112
|
+
- !ruby/object:Gem::Version
|
|
113
|
+
version: 3.1.0
|
|
114
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
115
|
+
requirements:
|
|
116
|
+
- - ">="
|
|
117
|
+
- !ruby/object:Gem::Version
|
|
118
|
+
version: '0'
|
|
119
|
+
requirements: []
|
|
120
|
+
rubygems_version: 4.0.3
|
|
121
|
+
specification_version: 4
|
|
122
|
+
summary: Ruby client for Runbox - secure code execution API
|
|
123
|
+
test_files: []
|