ce7cb702218693d08ea6d83464d2457c9250027c

This commit is contained in:
ms2k 2026-11-12 20:46:28 +09:00
commit 34c5f8963c
18 changed files with 145336 additions and 0 deletions

View file

@ -0,0 +1,92 @@
#load "bootstrap.fsx"
open System
open System.IO
open System.Runtime.InteropServices
open System.Security.Cryptography
open System.Text
open Bootstrap
let check condition message = if not condition then failwith message
let root = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"))
let paths = { Data = root + "/data"; State = root + "/state"; Backups = root + "/backups"; Files = root + "/files" }
let salt = Encoding.UTF8.GetBytes "delivered saltatory"
let saltHash = SHA256.HashData salt |> Convert.ToHexString |> _.ToLowerInvariant()
let config = { Paths = paths; PasswordPath = root + "/password"; Eula = "TRUE"; Args = "libraries/loader/unix_args.txt"; Saltatory = saltHash }
let mutable downloads = 0
let data = System.Collections.Generic.Dictionary<string, byte[]>()
let put key (value: string) = data[key] <- Encoding.UTF8.GetBytes value
let hash (value: byte[]) = SHA256.HashData value |> Convert.ToHexString |> _.ToLowerInvariant()
let manifests =
[ "mods", "mods.sha256"; "config", "config.sha256"; "defaultconfigs", "defaultconfigs.sha256"
"kubejs", "kubejs.sha256"; "libraries", "libraries.sha256"; "libraries-integratedscripting", "libraries-integratedscripting.sha256"
"settings", "settings.sha256"; "tools", "tools.sha256"
"world/datapacks", "world-datapacks.sha256"; "world/serverconfig", "world-serverconfig.sha256" ]
|> Map.ofList
let manifest name =
let prefix = manifests |> Map.toSeq |> Seq.find (fun (_, inventory) -> inventory = name) |> fst
data
|> Seq.filter (fun item -> item.Key.StartsWith(prefix + "/"))
|> Seq.map (fun item -> hash item.Value + " " + item.Key.Substring(prefix.Length + 1))
|> String.concat "\n"
let fetch key expected path =
if not (File.Exists path) || digest path <> expected then
downloads <- downloads + 1
File.WriteAllBytes(path, data[key])
check (digest path = expected) "fixture hash"
let fetchSaltatory expected path =
if not (File.Exists path) || digest path <> expected then
downloads <- downloads + 1
File.WriteAllBytes(path, salt)
check (digest path = expected) "saltatory hash"
let load () = run config Architecture.X64 manifest fetch fetchSaltatory
try
Directory.CreateDirectory paths.Files |> ignore
text (paths.Files + "/server.properties") "level-seed=123"
text config.PasswordPath "synthetic-password"
put "mods/old.jar" "old"
put "mods/saltatory-0.42.1.jar" "pack saltatory"
put "config/sample.toml" "config"
put "config/chunky/tasks/minecraft/overworld.properties" "artifact task state"
put "libraries/loader/unix_args.txt" "loader args"
put "settings/user_jvm_args.txt" "-Xmx1G"
put "tools/amd64/mc-server-runner" "runner"
put "tools/amd64/mc-monitor" "monitor"
put "world/serverconfig/ftbranks/ranks.snbt" "ranks"
load ()
let first = downloads
check (first = 8) "initial downloads"
check (not (File.Exists(paths.Data + "/config/chunky/tasks/minecraft/overworld.properties"))) "artifact task state copied"
check (Directory.Exists(paths.State + "/world/data/chunky")) "mutable task directory missing"
check (digest (paths.Data + "/mods/saltatory.jar") = saltHash) "delivered saltatory missing"
check (not (File.Exists(paths.Data + "/mods/saltatory-0.42.1.jar"))) "pack saltatory retained"
check (File.ReadAllText(paths.Data + "/server.properties") = "level-seed=123\nrcon.password=synthetic-password\n") "properties"
check (File.ReadAllText(paths.Data + "/server_args.txt") = "loader args") "loader args"
text (paths.State + "/ops.json") "operators"
text (paths.State + "/banned-players.json") "banned"
text (paths.State + "/world/serverconfig/ftbranks/players.snbt") "players"
text (paths.State + "/world/data/chunky/checkpoint") "task progress"
Directory.Delete(paths.Data, true)
load ()
check (downloads = first) "cached files re-downloaded"
check (File.ReadAllText(paths.State + "/ops.json") = "operators") "operators changed"
check (File.ReadAllText(paths.State + "/banned-players.json") = "banned") "bans changed"
check (File.ReadAllText(paths.State + "/world/serverconfig/ftbranks/players.snbt") = "players") "player state changed"
check (File.ReadAllText(paths.State + "/world/data/chunky/checkpoint") = "task progress") "task progress changed"
data.Remove "mods/old.jar" |> ignore
put "mods/new.jar" "new"
text (paths.State + "/cache/config/sample.toml") "corrupt"
load ()
check (downloads = first + 2) "changed or corrupt files not fetched"
check (not (File.Exists(paths.State + "/cache/mods/old.jar"))) "stale cache file"
check (not (File.Exists(paths.Data + "/mods/old.jar"))) "stale runtime mod"
check (File.ReadAllText(paths.State + "/cache/mods/new.jar") = "new") "new mod missing"
printfn "bootstrap tests passed"
finally
if Directory.Exists root then Directory.Delete(root, true)

View file

@ -0,0 +1,131 @@
module Bootstrap
open System
open System.IO
open System.Runtime.InteropServices
open System.Security.Cryptography
open System.Text
let digest path =
use stream = File.OpenRead path
SHA256.HashData stream |> Convert.ToHexString |> _.ToLowerInvariant()
let write (path: string) (bytes: byte[]) =
Directory.CreateDirectory(Path.GetDirectoryName path) |> ignore
File.WriteAllBytes(path, bytes)
let text path value = write path (Encoding.UTF8.GetBytes(value: string))
let trust source runtime =
write (Path.Combine(runtime, "cacerts")) (File.ReadAllBytes source)
type Paths =
{ Data: string
State: string
Backups: string
Files: string }
type Config =
{ Paths: Paths
PasswordPath: string
Eula: string
Args: string
Saltatory: string }
let prefixes =
[ "mods", "mods"
"config", "config"
"defaultconfigs", "defaultconfigs"
"kubejs", "kubejs"
"libraries", "libraries"
"libraries-integratedscripting", "libraries-integratedscripting"
"settings", "settings"
"tools", "tools" ]
let parse (value: string) =
value.Split('\n', StringSplitOptions.RemoveEmptyEntries)
|> Array.map (fun line ->
let line = line.TrimEnd '\r'
let hash, path = line.Substring(0, 64), line.Substring(66)
hash, path)
let sync cache prefix inventory includeFile fetchManifest fetch =
let root = Path.Combine(cache, prefix)
Directory.CreateDirectory root |> ignore
let files = fetchManifest inventory |> parse |> Array.filter (snd >> includeFile)
for hash, name in files do
let path = Path.Combine(root, name)
Directory.CreateDirectory(Path.GetDirectoryName path) |> ignore
fetch (prefix + "/" + name) hash path
let names = files |> Array.map (snd >> fun name -> Path.GetFullPath(Path.Combine(root, name))) |> Set.ofArray
for path in Directory.EnumerateFiles(root, "*", SearchOption.AllDirectories) do
if not (names.Contains(Path.GetFullPath path)) then
File.Delete path
files
let copyFiles source target files =
for _, name in files do
let path = Path.Combine(target, name)
Directory.CreateDirectory(Path.GetDirectoryName path) |> ignore
File.Copy(Path.Combine(source, name), path, true)
let run config arch fetchManifest fetch fetchSaltatory =
let p = config.Paths
let cache = Path.Combine(p.State, "cache")
Directory.CreateDirectory p.Data |> ignore
Directory.CreateDirectory p.Backups |> ignore
Directory.CreateDirectory(Path.Combine(p.State, "world")) |> ignore
Directory.CreateDirectory(Path.Combine(p.State, "world/data/chunky")) |> ignore
for prefix, target in prefixes do
let includeFile (name: string) =
(prefix <> "mods" || not (name.StartsWith("saltatory-", StringComparison.OrdinalIgnoreCase)))
&& (prefix <> "config" || not (name.StartsWith("chunky/tasks/", StringComparison.Ordinal)))
let files = sync cache prefix (prefix + ".sha256") includeFile fetchManifest fetch
let destination = Path.Combine(p.Data, target)
if Directory.Exists destination then Directory.Delete(destination, true)
copyFiles (Path.Combine(cache, prefix)) destination files
let saltatory = Path.Combine(cache, "saltatory.jar")
fetchSaltatory config.Saltatory saltatory
File.Copy(saltatory, Path.Combine(p.Data, "mods", "saltatory.jar"), true)
for name in [ "ops.json"; "whitelist.json"; "banned-ips.json"; "banned-players.json"; "usercache.json" ] do
let path = Path.Combine(p.State, name)
if not (File.Exists path) then text path "[]"
let names = Path.Combine(p.State, "usernamecache.json")
if not (File.Exists names) then text names "{}"
for name in [ "local"; "journeymap" ] do
Directory.CreateDirectory(Path.Combine(p.State, name)) |> ignore
let world = Path.Combine(p.State, "world")
for prefix in [ "datapacks"; "serverconfig" ] do
let name = "world/" + prefix
let files = sync cache name ("world-" + prefix + ".sha256") (fun _ -> true) fetchManifest fetch
copyFiles (Path.Combine(cache, name)) (Path.Combine(world, prefix)) files
let tools =
match arch with
| Architecture.X64 -> "amd64"
| Architecture.Arm64 -> "arm64"
| _ -> invalidOp "unsupported architecture"
for name in [ "mc-server-runner"; "mc-monitor" ] do
let source = Path.Combine(p.Data, "tools", tools, name)
let path = Path.Combine(p.Data, name)
File.Copy(source, path, true)
File.SetUnixFileMode(path, enum<UnixFileMode> 0o755)
let properties = File.ReadAllText(Path.Combine(p.Files, "server.properties")).TrimEnd()
let password = File.ReadAllText(config.PasswordPath).TrimEnd()
text (Path.Combine(p.Data, "server.properties")) (properties + "\nrcon.password=" + password + "\n")
File.Copy(Path.Combine(p.Data, config.Args), Path.Combine(p.Data, "server_args.txt"), true)
let args = File.ReadAllText(Path.Combine(p.Data, "settings", "user_jvm_args.txt")).TrimEnd()
text (Path.Combine(p.Data, "user_jvm_args.txt")) (args + "\n-Djavax.net.ssl.trustStore=/data/cacerts\n")
text (Path.Combine(p.Data, "eula.txt")) ("eula=" + config.Eula.ToLowerInvariant() + "\n")
printfn "Minecraft bootstrap ready"

View file

@ -0,0 +1,199 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: minecraft
namespace: minecraft
labels:
app.kubernetes.io/name: minecraft
app.kubernetes.io/instance: skies
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app.kubernetes.io/name: minecraft
app.kubernetes.io/instance: skies
template:
metadata:
labels:
app.kubernetes.io/name: minecraft
app.kubernetes.io/instance: skies
spec:
automountServiceAccountToken: false
dnsConfig:
options:
- name: ndots
value: "1"
imagePullSecrets:
- name: minecraft-pull
nodeSelector:
kubernetes.io/hostname: "********-nas"
terminationGracePeriodSeconds: 180
securityContext:
runAsUser: 1000
runAsGroup: 1000
runAsNonRoot: true
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
initContainers:
- name: bootstrap
image: registry.quorate.internal/quorate/fsharp-runner@sha256:d7b904f4f0cae9fdba7c7345cdf08c91162245834a8a13658d93396272aeec83
command: [/usr/share/dotnet/dotnet, /opt/fsi/fsi.dll, --exec]
args: [/bootstrap/run.fsx]
env:
- name: SSL_CERT_FILE
value: /artifacts/ca.crt
- name: HOME
value: /tmp
- name: DOTNET_NOLOGO
value: "true"
- name: DOTNET_CLI_TELEMETRY_OPTOUT
value: "1"
envFrom:
- secretRef:
name: minecraft
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
memory: 2Gi
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
volumeMounts:
- name: runtime
mountPath: /data
- name: state
mountPath: /state
- name: backups
mountPath: /backups
- name: files
mountPath: /files
readOnly: true
- name: bootstrap
mountPath: /bootstrap
readOnly: true
- name: rcon
mountPath: /rcon
readOnly: true
- name: artifacts
mountPath: /artifacts
readOnly: true
containers:
- name: minecraft
image: ghcr.io/graalvm/jdk-community:25i4@sha256:3508f7c5ad1e211f353e3a90f5de263574c4b09e40635fdf5a6e5952241f605b
workingDir: /data
command: [/data/mc-server-runner]
args:
- --stop-duration
- 150s
- --named-pipe
- /data/console.in
- --
- java
- "@user_jvm_args.txt"
- "@server_args.txt"
- nogui
ports:
- name: minecraft
containerPort: 25565
protocol: TCP
- name: metrics
containerPort: 19565
protocol: TCP
- name: rcon
containerPort: 25575
protocol: TCP
resources:
requests:
cpu: "2"
memory: 20Gi
limits:
memory: 24Gi
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
startupProbe:
exec:
command: [/data/mc-monitor, status, --host, 127.0.0.1, --port, "25565"]
periodSeconds: 10
failureThreshold: 90
timeoutSeconds: 5
readinessProbe:
exec:
command: [/data/mc-monitor, status, --host, 127.0.0.1, --port, "25565"]
periodSeconds: 10
timeoutSeconds: 5
volumeMounts:
- name: runtime
mountPath: /data
- name: state
mountPath: /data/ops.json
subPath: ops.json
- name: state
mountPath: /data/whitelist.json
subPath: whitelist.json
- name: state
mountPath: /data/banned-ips.json
subPath: banned-ips.json
- name: state
mountPath: /data/banned-players.json
subPath: banned-players.json
- name: state
mountPath: /data/usercache.json
subPath: usercache.json
- name: state
mountPath: /data/usernamecache.json
subPath: usernamecache.json
- name: state
mountPath: /data/world
subPath: world
- name: state
mountPath: /data/config/chunky/tasks
subPath: world/data/chunky
- name: state
mountPath: /data/local
subPath: local
- name: state
mountPath: /data/journeymap
subPath: journeymap
- name: backups
mountPath: /data/backups
volumes:
- name: artifacts
projected:
defaultMode: 0440
sources:
- secret:
name: minecraft-artifacts
- secret:
name: minecraft-ca
items:
- key: certificate
path: ca.crt
- name: rcon
secret:
secretName: minecraft-rcon
defaultMode: 0440
- name: bootstrap
configMap:
name: minecraft-boot
- name: state
persistentVolumeClaim:
claimName: minecraft
- name: backups
persistentVolumeClaim:
claimName: minecraft-backups
- name: runtime
emptyDir: {}
- name: files
secret:
secretName: minecraft-files
items:
- key: server.properties
path: server.properties

View file

@ -0,0 +1,18 @@
apiVersion: kyverno.io/v2
kind: PolicyException
metadata:
name: minecraft-singleton
namespace: kyverno
annotations:
policies.kyverno.io/reason: Minecraft owns one local world and permits one active server.
quorate.internal/exception-owner: workloads
spec:
exceptions:
- policyName: min-replicas
ruleNames: [min-replicas]
match:
any:
- resources:
kinds: [Deployment]
namespaces: [minecraft]
names: [minecraft]

View file

@ -0,0 +1,53 @@
apiVersion: operator.victoriametrics.com/v1beta1
kind: VMPodScrape
metadata:
name: minecraft
namespace: victoriametrics
labels:
observability.quorate.internal/scrape: "true"
spec:
namespaceSelector:
matchNames: [minecraft]
selector:
matchLabels:
app.kubernetes.io/name: minecraft
app.kubernetes.io/instance: skies
podMetricsEndpoints:
- port: metrics
path: /metrics
interval: 30s
scrapeTimeout: 10s
relabelConfigs:
- targetLabel: application
replacement: minecraft
- sourceLabels:
- __meta_kubernetes_pod_label_app_kubernetes_io_instance
targetLabel: instance
- action: labeldrop
regex: pod
metricRelabelConfigs:
- action: labeldrop
regex: pod
- action: labeldrop
regex: (?i)(.*password.*|.*passwd.*|.*secret.*|.*token.*|.*auth.*|.*bearer.*|.*cookie.*|.*dsn.*|.*uri.*|.*url.*|.*cmdline.*|.*command.*|.*args.*|.*env.*)
---
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: vmagent-minecraft
namespace: victoriametrics
spec:
endpointSelector:
matchLabels:
app.kubernetes.io/name: vmagent
app.kubernetes.io/instance: main
egress:
- toEndpoints:
- matchLabels:
k8s:io.kubernetes.pod.namespace: minecraft
app.kubernetes.io/name: minecraft
app.kubernetes.io/instance: skies
toPorts:
- ports:
- port: "19565"
protocol: TCP

View file

@ -0,0 +1,5 @@
apiVersion: v1
kind: Namespace
metadata:
name: minecraft
labels: {gateway.quorate.internal/allow-short: "true"}

View file

@ -0,0 +1,104 @@
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: minecraft
namespace: minecraft
spec:
endpointSelector:
matchLabels:
app.kubernetes.io/name: minecraft
app.kubernetes.io/instance: skies
ingress:
- fromEndpoints:
- matchLabels:
k8s:io.kubernetes.pod.namespace: grafana
app.kubernetes.io/name: grafana
app.kubernetes.io/instance: grafana
toPorts:
- ports:
- port: "25575"
protocol: TCP
- fromEndpoints:
- matchLabels:
k8s:io.kubernetes.pod.namespace: victoriametrics
app.kubernetes.io/name: vmagent
app.kubernetes.io/instance: main
toPorts:
- ports:
- port: "19565"
protocol: TCP
- fromEndpoints:
- matchLabels:
k8s:io.kubernetes.pod.namespace: short
app.kubernetes.io/name: tunnel
app.kubernetes.io/instance: short
toPorts:
- ports: [{port: "25565", protocol: TCP}]
- fromEntities: [host]
toPorts:
- ports:
- port: "25565"
protocol: TCP
- fromCIDR:
- 10.115.187.218/24
- 10.63.81.107/24
- 10.162.8.60/24
- 10.82.151.234/24
toPorts:
- ports:
- port: "25565"
protocol: TCP
egress:
- toFQDNs:
- matchName: s3.quorate.internal
toPorts:
- ports: [{port: "443", protocol: TCP}]
- toEndpoints:
- matchLabels:
k8s:io.kubernetes.pod.namespace: versitygw
app.kubernetes.io/name: versitygw
app.kubernetes.io/instance: versitygw
toPorts:
- ports: [{port: "7070", protocol: TCP}]
- toEndpoints:
- matchLabels:
k8s:io.kubernetes.pod.namespace: kube-system
k8s:k8s-app: kube-dns
toPorts:
- ports:
- port: "53"
protocol: UDP
- port: "53"
protocol: TCP
rules:
dns:
- matchName: sessionserver.mojang.com
- matchName: api.minecraftservices.com
- matchName: s3.quorate.internal
- toFQDNs:
- matchName: sessionserver.mojang.com
- matchName: api.minecraftservices.com
toPorts:
- ports:
- port: "443"
protocol: TCP
---
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: minecraft-artifacts
namespace: versitygw
spec:
endpointSelector:
matchLabels:
app.kubernetes.io/name: versitygw
app.kubernetes.io/instance: versitygw
ingress:
- fromEndpoints:
- matchLabels:
k8s:io.kubernetes.pod.namespace: minecraft
app.kubernetes.io/name: minecraft
app.kubernetes.io/instance: skies
toPorts:
- ports: [{port: "7070", protocol: TCP}]

View file

@ -0,0 +1,9 @@
apiVersion: gateway.networking.k8s.io/v1
kind: TCPRoute
metadata: {name: minecraft, namespace: minecraft}
spec:
parentRefs:
- {name: gateway-short, namespace: kube-system, sectionName: minecraft}
rules:
- backendRefs:
- {name: minecraft, port: 25565}

View file

@ -0,0 +1,36 @@
apiVersion: v1
kind: Service
metadata:
name: minecraft
namespace: minecraft
labels:
io.cilium/bgp-announce: "true"
annotations:
lbipam.cilium.io/ips: 10.197.181.214
spec:
type: LoadBalancer
externalTrafficPolicy: Local
selector:
app.kubernetes.io/name: minecraft
app.kubernetes.io/instance: skies
ports:
- name: minecraft
port: 25565
targetPort: minecraft
protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
name: minecraft-rcon
namespace: minecraft
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: minecraft
app.kubernetes.io/instance: skies
ports:
- name: rcon
port: 25575
targetPort: rcon
protocol: TCP

View file

@ -0,0 +1,23 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: minecraft
namespace: minecraft
spec:
accessModes: [ReadWriteOnce]
storageClassName: openebs-hostpath
resources:
requests:
storage: 50Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: minecraft-backups
namespace: minecraft
spec:
accessModes: [ReadWriteOnce]
storageClassName: openebs-zfspv-1m-zstd
resources:
requests:
storage: 100Gi

View file

@ -0,0 +1,148 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: minecraft-vso
namespace: minecraft
---
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultAuth
metadata:
name: default
namespace: minecraft
spec:
method: kubernetes
mount: kubernetes
kubernetes:
role: minecraft
serviceAccount: minecraft-vso
vaultConnectionRef: vault/vault
---
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: minecraft-artifacts
namespace: minecraft
spec:
type: kv-v2
mount: secret
path: versitygw/users/minecraft
refreshAfter: 1h
destination:
name: minecraft-artifacts
create: true
transformation:
excludeRaw: true
excludes: [.*]
templates:
accesskey:
text: minecraft
secretkey:
text: '{{ get .Secrets "secretAccessKey" }}'
vaultAuthRef: default
---
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: minecraft-ca
namespace: minecraft
spec:
type: kv-v2
mount: secret
path: ca/internal
refreshAfter: 1h
destination:
name: minecraft-ca
create: true
transformation:
excludeRaw: true
vaultAuthRef: default
---
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: minecraft
namespace: minecraft
spec:
type: kv-v2
mount: secret
path: minecraft/server
destination:
name: minecraft
create: true
overwrite: true
refreshAfter: 1h
vaultAuthRef: default
rolloutRestartTargets:
- kind: Deployment
name: minecraft
---
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: minecraft-files
namespace: minecraft
spec:
type: kv-v2
mount: secret
path: minecraft/files
destination:
name: minecraft-files
create: true
overwrite: true
transformation:
excludeRaw: true
excludes: [.*]
templates:
server.properties:
text: '{{ get .Secrets "server.properties" }}'
refreshAfter: 1h
vaultAuthRef: default
---
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: minecraft-rcon
namespace: minecraft
spec:
type: kv-v2
mount: secret
path: minecraft/rcon
destination:
name: minecraft-rcon
create: true
transformation:
excludeRaw: true
excludes: [.*]
templates:
password:
text: '{{ get .Secrets "password" }}'
refreshAfter: 1h
vaultAuthRef: default
rolloutRestartTargets:
- kind: Deployment
name: minecraft
---
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: minecraft-pull
namespace: minecraft
spec:
type: kv-v2
mount: secret
path: quay/oauth/project-delivery-reader
refreshAfter: 5m
destination:
name: minecraft-pull
create: true
overwrite: true
type: kubernetes.io/dockerconfigjson
transformation:
excludeRaw: true
excludes: [.*]
templates:
.dockerconfigjson:
text: |
{{- $token := get .Secrets "token" -}}
{"auths":{"registry.quorate.internal":{"username":"$oauthtoken","password":"{{ $token }}","auth":"{{ printf "%s:%s" "$oauthtoken" $token | b64enc }}"}}}
vaultAuthRef: default

View file

@ -0,0 +1,19 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- k8s/namespace.yaml
- k8s/vso.yaml
- k8s/storage.yaml
- k8s/deployment.yaml
- k8s/service.yaml
- k8s/route.yaml
- k8s/metrics.yaml
- k8s/network-policy.yaml
- k8s/exception.yaml
configMapGenerator:
- name: minecraft-boot
namespace: minecraft
files:
- bootstrap.fsx
- store.fsx
- run.fsx

103
services/minecraft/pack.fsx Normal file
View file

@ -0,0 +1,103 @@
open System
open System.IO
open System.IO.Compression
open System.Security.Cryptography
open System.Text
open System.Text.Json
open System.Text.Json.Nodes
let args = fsi.CommandLineArgs |> Array.skip 1
if args.Length <> 3 then failwith "Usage: pack.fsx SOURCE.mrpack CLIENT_DIR OUTPUT_DIR"
let source, client, output = args[0], args[1], args[2]
let digest algorithm path =
use stream = File.OpenRead path
(match algorithm with
| "sha512" -> SHA512.HashData stream
| "sha1" -> SHA1.HashData stream
| _ -> invalidArg "algorithm" algorithm)
|> Convert.ToHexString
|> _.ToLowerInvariant()
let bytes (entry: ZipArchiveEntry) =
use stream = entry.Open()
use memory = new MemoryStream()
stream.CopyTo memory
memory.ToArray()
let text value = Encoding.UTF8.GetBytes(value: string)
let json value = JsonValue.Create(value: string) :> JsonNode
let shaderMods =
set [ "iris-neoforge-1.8.12+mc1.21.1.jar"
"monocle-0.2.3.ms.jar"
"colorwheel-neoforge-1.3.1-mc1.21.1.jar" ]
let shaderConfig =
set [ "overrides/config/iris.properties"
"overrides/config/iris-excluded.json"
"overrides/config/monocle.properties"
"overrides/config/colorwheel-client.toml" ]
let restores =
[ "ftb-essentials-neoforge-2101.1.10.jar", "https://edge.forgecdn.net/files/8442/866/ftb-essentials-neoforge-2101.1.10.jar"
"AdvancedBackups-neoforge-1.21-3.7.1.jar", "https://cdn.modrinth.com/data/Jrmoreqs/versions/ufIaRDFo/AdvancedBackups-neoforge-1.21-3.7.1.jar" ]
let patcher = "EuphoriaPatcher-1.10.5-r5.9.3-neoforge.jar"
let shader = "ComplementaryReimagined_r5.9.3.zip"
let patched = "ComplementaryReimagined_r5.9.3 + EuphoriaPatches_1.10.5"
let file path url =
let local = Path.Combine(client, path)
let hashes = JsonObject()
hashes["sha512"] <- json (digest "sha512" local)
hashes["sha1"] <- json (digest "sha1" local)
let env = JsonObject()
env["client"] <- json "required"
env["server"] <- json "unsupported"
let item = JsonObject()
item["path"] <- json path
item["hashes"] <- hashes
item["env"] <- env
item["downloads"] <- JsonArray(json url)
item["fileSize"] <- JsonValue.Create(FileInfo(local).Length)
item :> JsonNode
let build shaders =
use original = ZipFile.OpenRead source
let index = original.GetEntry("modrinth.index.json") |> bytes |> Encoding.UTF8.GetString |> JsonNode.Parse
let files = index["files"].AsArray()
for item in files do
let path = item["path"].GetValue<string>()
let hashes = item["hashes"]
if digest "sha512" (Path.Combine(client, path)) <> hashes["sha512"].GetValue<string>() then
failwithf "Source mod differs: %s" path
let replaced = restores |> List.map (fun (name, _) -> "mods/" + name) |> Set.ofList |> Set.add ("mods/" + patcher)
let selected = JsonArray()
for item in files do
let path = item["path"].GetValue<string>()
if not (replaced.Contains path) && (shaders || not (shaderMods.Contains(Path.GetFileName path))) then
selected.Add(item.DeepClone())
for name, url in restores do selected.Add(file ("mods/" + name) url)
if shaders then
selected.Add(file ("mods/" + patcher) "https://cdn.modrinth.com/data/4H6sumDB/versions/QMsRXtSJ/EuphoriaPatcher-1.10.5-r5.9.3-neoforge.jar")
selected.Add(file ("shaderpacks/" + shader) "https://cdn.modrinth.com/data/HVnmMxH1/versions/Bqen1mJX/ComplementaryReimagined_r5.9.3.zip")
index["files"] <- selected
index["name"] <- json (if shaders then "Enigmatica 10 - Custom (Shaders)" else "Enigmatica 10 - Custom (Minimal)")
index["versionId"] <- json (if shaders then "1.33.0-shaders.2" else "1.33.0-minimal.2")
index["summary"] <- json (if shaders then "Multiplayer pack with Complementary Reimagined and Euphoria Patches." else "Multiplayer pack without the shader renderer or administrative tools.")
let entries = System.Collections.Generic.SortedDictionary<string, byte[]>(StringComparer.Ordinal)
for entry in original.Entries do
if [".jar"; ".zip"; ".mrpack"; ".glsl"; ".fsh"; ".vsh"; ".csh"] |> List.exists (fun suffix -> entry.FullName.EndsWith(suffix, StringComparison.OrdinalIgnoreCase)) then
failwithf "Embedded payload must be a manifest download: %s" entry.FullName
if entry.FullName <> "modrinth.index.json" && not (shaderConfig.Contains entry.FullName) then
entries.Add(entry.FullName, bytes entry)
entries.Add("modrinth.index.json", text (index.ToJsonString(JsonSerializerOptions(WriteIndented = true))))
if shaders then
entries["overrides/config/iris.properties"] <- text ("shaderPack=" + patched + "\n")
let name = if shaders then "e10c-shaders.mrpack" else "e10c.mrpack"
let path = Path.Combine(output, name)
use destination = File.Open(path, FileMode.CreateNew)
use archive = new ZipArchive(destination, ZipArchiveMode.Create)
for KeyValue(name, data) in entries do
let entry = archive.CreateEntry(name, CompressionLevel.Optimal)
entry.LastWriteTime <- DateTimeOffset(1980, 1, 1, 0, 0, 0, TimeSpan.Zero)
use stream = entry.Open()
stream.Write data
printfn "%s files=%d mods=%d" path selected.Count (selected |> Seq.filter (fun item -> item["path"].GetValue<string>().StartsWith("mods/")) |> Seq.length)
Directory.CreateDirectory output |> ignore
build false
build true

View file

@ -0,0 +1,103 @@
from __future__ import annotations
import argparse
import hashlib
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
from typing import Any
import boto3
from botocore.config import Config
PREFIX = "minecraft/"
GROUPS = (
"mods",
"config",
"defaultconfigs",
"kubejs",
"libraries",
"libraries-integratedscripting",
"settings",
"tools",
"world/datapacks",
"world/serverconfig",
)
def inventory_name(group: str) -> str:
return group.replace("/", "-") + ".sha256"
def files_for(source: Path, tools: Path, exporter: Path) -> dict[str, dict[str, Path]]:
files: dict[str, dict[str, Path]] = {group: {} for group in GROUPS}
for group in ("config", "defaultconfigs", "kubejs", "libraries", "libraries-integratedscripting"):
root = source / group
files[group] = {p.relative_to(root).as_posix(): p for p in root.rglob("*") if p.is_file()}
files["mods"] = {p.name: p for p in (source / "mods").glob("*.jar")}
files["mods"][exporter.name] = exporter
files["settings"]["user_jvm_args.txt"] = source / "user_jvm_args.txt"
files["tools"] = {p.relative_to(tools).as_posix(): p for p in tools.rglob("*") if p.is_file()}
ranks = source / "world/serverconfig/ftbranks/ranks.snbt"
files["world/serverconfig"]["ftbranks/ranks.snbt"] = ranks
return files
def digest(path: Path) -> str:
with path.open("rb") as source:
return hashlib.file_digest(source, "sha256").hexdigest()
def publish(client: Any, files: dict[str, dict[str, Path]]) -> None:
def upload(item: tuple[str, str, Path]) -> tuple[str, str, str]:
group, name, path = item
expected = digest(path)
key = PREFIX + group + "/" + name
with path.open("rb") as source:
client.put_object(Bucket="artifacts", Key=key, Body=source)
body = client.get_object(Bucket="artifacts", Key=key)["Body"]
try:
actual = hashlib.file_digest(body, "sha256").hexdigest()
finally:
body.close()
if actual != expected:
raise ValueError(f"checksum differs: {key}")
return group, name, expected
entries = [(group, name, path) for group, items in files.items() for name, path in sorted(items.items())]
with ThreadPoolExecutor(max_workers=8) as executor:
hashes = []
for result in executor.map(upload, entries):
hashes.append(result)
if len(hashes) % 100 == 0:
print(f"verified {len(hashes)}/{len(entries)} files", flush=True)
for group in GROUPS:
lines = [f"{sha} {name}\n" for current, name, sha in hashes if current == group]
body = "".join(lines).encode()
key = PREFIX + inventory_name(group)
client.put_object(Bucket="artifacts", Key=key, Body=body)
remote = client.get_object(Bucket="artifacts", Key=key)["Body"]
try:
if remote.read() != body:
raise ValueError(f"inventory differs: {key}")
finally:
remote.close()
print(f"published {group}: {len(lines)} files", flush=True)
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("source", type=Path)
parser.add_argument("tools", type=Path)
parser.add_argument("exporter", type=Path)
args = parser.parse_args()
client = boto3.client(
"s3",
endpoint_url="https://s3.quorate.internal",
region_name="us-east-1",
config=Config(s3={"addressing_style": "path"}, max_pool_connections=8),
)
publish(client, files_for(args.source, args.tools, args.exporter))
if __name__ == "__main__":
main()

View file

@ -0,0 +1,24 @@
#load "store.fsx"
open System
open System.Runtime.InteropServices
open Bootstrap
use client = Store.connect "/artifacts"
run
{ Paths =
{ Data = "/data"
State = "/state"
Backups = "/backups"
Files = "/files" }
Eula = Environment.GetEnvironmentVariable "EULA"
Args = "libraries/net/neoforged/neoforge/21.1.253/unix_args.txt"
PasswordPath = "/rcon/password"
Saltatory = "6f1f573d493dcaeacad327cacfa722af5a07618c2668c93427614982882092d5" }
RuntimeInformation.ProcessArchitecture
(Store.manifest client)
(Store.fetch client)
(Store.saltatory client)
trust "/etc/ssl/certs/java/cacerts" "/data"

View file

@ -0,0 +1,53 @@
#load "store.fsx"
open System
open System.IO
open System.Net
open System.Text
open System.Threading.Tasks
open Amazon.Runtime
open Amazon.S3
open Amazon.S3.Model
open Bootstrap
let check condition message = if not condition then failwith message
type MemoryStore() =
inherit AmazonS3Client(BasicAWSCredentials("fixture", "synthetic-secret"), AmazonS3Config(ServiceURL = "http://127.0.0.1", AuthenticationRegion = "us-east-1"))
let mutable reads = 0
let mutable key = ""
member _.Reads = reads
member _.Key = key
override _.GetObjectAsync(request: GetObjectRequest, _) =
reads <- reads + 1
key <- request.Key
let bytes = Encoding.UTF8.GetBytes(if request.Key.EndsWith ".sha256" then "inventory" else "synthetic artifact")
Task.FromResult(new GetObjectResponse(HttpStatusCode = HttpStatusCode.OK, ResponseStream = new MemoryStream(bytes)))
let root = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"))
Directory.CreateDirectory root |> ignore
try
use client = new MemoryStore()
check (Store.manifest client "mods.sha256" = "inventory") "inventory"
let sample = Path.Combine(root, "sample")
File.WriteAllText(sample, "synthetic artifact")
let expected = digest sample
let path = Path.Combine(root, "object")
Store.fetch client "mods/a.jar" expected path
check (digest path = expected) "download"
check (client.Key = "minecraft/mods/a.jar") "pack key"
Store.fetch client "mods/a.jar" expected path
check (client.Reads = 2) "cache hit"
let rejected =
try
Store.fetch client "mods/a.jar" (String.replicate 64 "0") path
false
with :? InvalidOperationException -> true
check rejected "wrong checksum accepted"
check (digest path = expected) "failed download replaced cache"
Store.saltatory client expected (Path.Combine(root, "saltatory.jar"))
check (client.Key = "saltatory/" + expected + ".jar") "saltatory key"
printfn "store tests passed"
finally
Directory.Delete(root, true)

View file

@ -0,0 +1,36 @@
module Store
#r "/opt/fsi/packages/AWSSDK.Core.dll"
#r "/opt/fsi/packages/AWSSDK.S3.dll"
#load "bootstrap.fsx"
open System.IO
open Amazon.Runtime
open Amazon.S3
open Amazon.S3.Model
open Bootstrap
let connect auth =
let credential name = File.ReadAllText(Path.Combine(auth, name))
let config = AmazonS3Config(ServiceURL = "https://s3.quorate.internal", AuthenticationRegion = "us-east-1", ForcePathStyle = true)
new AmazonS3Client(BasicAWSCredentials(credential "accesskey", credential "secretkey"), config)
let request key = GetObjectRequest(BucketName = "artifacts", Key = key)
let manifest (client: AmazonS3Client) key =
use reply = client.GetObjectAsync(request ("minecraft/" + key)).GetAwaiter().GetResult()
use reader = new StreamReader(reply.ResponseStream)
reader.ReadToEnd()
let fetchObject (client: AmazonS3Client) key hash path =
if not (File.Exists path) || digest path <> hash then
let temporary = path + ".download"
use reply = client.GetObjectAsync(request key).GetAwaiter().GetResult()
use output = File.Create temporary
reply.ResponseStream.CopyTo output
output.Close()
if digest temporary <> hash then invalidOp "download checksum differs"
File.Move(temporary, path, true)
let fetch client key = fetchObject client ("minecraft/" + key)
let saltatory client hash = fetchObject client ("saltatory/" + hash + ".jar") hash

View file

@ -0,0 +1,58 @@
from __future__ import annotations
import hashlib
import io
import tempfile
import unittest
from pathlib import Path
from typing import Any
import publish
class MemoryStore:
def __init__(self) -> None:
self.objects: dict[str, bytes] = {}
def put_object(self, *, Bucket: str, Key: str, Body: Any) -> None:
assert Bucket == "artifacts"
self.objects[Key] = Body if isinstance(Body, bytes) else Body.read()
def get_object(self, *, Bucket: str, Key: str) -> dict[str, io.BytesIO]:
assert Bucket == "artifacts"
return {"Body": io.BytesIO(self.objects[Key])}
class PublishTest(unittest.TestCase):
def test_publish(self) -> None:
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
source, tools = root / "pack", root / "tools"
for group in ("config", "defaultconfigs", "kubejs", "libraries", "libraries-integratedscripting", "mods"):
(source / group).mkdir(parents=True)
(source / group / "file").write_text(group)
(source / "mods/test.jar").write_bytes(b"mod")
(source / "user_jvm_args.txt").write_text("-Xmx1G")
ranks = source / "world/serverconfig/ftbranks/ranks.snbt"
ranks.parent.mkdir(parents=True)
ranks.write_text("ranks")
(ranks.parent / "players.snbt").write_text("test player")
(source / "server.properties").write_text("test credential")
(tools / "amd64").mkdir(parents=True)
(tools / "amd64/mc-monitor").write_bytes(b"monitor")
exporter = root / "exporter.jar"
exporter.write_bytes(b"exporter")
files = publish.files_for(source, tools, exporter)
client = MemoryStore()
publish.publish(client, files)
self.assertEqual(set(files["mods"]), {"test.jar", "exporter.jar"})
self.assertNotIn("minecraft/server.properties", client.objects)
self.assertNotIn("minecraft/world/serverconfig/ftbranks/players.snbt", client.objects)
self.assertIn("minecraft/world/serverconfig/ftbranks/ranks.snbt", client.objects)
self.assertEqual(client.objects["minecraft/world-datapacks.sha256"], b"")
expected = hashlib.sha256(b"mod").hexdigest()
self.assertIn(f"{expected} test.jar\n".encode(), client.objects["minecraft/mods.sha256"])
if __name__ == "__main__":
unittest.main()