~comcloudway/builds.sr.ht

0c943e604f0d08a7f7950876cc3c3d45dd02c2e8 — Drew DeVault 5 years ago eb5eb06
Add support for acquiring shell environment
7 files changed, 117 insertions(+), 8 deletions(-)

A buildsrht-keys
A buildsrht-shell
M buildsrht/manifest.py
M setup.py
M worker/context.go
M worker/http.go
M worker/manifest.go
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{}