A buildsrht-keys => buildsrht-keys +16 -0
@@ 0,0 1,16 @@
+#!/usr/bin/env python3
+# We just let everyone in at this stage, authentication is done later on.
+from srht.config import cfg
+import sys
+import os
+
+key_type = sys.argv[3]
+b64key = sys.argv[4]
+
+default_shell = os.path.join(os.path.dirname(sys.argv[0]), "buildsrht-shell")
+shell = cfg("git.sr.ht", "shell", default=default_shell)
+keys = ("command=\"{} '{}'\",".format(shell, b64key) +
+ "no-port-forwarding,no-X11-forwarding,no-agent-forwarding" +
+ " {} {} somebody".format(key_type, b64key) + "\n")
+print(keys)
+sys.exit(0)
A buildsrht-shell => buildsrht-shell +52 -0
@@ 0,0 1,52 @@
+#!/usr/bin/env python3
+from srht.config import cfg, get_origin
+import os
+import requests
+import shlex
+import sys
+
+def fail(reason):
+ owner = cfg("sr.ht", "owner-name")
+ email = cfg("sr.ht", "owner-email")
+ print(reason)
+ print(f"Please reach out to {owner} <{email}> for support.")
+ sys.exit(1)
+
+b64key = sys.argv[1]
+
+cmd = os.environ.get("SSH_ORIGINAL_COMMAND") or ""
+cmd = shlex.split(cmd)
+if len(cmd) != 2:
+ fail("Usage: ssh ... connect <job ID>")
+op = cmd[0]
+if op not in ["connect"]:
+ fail("Usage: ssh ... connect <job ID>")
+job_id = int(cmd[1])
+
+r = requests.get(f"http://localhost:8080/job/{job_id}/info")
+if r.status_code != 200:
+ fail("No such job found.")
+info = r.json()
+
+meta_origin = get_origin("meta.sr.ht")
+r = requests.get(f"{meta_origin}/api/ssh-key/{b64key}")
+if r.status_code == 200:
+ username = r.json()["owner"]["name"]
+else:
+ fail("Temporary authentication failure. Try again later.")
+
+if username != info["username"]:
+ fail("You are not permitted to connect to this job.")
+
+sys.stdout.flush()
+sys.stderr.flush()
+tty = os.open("/dev/tty", os.O_RDWR)
+os.dup2(0, tty)
+os.execvp("ssh", [
+ "ssh", "-qt",
+ "-p", str(info["port"]),
+ "-o", "UserKnownHostsFile=/dev/null",
+ "-o", "StrictHostKeyChecking=no",
+ "-o", "LogLevel=quiet",
+ "build@localhost", "bash"
+])
M buildsrht/manifest.py => buildsrht/manifest.py +10 -2
@@ 58,10 58,11 @@ class Manifest:
sources = self.yaml.get("sources")
env = self.yaml.get("environment")
secrets = self.yaml.get("secrets")
+ shell = self.yaml.get("shell")
if not image:
raise Exception("Missing image in manifest")
if not isinstance(image, str):
- raise Exception("Expected imagease to be a string")
+ raise Exception("Expected image to be a string")
if packages:
if not isinstance(packages, list) or not all([isinstance(p, str) for p in packages]):
raise Exception("Expected packages to be a string array")
@@ 82,6 83,8 @@ class Manifest:
raise Exception("Expected secrets to be a UUID array")
# Will throw exception on invalid UUIDs as well
secrets = list(map(uuid.UUID, secrets))
+ if not isinstance(shell, bool):
+ raise Exception("Expected shell to be a boolean")
self.image = image
self.arch = arch
self.packages = packages
@@ 89,9 92,13 @@ class Manifest:
self.sources = sources
self.environment = env
self.secrets = secrets
+ self.shell = shell
tasks = self.yaml.get("tasks")
if not tasks or not isinstance(tasks, list):
- raise Exception("Attempted to create manifest with no tasks")
+ if (tasks is None or tasks == []) and not self.shell:
+ raise Exception("Attempted to create manifest with no tasks")
+ else:
+ tasks = []
self.tasks = [Task(t) for t in tasks]
for task in self.tasks:
if len([t for t in self.tasks if t.name == task.name]) != 1:
@@ 113,6 120,7 @@ class Manifest:
"secrets": [str(s) for s in self.secrets] if self.secrets else None,
"tasks": [{ t.name: t.script } for t in self.tasks],
"triggers": [t.to_dict() for t in self.triggers] if any(self.triggers) else None,
+ "shell": self.shell,
}
def to_yaml(self):
M setup.py => setup.py +3 -1
@@ 68,6 68,8 @@ setup(
]
},
scripts = [
- 'buildsrht-migrate'
+ 'buildsrht-keys',
+ 'buildsrht-migrate',
+ 'buildsrht-shell',
]
)
M worker/context.go => worker/context.go +5 -0
@@ 173,6 173,11 @@ func (wctx *WorkerContext) RunBuild(
}
}
+ if manifest.Shell {
+ ctx.Log.Println("TODO: Print shell access details here")
+ <-goctx.Done()
+ }
+
jobsMutex.Lock()
delete(jobs, job_id)
jobsMutex.Unlock()
M worker/http.go => worker/http.go +30 -5
@@ 1,6 1,7 @@
package main
import (
+ "encoding/json"
"net/http"
"fmt"
@@ 19,13 20,37 @@ func HttpServer() {
w.Write([]byte("404 not found"))
return
}
- if r.Method != "POST" {
- w.WriteHeader(405)
- w.Write([]byte("405 method not allowed"))
- return
- }
switch op {
+ case "info":
+ if r.Method != "GET" {
+ w.WriteHeader(405)
+ w.Write([]byte("405 method not allowed"))
+ return
+ }
+ if job, ok := jobs[jobId]; ok {
+ w.WriteHeader(200)
+ bytes, _ := json.Marshal(struct {
+ Note *string `json:"note"`
+ OwnerId int `json:"owner_id"`
+ Port int `json:"port"`
+ Username string `json:"username"`
+ } {
+ Note: job.Job.Note,
+ OwnerId: job.Job.OwnerId,
+ Port: job.Port,
+ Username: job.Job.Username,
+ })
+ w.Write(bytes)
+ } else {
+ w.WriteHeader(404)
+ w.Write([]byte("404 not found"))
+ }
case "cancel":
+ if r.Method != "POST" {
+ w.WriteHeader(405)
+ w.Write([]byte("405 method not allowed"))
+ return
+ }
jobsMutex.Lock()
defer jobsMutex.Unlock()
if job, ok := jobs[jobId]; ok {
M worker/manifest.go => worker/manifest.go +1 -0
@@ 7,6 7,7 @@ type Manifest struct {
Packages []string
Repositories map[string]string
Secrets []string
+ Shell bool
Sources []string
Tasks []map[string]string
Triggers []map[string]interface{}