hackerrun 0.1.0
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.
- package/.claude/settings.local.json +22 -0
- package/.env.example +9 -0
- package/CLAUDE.md +532 -0
- package/README.md +94 -0
- package/dist/index.js +2813 -0
- package/package.json +38 -0
- package/src/commands/app.ts +394 -0
- package/src/commands/builds.ts +314 -0
- package/src/commands/config.ts +129 -0
- package/src/commands/connect.ts +197 -0
- package/src/commands/deploy.ts +227 -0
- package/src/commands/env.ts +174 -0
- package/src/commands/login.ts +120 -0
- package/src/commands/logs.ts +97 -0
- package/src/index.ts +43 -0
- package/src/lib/app-config.ts +95 -0
- package/src/lib/cluster.ts +428 -0
- package/src/lib/config.ts +137 -0
- package/src/lib/platform-auth.ts +20 -0
- package/src/lib/platform-client.ts +637 -0
- package/src/lib/platform.ts +87 -0
- package/src/lib/ssh-cert.ts +264 -0
- package/src/lib/uncloud-runner.ts +342 -0
- package/src/lib/uncloud.ts +149 -0
- package/tsconfig.json +17 -0
- package/tsup.config.ts +17 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"WebFetch(domain:uncloud.run)",
|
|
5
|
+
"WebFetch(domain:www.ubicloud.com)",
|
|
6
|
+
"Bash(grep:*)",
|
|
7
|
+
"Bash(ssh root@46.4.244.246 \"jool -i hackerrun pool4 add 46.4.244.246 0-65535 --icmp\")",
|
|
8
|
+
"Bash(ssh root@46.4.244.246 \"jool -i hackerrun stats display\")",
|
|
9
|
+
"Bash(ssh root@46.4.244.246 \"ip -6 route show | grep 64:ff9b\")",
|
|
10
|
+
"Bash(ssh root@46.4.244.246 \"ip6tables -L -v -n\")",
|
|
11
|
+
"Bash(ssh:*)",
|
|
12
|
+
"Bash(find:*)",
|
|
13
|
+
"Bash(npm install:*)",
|
|
14
|
+
"Bash(npm run build:*)",
|
|
15
|
+
"Bash(node dist/index.js:*)",
|
|
16
|
+
"Bash(npx prisma migrate:*)",
|
|
17
|
+
"Bash(ls:*)",
|
|
18
|
+
"Bash(npm ls:*)",
|
|
19
|
+
"Bash(npx prisma generate:*)"
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
}
|
package/.env.example
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Ubicloud API Configuration
|
|
2
|
+
UBICLOUD_API_TOKEN=your_api_token_here
|
|
3
|
+
UBICLOUD_PROJECT_ID=your_project_id_here
|
|
4
|
+
|
|
5
|
+
# Optional: Default values
|
|
6
|
+
# Available locations: eu-central-h1, eu-north-h1, us-east-a2
|
|
7
|
+
UBICLOUD_DEFAULT_LOCATION=us-east-a2
|
|
8
|
+
UBICLOUD_DEFAULT_SIZE=standard-2
|
|
9
|
+
UBICLOUD_DEFAULT_IMAGE=ubuntu-noble
|
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
# Hackerrun CLI
|
|
2
|
+
|
|
3
|
+
CLI tool for deploying apps using Ubicloud VMs + Uncloud container orchestration.
|
|
4
|
+
|
|
5
|
+
## Related Projects
|
|
6
|
+
|
|
7
|
+
- `../hackerrun-platform` - Platform API (Next.js)
|
|
8
|
+
- `../uncloud` - Container orchestration (not our code)
|
|
9
|
+
|
|
10
|
+
## Domains
|
|
11
|
+
|
|
12
|
+
| Domain | Purpose |
|
|
13
|
+
|--------|---------|
|
|
14
|
+
| `hackerrun.com` | Platform website, docs, dashboard |
|
|
15
|
+
| `hackerrun.app` | User deployed applications |
|
|
16
|
+
|
|
17
|
+
## User Experience
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
$ hackerrun login # One-time GitHub OAuth
|
|
21
|
+
$ cd myapp
|
|
22
|
+
$ hackerrun deploy # App live at https://laughing-buddha.hackerrun.app
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### App Naming
|
|
26
|
+
|
|
27
|
+
| Identifier | Scope | Example |
|
|
28
|
+
|------------|-------|---------|
|
|
29
|
+
| **App Name** | Per user | `myapp` |
|
|
30
|
+
| **Domain Name** | Global | `laughing-buddha` (Railway-style random) |
|
|
31
|
+
|
|
32
|
+
### Domain Format
|
|
33
|
+
|
|
34
|
+
- Single service: `{domain}.hackerrun.app`
|
|
35
|
+
- Multi-service: `{domain}-{service}.hackerrun.app` (flattened for Cloudflare wildcard SSL)
|
|
36
|
+
|
|
37
|
+
### Config Files
|
|
38
|
+
|
|
39
|
+
- **`.hackerrun`** - Links folder to app (contains app name)
|
|
40
|
+
- **`hackerrun.yml`** - Production compose with `x-ports` for public exposure
|
|
41
|
+
- **`docker-compose.yml`** - Local dev (ignored by hackerrun)
|
|
42
|
+
|
|
43
|
+
## Commands
|
|
44
|
+
|
|
45
|
+
| Command | Description |
|
|
46
|
+
|---------|-------------|
|
|
47
|
+
| `hackerrun login` | Auth via GitHub |
|
|
48
|
+
| `hackerrun deploy` | Deploy app |
|
|
49
|
+
| `hackerrun apps` | List apps |
|
|
50
|
+
| `hackerrun logs <app>` | View logs |
|
|
51
|
+
| `hackerrun ssh <app>` | SSH into VM |
|
|
52
|
+
| `hackerrun destroy <app>` | Delete app |
|
|
53
|
+
| `hackerrun link <app>` | Link folder to existing app |
|
|
54
|
+
| `hackerrun unlink` | Remove `.hackerrun` |
|
|
55
|
+
| `hackerrun rename --app <old> <new>` | Rename app |
|
|
56
|
+
| `hackerrun domain --app <app> <new>` | Change domain |
|
|
57
|
+
|
|
58
|
+
## Architecture: IPv4 Cost Optimization
|
|
59
|
+
|
|
60
|
+
**Problem:** Ubicloud charges $3/mo per public IPv4.
|
|
61
|
+
|
|
62
|
+
**Solution:** Single Gateway ($3/mo fixed) handles all traffic:
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
INBOUND: User → Cloudflare → Tunnel → Gateway (Caddy) → App VMs (IPv6-only)
|
|
66
|
+
OUTBOUND: Container → DNS64 → NAT64 → IPv4 Internet
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Gateway Components
|
|
70
|
+
|
|
71
|
+
| Component | Purpose |
|
|
72
|
+
|-----------|---------|
|
|
73
|
+
| cloudflared | Tunnel endpoint (no inbound ports exposed) |
|
|
74
|
+
| Caddy | Routes by hostname to App VMs |
|
|
75
|
+
| BIND9 | DNS64 (synthesizes AAAA for IPv4 hosts) |
|
|
76
|
+
| Jool | NAT64 (translates 64:ff9b::/96 to IPv4) |
|
|
77
|
+
| WireGuard | NAT64 tunnel from App VMs |
|
|
78
|
+
|
|
79
|
+
### Gateway Details
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
IPv4: 46.4.244.246 | IPv6: 2a01:4f8:2b01:1317:e830::2
|
|
83
|
+
Location: eu-central-h1 | SSH: ssh root@46.4.244.246
|
|
84
|
+
Tunnel: 29a25987-06a5-425e-bbed-b090492a4e71
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### x-ports Protocol
|
|
88
|
+
|
|
89
|
+
Use `/http` (Cloudflare terminates external HTTPS):
|
|
90
|
+
|
|
91
|
+
```yaml
|
|
92
|
+
services:
|
|
93
|
+
app:
|
|
94
|
+
x-ports:
|
|
95
|
+
- "3000/http" # Single service
|
|
96
|
+
- "laughing-buddha-api.hackerrun.app:8080/http" # Multi-service
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## SSH Authentication: Temporary Certificates
|
|
100
|
+
|
|
101
|
+
Users don't manage SSH keys. Instead, CLI gets short-lived certificates signed by platform CA.
|
|
102
|
+
|
|
103
|
+
### Architecture
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
107
|
+
│ PLATFORM API │
|
|
108
|
+
│ │
|
|
109
|
+
│ SSH CA Private Key (signs certificates) │
|
|
110
|
+
│ Platform SSH Key (for VM creation + admin access) │
|
|
111
|
+
│ │
|
|
112
|
+
│ POST /api/apps/:appName/ssh-certificate │
|
|
113
|
+
│ → Signs short-lived cert (5 min TTL) │
|
|
114
|
+
│ → Returns certificate to CLI │
|
|
115
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
116
|
+
│
|
|
117
|
+
▼
|
|
118
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
119
|
+
│ CLI │
|
|
120
|
+
│ │
|
|
121
|
+
│ 1. Generate ephemeral keypair │
|
|
122
|
+
│ 2. Request cert from platform │
|
|
123
|
+
│ 3. Load key+cert into temp SSH agent │
|
|
124
|
+
│ 4. Run uc --connect ssh+cli://root@[vm-ip] <command> │
|
|
125
|
+
│ 5. Cleanup (kill agent, delete temp files) │
|
|
126
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
127
|
+
│ │
|
|
128
|
+
│ IPv6 (direct) │ IPv4 (tunnel via gateway)
|
|
129
|
+
▼ ▼
|
|
130
|
+
┌─────────────────┐ ┌─────────────────┐
|
|
131
|
+
│ App VM │ │ Gateway │
|
|
132
|
+
│ Trusts CA │ │ Trusts CA │
|
|
133
|
+
│ │ │ │ │
|
|
134
|
+
│ TrustedUserCA │ │ ▼ │
|
|
135
|
+
│ Keys = ca.pub │ │ App VM │
|
|
136
|
+
└─────────────────┘ └─────────────────┘
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### VM Authentication Setup
|
|
140
|
+
|
|
141
|
+
When creating app VMs via Ubicloud:
|
|
142
|
+
|
|
143
|
+
| Key | Location on VM | Purpose |
|
|
144
|
+
|-----|----------------|---------|
|
|
145
|
+
| Platform SSH public key | `/root/.ssh/authorized_keys` | Ubicloud requirement + admin access |
|
|
146
|
+
| CA public key | `/etc/ssh/hackerrun-ca.pub` | User certificate validation |
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
# /etc/ssh/sshd_config.d/hackerrun-ca.conf
|
|
150
|
+
TrustedUserCAKeys /etc/ssh/hackerrun-ca.pub
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### IPv6 vs IPv4 Connectivity
|
|
154
|
+
|
|
155
|
+
CLI detects network and handles automatically (same logic as current `hackerrun ssh`):
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
// Test IPv6 connectivity (3s timeout)
|
|
159
|
+
let useProxy = false;
|
|
160
|
+
try {
|
|
161
|
+
execSync(`ssh -o ConnectTimeout=3 root@${vmIpv6} exit`, { timeout: 5000 });
|
|
162
|
+
} catch {
|
|
163
|
+
useProxy = true;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (useProxy) {
|
|
167
|
+
// Tunnel through gateway, then connect
|
|
168
|
+
ssh -L localPort:[vm-ipv6]:22 root@gateway-ipv4 &
|
|
169
|
+
uc --connect "ssh+cli://root@localhost:localPort" deploy
|
|
170
|
+
} else {
|
|
171
|
+
// Direct connection
|
|
172
|
+
uc --connect "ssh+cli://root@[vm-ipv6]" deploy
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Why `ssh+cli://` (not `ssh://`)
|
|
177
|
+
|
|
178
|
+
| Connector | SSH Config | ProxyCommand | SSH Agent |
|
|
179
|
+
|-----------|------------|--------------|-----------|
|
|
180
|
+
| `ssh://` | No | No | Yes |
|
|
181
|
+
| `ssh+cli://` | Yes | Yes | Yes |
|
|
182
|
+
|
|
183
|
+
`ssh+cli://` uses system ssh binary which respects SSH agent where we load our temp cert.
|
|
184
|
+
|
|
185
|
+
## Deploy Flow (No Local Config)
|
|
186
|
+
|
|
187
|
+
### Current (Being Replaced)
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
.hackerrun → app name → sync uncloud config → uc -c <context> deploy
|
|
191
|
+
│
|
|
192
|
+
└── Local ~/.config/uncloud/config.yaml
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### New (Direct Connection)
|
|
196
|
+
|
|
197
|
+
```
|
|
198
|
+
.hackerrun → app name → platform API → VM IP → uc --connect ssh+cli://root@[vm-ip] deploy
|
|
199
|
+
│
|
|
200
|
+
└── No local config, platform is source of truth
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Command Execution Flow
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
async function runUncloudCommand(command: string) {
|
|
207
|
+
// 1. Read app name from .hackerrun
|
|
208
|
+
const appName = (await readFile('.hackerrun', 'utf-8')).trim();
|
|
209
|
+
|
|
210
|
+
// 2. Get app details from platform API
|
|
211
|
+
const app = await platformClient.getApp(appName);
|
|
212
|
+
const vmIpv6 = app.nodes[0].ipv6;
|
|
213
|
+
|
|
214
|
+
// 3. Get temp SSH certificate
|
|
215
|
+
const { keyPath, certPath } = await getTempSSHCertificate(app);
|
|
216
|
+
|
|
217
|
+
// 4. Start temp SSH agent, add key+cert
|
|
218
|
+
const { authSock, agentPid } = await startTempAgent(keyPath);
|
|
219
|
+
|
|
220
|
+
// 5. Test IPv6, connect directly or via tunnel
|
|
221
|
+
// 6. Run: uc --connect "ssh+cli://root@[vm-ip]" <command>
|
|
222
|
+
|
|
223
|
+
// 7. Cleanup
|
|
224
|
+
process.kill(agentPid);
|
|
225
|
+
await rm(keyPath);
|
|
226
|
+
await rm(certPath);
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### What Gets Removed
|
|
231
|
+
|
|
232
|
+
| File/Logic | Purpose | Status |
|
|
233
|
+
|------------|---------|--------|
|
|
234
|
+
| `src/lib/uncloud-sync.ts` | Syncs uncloud config locally | **Remove** |
|
|
235
|
+
| `src/lib/ssh.ts` (SSHKeyManager) | Manages user SSH keys | **Remove** |
|
|
236
|
+
| `~/.config/uncloud/` | Local uncloud contexts | **Not needed** |
|
|
237
|
+
| Context switching (`uc -c`) | Selects cluster | **Not needed** |
|
|
238
|
+
|
|
239
|
+
## Platform DB Schema
|
|
240
|
+
|
|
241
|
+
```prisma
|
|
242
|
+
model App {
|
|
243
|
+
id Int @id @default(autoincrement())
|
|
244
|
+
userId Int @map("user_id")
|
|
245
|
+
appName String @map("app_name")
|
|
246
|
+
domainName String @unique @map("domain_name")
|
|
247
|
+
location String
|
|
248
|
+
createdAt DateTime @default(now()) @map("created_at")
|
|
249
|
+
lastDeployedAt DateTime? @map("last_deployed_at")
|
|
250
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
251
|
+
nodes AppNode[]
|
|
252
|
+
routes AppRoute[]
|
|
253
|
+
@@unique([userId, appName])
|
|
254
|
+
@@map("apps")
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
model Gateway {
|
|
258
|
+
id Int @id @default(autoincrement())
|
|
259
|
+
vmId String @unique @map("vm_id")
|
|
260
|
+
ipv4 String
|
|
261
|
+
ipv6 String
|
|
262
|
+
tunnelId String @map("tunnel_id")
|
|
263
|
+
location String
|
|
264
|
+
@@map("gateways")
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
model AppRoute {
|
|
268
|
+
id Int @id @default(autoincrement())
|
|
269
|
+
appId Int @map("app_id")
|
|
270
|
+
hostname String @unique
|
|
271
|
+
backendIpv6 String @map("backend_ipv6")
|
|
272
|
+
backendPort Int @default(443)
|
|
273
|
+
@@map("app_routes")
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
# New models for SSH auth
|
|
277
|
+
model SSHCertificateAuthority {
|
|
278
|
+
id Int @id @default(autoincrement())
|
|
279
|
+
publicKey String @map("public_key") @db.Text
|
|
280
|
+
privateKeyEncrypted String @map("private_key_encrypted") @db.Text
|
|
281
|
+
createdAt DateTime @default(now()) @map("created_at")
|
|
282
|
+
@@map("ssh_ca")
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
model PlatformSSHKey {
|
|
286
|
+
id Int @id @default(autoincrement())
|
|
287
|
+
publicKey String @map("public_key") @db.Text
|
|
288
|
+
privateKeyEncrypted String @map("private_key_encrypted") @db.Text
|
|
289
|
+
createdAt DateTime @default(now()) @map("created_at")
|
|
290
|
+
@@map("platform_ssh_key")
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
## App VM Setup (cluster.ts)
|
|
295
|
+
|
|
296
|
+
1. Create IPv6-only VM with **platform SSH key** (not user key)
|
|
297
|
+
2. Configure CA for user certificates:
|
|
298
|
+
```bash
|
|
299
|
+
echo "$CA_PUBLIC_KEY" > /etc/ssh/hackerrun-ca.pub
|
|
300
|
+
echo "TrustedUserCAKeys /etc/ssh/hackerrun-ca.pub" > /etc/ssh/sshd_config.d/hackerrun-ca.conf
|
|
301
|
+
systemctl reload sshd
|
|
302
|
+
```
|
|
303
|
+
3. Configure DNS64 via systemd-resolved
|
|
304
|
+
4. Set up WireGuard for NAT64
|
|
305
|
+
5. Run `uc machine init --no-dns`
|
|
306
|
+
|
|
307
|
+
## Implementation Status
|
|
308
|
+
|
|
309
|
+
### Completed
|
|
310
|
+
|
|
311
|
+
- [x] Gateway/AppRoute models, random name generator
|
|
312
|
+
- [x] IPv6-only VM creation, SSH with gateway fallback
|
|
313
|
+
- [x] Deploy with route registration, gateway sync
|
|
314
|
+
- [x] WireGuard/NAT64/Docker IPv6 config
|
|
315
|
+
- [x] Cloudflare Tunnel + wildcard DNS
|
|
316
|
+
- [x] SSH Certificates + Remove Local Config (temp certs, SSH agent, `uc --connect ssh+cli://`, removed uncloud-sync.ts and ssh.ts)
|
|
317
|
+
|
|
318
|
+
## Docker Container NAT64
|
|
319
|
+
|
|
320
|
+
Containers need config to reach IPv4 APIs via NAT64:
|
|
321
|
+
|
|
322
|
+
1. Enable IPv6 in Docker daemon
|
|
323
|
+
2. Recreate `uncloud` network with IPv6
|
|
324
|
+
3. Block IPv4 forwarding (iptables REJECT) so apps fall back to IPv6 quickly
|
|
325
|
+
|
|
326
|
+
## Gateway Caddyfile
|
|
327
|
+
|
|
328
|
+
Auto-generated by `gateway-sync.ts`:
|
|
329
|
+
|
|
330
|
+
```caddyfile
|
|
331
|
+
{
|
|
332
|
+
http_port 80
|
|
333
|
+
auto_https off
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
:80 {
|
|
337
|
+
@bright_ocean host bright-ocean.hackerrun.app *.bright-ocean.hackerrun.app
|
|
338
|
+
handle @bright_ocean {
|
|
339
|
+
reverse_proxy http://[2a01:4f8:2b01:131a:244c::2]:80
|
|
340
|
+
}
|
|
341
|
+
handle {
|
|
342
|
+
respond "App not found" 404
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
## Future: Scalability
|
|
348
|
+
|
|
349
|
+
### Multi-Gateway HA
|
|
350
|
+
- Multiple cloudflared connect to same tunnel
|
|
351
|
+
- App VMs use multiple DNS64/NAT64 gateways
|
|
352
|
+
|
|
353
|
+
### Scale-to-Zero
|
|
354
|
+
- Gateway detects idle → Platform stops VM → Traffic arrives → Gateway wakes VM
|
|
355
|
+
- Requires Ubicloud Stop/Resume feature
|
|
356
|
+
|
|
357
|
+
## CI/CD: Integrated Build & Deploy
|
|
358
|
+
|
|
359
|
+
Railway/Render-style CI/CD without GitHub Actions. User connects their GitHub repo, pushes trigger automatic builds and deploys.
|
|
360
|
+
|
|
361
|
+
### Architecture
|
|
362
|
+
|
|
363
|
+
```
|
|
364
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
365
|
+
│ 1. User pushes to GitHub │
|
|
366
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
367
|
+
│
|
|
368
|
+
▼
|
|
369
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
370
|
+
│ 2. GitHub webhook (push event) → hackerrun-platform │
|
|
371
|
+
│ - Lookup app by repo URL │
|
|
372
|
+
│ - Generate build token (scoped to app, 15 min TTL) │
|
|
373
|
+
│ - Create ephemeral build VM via Ubicloud API │
|
|
374
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
375
|
+
│
|
|
376
|
+
▼
|
|
377
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
378
|
+
│ 3. Ephemeral Build VM │
|
|
379
|
+
│ Pre-installed: git, docker, hackerrun CLI, uc CLI │
|
|
380
|
+
│ Receives: REPO_URL, COMMIT_SHA, BUILD_TOKEN, APP_NAME │
|
|
381
|
+
│ │
|
|
382
|
+
│ Build script: │
|
|
383
|
+
│ git clone $REPO_URL /build │
|
|
384
|
+
│ cd /build && git checkout $COMMIT_SHA │
|
|
385
|
+
│ hackerrun deploy --build-token $BUILD_TOKEN --app $APP │
|
|
386
|
+
│ │
|
|
387
|
+
│ hackerrun deploy internally: │
|
|
388
|
+
│ 1. Auth via build token (not user's CLI token) │
|
|
389
|
+
│ 2. docker build (on build VM, with cache) │
|
|
390
|
+
│ 3. SSH to app cluster VM │
|
|
391
|
+
│ 4. Transfer image via unregistry (direct, no registry) │
|
|
392
|
+
│ 5. Deploy containers │
|
|
393
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
394
|
+
│
|
|
395
|
+
▼
|
|
396
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
397
|
+
│ 4. Build complete → VM destroyed │
|
|
398
|
+
│ - Build logs stored in platform DB │
|
|
399
|
+
│ - Status webhook to GitHub (optional) │
|
|
400
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
### Key Design Decisions
|
|
404
|
+
|
|
405
|
+
| Decision | Rationale |
|
|
406
|
+
|----------|-----------|
|
|
407
|
+
| **Reuse `hackerrun deploy`** | Same deploy path for local and CI, no separate build system |
|
|
408
|
+
| **No container registry** | unregistry transfers images directly to cluster |
|
|
409
|
+
| **Ephemeral build VMs** | Clean environment, no multi-tenant security concerns |
|
|
410
|
+
| **Build tokens** | Scoped, short-lived auth for build VM to act on user's behalf |
|
|
411
|
+
|
|
412
|
+
### CLI Changes
|
|
413
|
+
|
|
414
|
+
```bash
|
|
415
|
+
# Local deploy (unchanged)
|
|
416
|
+
hackerrun deploy
|
|
417
|
+
└── Uses ~/.hackerrun/token
|
|
418
|
+
└── Reads app name from .hackerrun or hackerrun.yaml
|
|
419
|
+
|
|
420
|
+
# CI/CD deploy (new flags)
|
|
421
|
+
hackerrun deploy --build-token $TOKEN --app myapp
|
|
422
|
+
└── Uses provided token (no file lookup)
|
|
423
|
+
└── Uses provided app name (no file lookup)
|
|
424
|
+
└── Non-interactive, suitable for automation
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
### Docker Build Caching
|
|
428
|
+
|
|
429
|
+
Ephemeral VMs need external cache storage for fast rebuilds.
|
|
430
|
+
|
|
431
|
+
**Option 1: BuildKit with S3/R2 Remote Cache (Recommended)**
|
|
432
|
+
|
|
433
|
+
```bash
|
|
434
|
+
docker buildx build \
|
|
435
|
+
--cache-from=type=s3,bucket=hackerrun-cache,region=auto,name=${userId}/${app} \
|
|
436
|
+
--cache-to=type=s3,bucket=hackerrun-cache,region=auto,name=${userId}/${app},mode=max \
|
|
437
|
+
-t image:tag .
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
- Cache persists in Cloudflare R2 (no egress fees)
|
|
441
|
+
- Per-user/app cache isolation
|
|
442
|
+
- First build slow, subsequent builds fast
|
|
443
|
+
|
|
444
|
+
**Option 2: Pre-baked Build VM Image**
|
|
445
|
+
|
|
446
|
+
- Custom Ubicloud boot image with common base images pre-pulled
|
|
447
|
+
- `node:20`, `python:3.12`, `golang:1.22`, etc. already cached
|
|
448
|
+
- Reduces cold-start time for first builds
|
|
449
|
+
|
|
450
|
+
### New Platform Models
|
|
451
|
+
|
|
452
|
+
```prisma
|
|
453
|
+
model BuildToken {
|
|
454
|
+
id Int @id @default(autoincrement())
|
|
455
|
+
userId Int @map("user_id")
|
|
456
|
+
appName String @map("app_name")
|
|
457
|
+
token String @unique
|
|
458
|
+
expiresAt DateTime @map("expires_at")
|
|
459
|
+
createdAt DateTime @default(now()) @map("created_at")
|
|
460
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
461
|
+
@@map("build_tokens")
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
model Build {
|
|
465
|
+
id Int @id @default(autoincrement())
|
|
466
|
+
appId Int @map("app_id")
|
|
467
|
+
commitSha String @map("commit_sha")
|
|
468
|
+
commitMsg String? @map("commit_msg")
|
|
469
|
+
branch String
|
|
470
|
+
status String @default("pending") // pending, building, success, failed
|
|
471
|
+
logs String? @db.Text
|
|
472
|
+
vmId String? @map("vm_id") // Ephemeral build VM
|
|
473
|
+
startedAt DateTime? @map("started_at")
|
|
474
|
+
completedAt DateTime? @map("completed_at")
|
|
475
|
+
createdAt DateTime @default(now()) @map("created_at")
|
|
476
|
+
app App @relation(fields: [appId], references: [id], onDelete: Cascade)
|
|
477
|
+
@@map("builds")
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
model GithubRepo {
|
|
481
|
+
id Int @id @default(autoincrement())
|
|
482
|
+
appId Int @unique @map("app_id")
|
|
483
|
+
installationId BigInt @map("installation_id") // GitHub App installation
|
|
484
|
+
repoFullName String @map("repo_full_name") // owner/repo
|
|
485
|
+
branch String @default("main") // Branch to deploy
|
|
486
|
+
createdAt DateTime @default(now()) @map("created_at")
|
|
487
|
+
app App @relation(fields: [appId], references: [id], onDelete: Cascade)
|
|
488
|
+
@@map("github_repos")
|
|
489
|
+
}
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
### New Platform Endpoints
|
|
493
|
+
|
|
494
|
+
| Endpoint | Purpose |
|
|
495
|
+
|----------|---------|
|
|
496
|
+
| `POST /api/webhooks/github` | Receive push events, trigger builds |
|
|
497
|
+
| `POST /api/builds/token` | Generate short-lived build token |
|
|
498
|
+
| `GET /api/apps/:app/builds` | List builds for an app |
|
|
499
|
+
| `GET /api/apps/:app/builds/:id` | Get build details + logs |
|
|
500
|
+
| `POST /api/apps/:app/connect-repo` | Link GitHub repo to app |
|
|
501
|
+
|
|
502
|
+
### GitHub App Requirements
|
|
503
|
+
|
|
504
|
+
Need a GitHub App (separate from OAuth) for:
|
|
505
|
+
- Receiving webhooks (push events)
|
|
506
|
+
- Cloning private repos on build VM
|
|
507
|
+
- Setting commit status (optional)
|
|
508
|
+
|
|
509
|
+
Permissions needed:
|
|
510
|
+
- Contents: read (clone repos)
|
|
511
|
+
- Webhooks: receive push events
|
|
512
|
+
- Commit statuses: write (optional, for status checks)
|
|
513
|
+
|
|
514
|
+
### Implementation Status
|
|
515
|
+
|
|
516
|
+
- [ ] BuildToken model + API endpoint
|
|
517
|
+
- [ ] CLI `--build-token` and `--app` flags
|
|
518
|
+
- [ ] GitHub App setup + webhook handler
|
|
519
|
+
- [ ] Build model + status tracking
|
|
520
|
+
- [ ] Ephemeral build VM provisioning
|
|
521
|
+
- [ ] Build log streaming/storage
|
|
522
|
+
- [ ] S3/R2 cache bucket setup
|
|
523
|
+
- [ ] BuildKit cache integration
|
|
524
|
+
- [ ] Pre-baked build VM image (optional)
|
|
525
|
+
|
|
526
|
+
## Development
|
|
527
|
+
|
|
528
|
+
```bash
|
|
529
|
+
npm run build
|
|
530
|
+
npm run dev -- <command>
|
|
531
|
+
npm link # Global testing
|
|
532
|
+
```
|
package/README.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Hackerrun CLI
|
|
2
|
+
|
|
3
|
+
Deploy apps with full control over your infrastructure. Hackerrun provides a Railway/Render-like experience on top of [Ubicloud](https://ubicloud.com) VMs with [Uncloud](https://github.com/psviderski/uncloud) container orchestration.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Install
|
|
9
|
+
npm install -g hackerrun
|
|
10
|
+
|
|
11
|
+
# Login (one-time GitHub OAuth)
|
|
12
|
+
hackerrun login
|
|
13
|
+
|
|
14
|
+
# Deploy your app
|
|
15
|
+
cd myapp
|
|
16
|
+
hackerrun deploy
|
|
17
|
+
# => App live at https://laughing-buddha.hackerrun.app
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Commands
|
|
21
|
+
|
|
22
|
+
| Command | Description |
|
|
23
|
+
|---------|-------------|
|
|
24
|
+
| `hackerrun login` | Authenticate via GitHub |
|
|
25
|
+
| `hackerrun deploy` | Deploy your application |
|
|
26
|
+
| `hackerrun apps` | List all your apps |
|
|
27
|
+
| `hackerrun logs <app>` | View application logs |
|
|
28
|
+
| `hackerrun ssh <app>` | SSH into app's VM |
|
|
29
|
+
| `hackerrun destroy <app>` | Delete app and infrastructure |
|
|
30
|
+
| `hackerrun link <app>` | Link current directory to an existing app |
|
|
31
|
+
| `hackerrun rename --app <old> <new>` | Rename an app |
|
|
32
|
+
| `hackerrun domain --app <app> <new>` | Change app's domain |
|
|
33
|
+
|
|
34
|
+
## How It Works
|
|
35
|
+
|
|
36
|
+
1. **First deploy** creates an IPv6-only VM on Ubicloud
|
|
37
|
+
2. Your app is containerized and deployed via Uncloud
|
|
38
|
+
3. Traffic flows through our gateway: `User → Cloudflare → Gateway → Your App`
|
|
39
|
+
4. Apps get a random domain like `laughing-buddha.hackerrun.app`
|
|
40
|
+
|
|
41
|
+
## App Naming
|
|
42
|
+
|
|
43
|
+
| Identifier | Scope | Example |
|
|
44
|
+
|------------|-------|---------|
|
|
45
|
+
| **App Name** | Per user | `myapp` (you choose) |
|
|
46
|
+
| **Domain** | Global | `laughing-buddha` (auto-generated) |
|
|
47
|
+
|
|
48
|
+
You can change the domain anytime with `hackerrun domain`.
|
|
49
|
+
|
|
50
|
+
## Configuration Files
|
|
51
|
+
|
|
52
|
+
- **`hackerrun.yml`** - Docker Compose file for production with `x-ports` for public exposure
|
|
53
|
+
- **`docker-compose.yml`** - Local development (ignored by hackerrun)
|
|
54
|
+
|
|
55
|
+
### Example hackerrun.yml
|
|
56
|
+
|
|
57
|
+
```yaml
|
|
58
|
+
services:
|
|
59
|
+
web:
|
|
60
|
+
build: .
|
|
61
|
+
x-ports:
|
|
62
|
+
- "3000/http"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The `x-ports` extension exposes your service publicly through the gateway.
|
|
66
|
+
|
|
67
|
+
## Domains
|
|
68
|
+
|
|
69
|
+
| Domain | Purpose |
|
|
70
|
+
|--------|---------|
|
|
71
|
+
| `hackerrun.com` | Platform website & dashboard |
|
|
72
|
+
| `hackerrun.app` | Your deployed applications |
|
|
73
|
+
|
|
74
|
+
### Domain Format
|
|
75
|
+
|
|
76
|
+
- Single service: `{domain}.hackerrun.app`
|
|
77
|
+
- Multi-service: `{domain}-{service}.hackerrun.app`
|
|
78
|
+
|
|
79
|
+
## Development
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
# Build
|
|
83
|
+
npm run build
|
|
84
|
+
|
|
85
|
+
# Run in development
|
|
86
|
+
npm run dev -- <command>
|
|
87
|
+
|
|
88
|
+
# Link globally for testing
|
|
89
|
+
npm link
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## License
|
|
93
|
+
|
|
94
|
+
MIT
|