Add/change various things

This commit is contained in:
Radon Rosborough 2022-12-28 19:52:59 -07:00
parent ed0be93886
commit 48bdc9abf8
8 changed files with 319 additions and 105 deletions

View File

@ -2,4 +2,7 @@ module github.com/radian-software/riju/agent
go 1.18
require github.com/gorilla/websocket v1.5.0 // indirect
require (
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
)

View File

@ -1,2 +1,4 @@
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=

View File

@ -8,6 +8,7 @@ import (
"os/exec"
"time"
"github.com/google/shlex"
"github.com/gorilla/websocket"
)
@ -67,6 +68,25 @@ func warnf(ms *ManagedWebsocket, format string, arg ...interface{}) {
warn(ms, fmt.Errorf(format, arg...))
}
func getCommandPrefix() []string {
prefix := os.Getenv("RIJU_AGENT_COMMAND_PREFIX")
if prefix == "" {
logErrorf("must specify RIJU_AGENT_COMMAND_PREFIX for security reasons")
os.Exit(1)
}
if prefix == "0" {
return []string{}
}
list, err := shlex.Split(prefix)
if err != nil {
logErrorf("parsing RIJU_AGENT_COMMAND_PREFIX: %w", err)
os.Exit(1)
}
return list
}
var CommandPrefix = getCommandPrefix()
// https://github.com/gorilla/websocket/blob/76ecc29eff79f0cedf70c530605e486fc32131d1/examples/command/main.go
func handler(w http.ResponseWriter, r *http.Request) {
// Upgrade http connection to websocket
@ -98,6 +118,7 @@ func handler(w http.ResponseWriter, r *http.Request) {
fatalf(ms, "cmdline query parameter missing")
return
}
cmdline = append(CommandPrefix, cmdline...)
binary, err := exec.LookPath(cmdline[0])
if err != nil {
fatalf(ms, "searching for executable: %w", err)
@ -191,7 +212,12 @@ func main() {
host = "0.0.0.0"
}
fmt.Printf("Listening on http://%s:%s\n", host, port)
err := http.ListenAndServe(fmt.Sprintf("%s:%s", host, port), http.HandlerFunc(handler))
mux := http.NewServeMux()
mux.HandleFunc("/exec", handler)
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
err := http.ListenAndServe(fmt.Sprintf("%s:%s", host, port), mux)
if err != nil {
logError(err)
os.Exit(1)

View File

@ -1,10 +1,54 @@
import * as k8sClient from "@kubernetes/client-node";
import lodash from "lodash";
const kubeconfig = new k8sClient.KubeConfig();
kubeconfig.loadFromDefault();
const k8s = kubeconfig.makeApiClient(k8sClient.CoreV1Api);
export function watchPods() {
const callbacks = {};
const pods = {};
// https://github.com/kubernetes-client/javascript/blob/1f76ee10c54e33a998abb4686488ccff4285366a/examples/typescript/informer/informer.ts
//
// The watch functionality seems to be wholly undocumented. Copy,
// paste, and pray.
const informer = k8sClient.makeInformer(
kubeconfig,
"/api/v1/namespaces/riju-user/pods",
() => k8s.listNamespacedPod("riju-user")
);
for (const event of ["add", "update", "delete"]) {
informer.on(event, (pod) => {
if (pod.metadata.name in callbacks) {
callbacks[pod.metadata.name](event, pod);
}
pods[pod.metadata.name] = pod;
if (event == "delete") {
delete callbacks[pod.metadata.name];
delete pods[pod.metadata.name];
}
});
}
informer.on("error", (err) => {
console.error(err);
setTimeout(() => informer.start(), 5000);
});
informer.start();
return {
setCallback: (podName, callback) => {
callbacks[podName] = callback;
if (podName in pods) {
callback("add", pods[podName]);
}
},
};
}
export async function listUserSessions() {
return (await k8s.listNamespacedPod("riju-user")).body.items.map((pod) => ({
podName: pod.metadata.name,
@ -12,116 +56,150 @@ export async function listUserSessions() {
}));
}
export async function createUserSession({ sessionID, langConfig, revisions }) {
const { body: pod } = await k8s.createNamespacedPod("riju-user", {
metadata: {
name: `riju-user-session-${sessionID}`,
labels: {
"riju.codes/user-session-id": sessionID,
export async function createUserSession({
watcher,
sessionID,
langConfig,
revisions,
}) {
const pod = (
await k8s.createNamespacedPod("riju-user", {
metadata: {
name: `riju-user-session-${sessionID}`,
labels: {
"riju.codes/user-session-id": sessionID,
},
},
},
spec: {
volumes: [
{
name: "minio-config",
secret: {
secretName: "minio-user-login",
},
},
{
name: "riju-bin",
emptyDir: {},
},
],
imagePullSecrets: [
{
name: "registry-user-login",
},
],
initContainers: [
{
name: "download",
image: "minio/mc:RELEASE.2022-12-13T00-23-28Z",
resources: {},
command: ["sh", "-c"],
args: [
`mkdir -p /root/.mc && cp -LT /mc/config.json /root/.mc/config.json &&` +
`mc cp riju/agent/${revisions.agent} /riju-bin/agent && chmod +x /riju-bin/agent &&` +
`mc cp riju/ptyify/${revisions.ptyify} /riju-bin/ptyify && chmod +x /riju-bin/ptyify`,
],
volumeMounts: [
{
name: "minio-config",
mountPath: "/mc",
readOnly: true,
},
{
name: "riju-bin",
mountPath: "/riju-bin",
},
],
},
],
containers: [
{
name: "session",
image: `localhost:30999/riju-lang:${langConfig.id}-${revisions.langImage}`,
resources: {
requests: {},
limits: {
cpu: "1000m",
memory: "4Gi",
spec: {
volumes: [
{
name: "minio-config",
secret: {
secretName: "minio-user-login",
},
},
startupProbe: {
httpGet: {
path: "/health",
port: 869,
scheme: "HTTP",
},
failureThreshold: 30,
initialDelaySeconds: 0,
periodSeconds: 1,
successThreshold: 1,
timeoutSeconds: 2,
{
name: "riju-bin",
emptyDir: {},
},
readinessProbe: {
httpGet: {
path: "/health",
port: 869,
scheme: "HTTP",
},
failureThreshold: 1,
initialDelaySeconds: 2,
periodSeconds: 10,
successThreshold: 1,
timeoutSeconds: 2,
],
imagePullSecrets: [
{
name: "registry-user-login",
},
livenessProbe: {
httpGet: {
path: "/health",
port: 869,
scheme: "HTTP",
},
failureThreshold: 3,
initialDelaySeconds: 2,
periodSeconds: 10,
successThreshold: 1,
timeoutSeconds: 2,
],
initContainers: [
{
name: "download",
image: "minio/mc:RELEASE.2022-12-13T00-23-28Z",
resources: {},
command: ["sh", "-c"],
args: [
`mkdir -p /root/.mc && cp -LT /mc/config.json /root/.mc/config.json &&` +
`mc cp riju/agent/${revisions.agent} /riju-bin/agent && chmod +x /riju-bin/agent &&` +
`mc cp riju/ptyify/${revisions.ptyify} /riju-bin/ptyify && chmod +x /riju-bin/ptyify`,
],
volumeMounts: [
{
name: "minio-config",
mountPath: "/mc",
readOnly: true,
},
{
name: "riju-bin",
mountPath: "/riju-bin",
},
],
},
volumeMounts: [
{
name: "riju-bin",
mountPath: "/riju-bin",
readOnly: true,
],
containers: [
{
name: "session",
image: `localhost:30999/riju-lang:${langConfig.id}-${revisions.langImage}`,
resources: {
requests: {},
limits: {
cpu: "1000m",
memory: "4Gi",
},
},
],
},
],
restartPolicy: "Never",
},
command: ["/riju-bin/agent"],
env: [
{
name: "RIJU_AGENT_COMMAND_PREFIX",
value: "runuser -u riju --",
},
],
securityContext: {
runAsUser: 0,
},
startupProbe: {
httpGet: {
path: "/health",
port: 869,
scheme: "HTTP",
},
failureThreshold: 30,
initialDelaySeconds: 0,
periodSeconds: 1,
successThreshold: 1,
timeoutSeconds: 2,
},
readinessProbe: {
httpGet: {
path: "/health",
port: 869,
scheme: "HTTP",
},
failureThreshold: 1,
initialDelaySeconds: 2,
periodSeconds: 10,
successThreshold: 1,
timeoutSeconds: 2,
},
livenessProbe: {
httpGet: {
path: "/health",
port: 869,
scheme: "HTTP",
},
failureThreshold: 3,
initialDelaySeconds: 2,
periodSeconds: 10,
successThreshold: 1,
timeoutSeconds: 2,
},
volumeMounts: [
{
name: "riju-bin",
mountPath: "/riju-bin",
readOnly: true,
},
],
},
],
restartPolicy: "Never",
},
})
).body;
const podIP = await new Promise((resolve, reject) => {
setTimeout(() => reject("timed out"), 5 * 60 * 1000);
watcher.setCallback(pod.metadata.name, (event, pod) => {
if (event == "delete") {
reject(new Error("pod was deleted"));
} else if (pod.status.phase === "Failed") {
reject(new Error("pod status became Failed"));
} else if (
pod.status.podIP &&
lodash.every(pod.status.containerStatuses, (status) => status.ready)
) {
resolve(pod.status.podIP);
} else {
console.log(event, JSON.stringify(pod.status, null, 2));
}
});
});
console.log(pod);
return podIP;
}
export async function deleteUserSessions(sessionsToDelete) {

View File

@ -26,12 +26,15 @@ async function main() {
}
const sessionID = getUUID();
console.log(`Starting session with UUID ${sessionID}`);
const watcher = k8s.watchPods();
await k8s.createUserSession({
watcher,
sessionID,
langConfig,
revisions: {
agent: "20221228-023645-invisible-amaranth-sparrow",
agent: "20221229-002450-semantic-moccasin-albatross",
ptyify: "20221228-023645-clean-white-gorilla",
langImage: "20221227-195753-forward-harlequin-wolverine",
},
});
// let buffer = "";

View File

@ -5,6 +5,7 @@ set -euo pipefail
cd "$(dirname "$0")"
registry_password="$(pwgen -s 20 1)"
proxy_password="$(pwgen -s 20 1)"
cat <<EOF
networking:
@ -25,4 +26,8 @@ registry:
minio:
accessKey: "$(head -c16 /dev/urandom | xxd -p)"
secretKey: "$(head -c16 /dev/urandom | xxd -p)"
proxy:
password: "${proxy_password}"
htpasswd: "$(htpasswd -nbB admin "${proxy_password}")"
EOF

88
k8s/riju-proxy.yaml Normal file
View File

@ -0,0 +1,88 @@
---
kind: ConfigMap
apiVersion: v1
metadata:
namespace: riju
name: riju-proxy-config
data:
squid.conf: |
cache deny all
acl riju_src src 10.244.0.0/16
http_access deny !riju_src
acl riju_dst dst 10.244.0.0/16
http_access deny !riju_dst
acl riju_port port 869
http_access deny !riju_port
acl riju_method method GET
http_access deny !riju_method
auth_param basic program /usr/lib/squid/basic_ncsa_auth
auth_param basic children 5 startup=5 idle=1
auth_param basic realm Riju administrative proxy
auth_param basic credentialsttl 24 hours
acl riju_auth proxy_auth REQUIRED
http_access deny !riju_auth
http_access allow all
http_access deny all
http_port 3128
---
kind: Deployment
apiVersion: apps/v1
metadata:
namespace: riju
name: riju-proxy
spec:
replicas: 1
selector:
matchLabels:
app: riju-proxy
template:
metadata:
labels:
app: riju-proxy
spec:
volumes:
- name: config
configMap:
name: riju-proxy-config
- name: auth
secret:
secretName: riju-proxy-auth
containers:
- name: nginx
image: "ubuntu/squid:5.2-22.04_beta"
resources: {}
readinessProbe:
tcpSocket:
port: 3128
failureThreshold: 1
initialDelaySeconds: 2
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 2
livenessProbe:
tcpSocket:
port: 3128
failureThreshold: 3
initialDelaySeconds: 2
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 2
ports:
- name: http
containerPort: 3128
volumeMounts:
- name: config
mountPath: /etc/squid/squid.conf
subPath: squid.conf
- name: auth
mountPath: /etc/squid/passwd
subPath: htpasswd

View File

@ -84,3 +84,12 @@ stringData:
}
}
}
---
kind: Secret
apiVersion: v1
metadata:
namespace: riju
name: riju-proxy-auth
data:
htpasswd: "{{ .proxy.htpasswd | println | b64enc }}"