feat(deploy): add one-line bootstrap installer and container image

bootstrap.sh auto-detects the host package manager (apt/dnf) and installs whatever is missing: Docker, k3s, and PostgreSQL. It builds and imports the felis image, opens pg_hba to the pod CIDR, runs migrations, and applies the rendered control-plane bundle, leaving Web disabled pending 'felis setup'. The Dockerfile builds the distroless felis image; deploy/crd holds the MinecraftServer CRD.
This commit is contained in:
flyemoji committed 2026-06-27 00:40:39 +09:00
1 parent 99de43f74f
commit 58fa4b0af8
4 files changed
+869

No files matched your search

+424
View File
@@ -0,0 +1,424 @@
#!/usr/bin/env bash
#
# Felis one-line bootstrap installer.
#
# curl -fsSL <raw-url>/deploy/bootstrap.sh | sudo bash
#
# Brings a fresh single-node Linux host from nothing to a running Felis control
# plane: it installs whatever is missing (picking dnf or apt by OS), provisions a
# swap file on tiny hosts, then configures Docker, k3s and PostgreSQL, builds and
# imports the felis image, runs database migrations and applies the rendered
# install bundle (CRD + namespaces + RBAC + NetworkPolicies + control-plane
# Deployments + in-cluster registry).
#
# By design it stops short of serving the web panel. After it finishes you run
# `felis setup` on the host (a TUI) to create the Owner account; the SysAdmin web
# surface only unlocks once Web Zero-Trust is configured. See deploy/README.md.
#
# The script is idempotent: re-running it converges rather than duplicating, and
# generated secrets are persisted to /etc/felis/secrets.env so reruns reuse them.
#
# Tunables (export before running to override the demo defaults):
# FELIS_REPO_URL git URL to build from (default: the upstream repo)
# FELIS_REF branch/tag/sha (default: main)
# FELIS_IMAGE local image tag (default: felis:demo — never :latest)
# FELIS_ROOT_DOMAIN deployment root domain (default: <node-ip>.nip.io)
# FELIS_EGRESS_MODE loadbalancer|nodeport (default: nodeport — no MetalLB on a demo box)
set -euo pipefail
# ---------------------------------------------------------------------------
# Configuration & constants
# ---------------------------------------------------------------------------
FELIS_REPO_URL="${FELIS_REPO_URL:-https://github.com/MliroLirrorsIngenuity/Felis.git}"
FELIS_REF="${FELIS_REF:-main}"
FELIS_IMAGE="${FELIS_IMAGE:-felis:demo}"
FELIS_EGRESS_MODE="${FELIS_EGRESS_MODE:-nodeport}"
CONTROL_NS="felis"
MINECRAFT_NS="minecraft"
BUILD_NS="felis-build"
POD_CIDR="10.42.0.0/16" # k3s default cluster CIDR
DB_NAME="felis"
DB_USER="felis"
REGISTRY_URL="registry.felis.svc:5000"
STATE_DIR="/etc/felis"
SECRETS_ENV="${STATE_DIR}/secrets.env"
SRC_DIR="/opt/felis/src"
HOST_BIN="/usr/local/bin/felis"
# ---------------------------------------------------------------------------
# Logging
# ---------------------------------------------------------------------------
log() { printf '\033[1;36m[felis]\033[0m %s\n' "$*"; }
ok() { printf '\033[1;32m[ ok ]\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33m[warn]\033[0m %s\n' "$*" >&2; }
die() { printf '\033[1;31m[fail]\033[0m %s\n' "$*" >&2; exit 1; }
kube() { k3s kubectl "$@"; }
# ---------------------------------------------------------------------------
# 0. Privilege & host facts
# ---------------------------------------------------------------------------
if [ "$(id -u)" -ne 0 ]; then
log "re-executing under sudo"
exec sudo -E bash "$0" "$@"
fi
detect_os() {
[ -r /etc/os-release ] || die "cannot read /etc/os-release; unsupported host"
# shellcheck disable=SC1091
. /etc/os-release
OS_ID="${ID:-unknown}"
OS_VERSION="${VERSION_ID:-unknown}"
if command -v apt-get >/dev/null 2>&1; then
PKG="apt"
elif command -v dnf >/dev/null 2>&1; then
PKG="dnf"
elif command -v yum >/dev/null 2>&1; then
PKG="yum"
else
die "no supported package manager (apt/dnf/yum) found on ${OS_ID} ${OS_VERSION}"
fi
log "host: ${PRETTY_NAME:-$OS_ID $OS_VERSION} (package manager: ${PKG})"
}
detect_node_ip() {
NODE_IP="$(ip -4 route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="src"){print $(i+1); exit}}')"
[ -n "${NODE_IP:-}" ] || NODE_IP="$(hostname -I 2>/dev/null | awk '{print $1}')"
[ -n "${NODE_IP:-}" ] || die "could not determine this host's primary IPv4 address"
FELIS_ROOT_DOMAIN="${FELIS_ROOT_DOMAIN:-${NODE_IP}.nip.io}"
log "node IP: ${NODE_IP} root domain: ${FELIS_ROOT_DOMAIN}"
}
pkg_install() {
case "$PKG" in
apt) DEBIAN_FRONTEND=noninteractive apt-get install -y "$@" ;;
dnf) dnf install -y "$@" ;;
yum) yum install -y "$@" ;;
esac
}
pkg_refresh_once() {
[ -n "${_PKG_REFRESHED:-}" ] && return 0
case "$PKG" in
apt) DEBIAN_FRONTEND=noninteractive apt-get update -y ;;
dnf|yum) : ;; # dnf/yum refresh metadata on demand
esac
_PKG_REFRESHED=1
}
# ---------------------------------------------------------------------------
# 1. Swap — k3s + Postgres + a Go build will OOM on a <2 GiB box without it
# ---------------------------------------------------------------------------
ensure_swap() {
local mem_kb swap_kb
mem_kb="$(awk '/^MemTotal:/{print $2}' /proc/meminfo)"
swap_kb="$(awk '/^SwapTotal:/{print $2}' /proc/meminfo)"
if [ "${swap_kb:-0}" -gt 0 ]; then
ok "swap already present ($((swap_kb/1024)) MiB)"
return 0
fi
if [ "${mem_kb:-0}" -ge 2097152 ]; then
ok "RAM $((mem_kb/1024)) MiB is sufficient; skipping swap"
return 0
fi
log "low RAM ($((mem_kb/1024)) MiB) and no swap — creating a 2 GiB swap file"
if ! fallocate -l 2G /swapfile 2>/dev/null; then
dd if=/dev/zero of=/swapfile bs=1M count=2048 status=none
fi
chmod 600 /swapfile
mkswap /swapfile >/dev/null
swapon /swapfile
grep -q '^/swapfile ' /etc/fstab || echo '/swapfile none swap sw 0 0' >> /etc/fstab
ok "2 GiB swap active"
}
# ---------------------------------------------------------------------------
# 2. Base packages
# ---------------------------------------------------------------------------
install_base() {
pkg_refresh_once
pkg_install curl ca-certificates git openssl
ok "base tools present"
}
# ---------------------------------------------------------------------------
# 3. Docker (used only to build & export the felis image; k3s uses containerd)
# ---------------------------------------------------------------------------
install_docker() {
if command -v docker >/dev/null 2>&1; then
ok "docker already installed"
else
log "installing docker via get.docker.com"
curl -fsSL https://get.docker.com | sh
fi
systemctl enable --now docker
ok "docker running"
}
# ---------------------------------------------------------------------------
# 4. k3s — single node, trimmed for RAM. NetworkPolicy stays ENABLED on purpose:
# Felis's minecraft fence (default-deny + allow-rcon/allow-game) is a core
# security claim, so we must NOT pass --disable-network-policy.
# ---------------------------------------------------------------------------
install_k3s() {
if command -v k3s >/dev/null 2>&1; then
ok "k3s already installed"
else
log "installing k3s (no traefik/servicelb/metrics-server)"
curl -sfL https://get.k3s.io | \
INSTALL_K3S_EXEC="--disable traefik --disable servicelb --disable metrics-server --write-kubeconfig-mode 644" \
sh -
fi
systemctl enable --now k3s
export KUBECONFIG=/etc/rancher/k3s/k3s.yaml
log "waiting for the node to become Ready"
local i
for i in $(seq 1 60); do
if kube get nodes 2>/dev/null | grep -q ' Ready '; then
ok "k3s node Ready"
return 0
fi
sleep 5
done
kube get nodes || true
die "k3s node did not become Ready in time"
}
# ---------------------------------------------------------------------------
# 5. Source + image build + host binary + containerd import
# ---------------------------------------------------------------------------
fetch_source() {
if [ -n "${FELIS_SKIP_FETCH:-}" ]; then
[ -d "$SRC_DIR" ] || die "FELIS_SKIP_FETCH set but ${SRC_DIR} does not exist"
ok "skipping fetch; using pre-staged source at ${SRC_DIR}"
return 0
fi
if [ -d "${SRC_DIR}/.git" ]; then
log "updating source in ${SRC_DIR}"
git -C "$SRC_DIR" fetch --depth 1 origin "$FELIS_REF"
git -C "$SRC_DIR" checkout -f FETCH_HEAD
else
log "cloning ${FELIS_REPO_URL} (${FELIS_REF})"
mkdir -p "$(dirname "$SRC_DIR")"
git clone --depth 1 --branch "$FELIS_REF" "$FELIS_REPO_URL" "$SRC_DIR" 2>/dev/null \
|| git clone "$FELIS_REPO_URL" "$SRC_DIR"
fi
ok "source ready at ${SRC_DIR}"
}
build_image() {
systemctl start docker
log "building ${FELIS_IMAGE} (this compiles the Go binary; first run is slow)"
docker build -t "$FELIS_IMAGE" "$SRC_DIR"
log "extracting the felis binary onto the host (${HOST_BIN})"
local cid
cid="$(docker create "$FELIS_IMAGE")"
docker cp "${cid}:/usr/local/bin/felis" "$HOST_BIN"
docker rm "$cid" >/dev/null
chmod 0755 "$HOST_BIN"
log "importing ${FELIS_IMAGE} into k3s containerd"
docker save "$FELIS_IMAGE" | k3s ctr images import -
# Reclaim the ~150 MiB the docker daemon holds; reruns restart it on demand.
systemctl stop docker docker.socket 2>/dev/null || true
ok "image built, binary on host, image imported"
}
# ---------------------------------------------------------------------------
# 6. PostgreSQL on the host (apt/dnf). felis-api pods reach it at <node-ip>:5432;
# migrations run from the host binary against 127.0.0.1.
# ---------------------------------------------------------------------------
install_postgres() {
if command -v psql >/dev/null 2>&1 && systemctl list-unit-files 2>/dev/null | grep -q '^postgresql'; then
ok "postgresql already installed"
else
log "installing postgresql"
case "$PKG" in
apt) pkg_install postgresql ;;
dnf) pkg_install postgresql-server postgresql ;;
yum) pkg_install postgresql-server postgresql ;;
esac
fi
# RHEL-family ships an uninitialised data dir.
if [ "$PKG" != "apt" ] && [ ! -f /var/lib/pgsql/data/PG_VERSION ]; then
log "initialising postgresql data directory"
if command -v postgresql-setup >/dev/null 2>&1; then
postgresql-setup --initdb || /usr/bin/postgresql-setup initdb
fi
fi
systemctl enable --now postgresql
ok "postgresql running"
}
configure_postgres() {
local cfg hba
cfg="$(sudo -u postgres psql -tAc 'SHOW config_file;' 2>/dev/null || true)"
hba="$(sudo -u postgres psql -tAc 'SHOW hba_file;' 2>/dev/null || true)"
[ -n "$cfg" ] && [ -n "$hba" ] || die "could not query postgresql config/hba file paths"
# Listen on all interfaces (applied on restart). ALTER SYSTEM is idempotent.
sudo -u postgres psql -v ON_ERROR_STOP=1 -c "ALTER SYSTEM SET listen_addresses = '*';" >/dev/null
# Allow the host loopback, the pod CIDR, and the node IP (covers SNAT either way).
local line
for line in \
"host ${DB_NAME} ${DB_USER} 127.0.0.1/32 scram-sha-256" \
"host ${DB_NAME} ${DB_USER} ${POD_CIDR} scram-sha-256" \
"host ${DB_NAME} ${DB_USER} ${NODE_IP}/32 scram-sha-256" ; do
grep -qF "$line" "$hba" || echo "$line" >> "$hba"
done
# Role + database (idempotent), and (re)set the password to our generated one.
sudo -u postgres psql -v ON_ERROR_STOP=1 <<SQL >/dev/null
DO \$\$
BEGIN
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '${DB_USER}') THEN
CREATE ROLE ${DB_USER} LOGIN PASSWORD '${DB_PASSWORD}';
END IF;
END
\$\$;
ALTER ROLE ${DB_USER} WITH LOGIN PASSWORD '${DB_PASSWORD}';
SQL
if ! sudo -u postgres psql -tAc "SELECT 1 FROM pg_database WHERE datname='${DB_NAME}'" | grep -q 1; then
sudo -u postgres createdb -O "$DB_USER" "$DB_NAME"
fi
systemctl restart postgresql
ok "postgresql configured (listen=*, role/db '${DB_NAME}', pg_hba opened to pods)"
}
# ---------------------------------------------------------------------------
# 7. Secrets + felis.toml (pod variant reaches Postgres at the node IP; host
# variant at 127.0.0.1 for migrations)
# ---------------------------------------------------------------------------
load_or_make_secrets() {
mkdir -p "$STATE_DIR"
chmod 0700 "$STATE_DIR"
if [ -f "$SECRETS_ENV" ]; then
# shellcheck disable=SC1090
. "$SECRETS_ENV"
ok "reusing persisted secrets from ${SECRETS_ENV}"
fi
DB_PASSWORD="${DB_PASSWORD:-$(openssl rand -hex 24)}"
SERVICE_TOKEN="${SERVICE_TOKEN:-$(openssl rand -hex 32)}"
SESSION_SECRET="${SESSION_SECRET:-$(openssl rand -hex 32)}"
umask 077
cat > "$SECRETS_ENV" <<EOF
DB_PASSWORD=${DB_PASSWORD}
SERVICE_TOKEN=${SERVICE_TOKEN}
SESSION_SECRET=${SESSION_SECRET}
EOF
chmod 0600 "$SECRETS_ENV"
}
write_felis_toml() {
local target="$1" db_host="$2"
cat > "$target" <<EOF
# Generated by deploy/bootstrap.sh — do not edit by hand; rerun the installer.
[server]
listen = "0.0.0.0:8080"
root_domain = "${FELIS_ROOT_DOMAIN}"
[database]
url = "postgres://${DB_USER}:${DB_PASSWORD}@${db_host}:5432/${DB_NAME}?sslmode=disable"
[k8s]
namespace = "${MINECRAFT_NS}"
egress_mode = "${FELIS_EGRESS_MODE}"
[registry]
url = "${REGISTRY_URL}"
build_namespace = "${BUILD_NS}"
[archive]
store = "tarLocal"
local_path = "/var/lib/felis/archives"
[auth]
admin_hostname = "admin.${FELIS_ROOT_DOMAIN}"
panel_hostname = "panel.${FELIS_ROOT_DOMAIN}"
EOF
}
# ---------------------------------------------------------------------------
# 8. Migrate + deploy bundle
# ---------------------------------------------------------------------------
run_migrations() {
write_felis_toml "${STATE_DIR}/felis.host.toml" "127.0.0.1"
log "running database migrations (host binary -> 127.0.0.1)"
"$HOST_BIN" migrate up -config "${STATE_DIR}/felis.host.toml"
ok "migrations applied"
}
deploy_bundle() {
export KUBECONFIG=/etc/rancher/k3s/k3s.yaml
write_felis_toml "${STATE_DIR}/felis.pod.toml" "${NODE_IP}"
log "applying MinecraftServer CRD"
kube apply -f "${SRC_DIR}/deploy/crd/"
log "ensuring namespaces"
local ns
for ns in "$CONTROL_NS" "$MINECRAFT_NS" "$BUILD_NS"; do
kube create namespace "$ns" --dry-run=client -o yaml | kube apply -f -
done
log "provisioning felis-config + felis-service-token secrets (out-of-band, never in the bundle)"
kube -n "$CONTROL_NS" create secret generic felis-config \
--from-file=felis.toml="${STATE_DIR}/felis.pod.toml" \
--dry-run=client -o yaml | kube apply -f -
kube -n "$CONTROL_NS" create secret generic felis-service-token \
--from-literal=token="${SERVICE_TOKEN}" \
--dry-run=client -o yaml | kube apply -f -
log "rendering + applying the control-plane bundle"
"$HOST_BIN" manifests \
--felis-image "$FELIS_IMAGE" \
--velocity-cidr "${NODE_IP}/32" \
| kube apply -f -
log "waiting for control-plane rollouts"
local d
for d in $(kube -n "$CONTROL_NS" get deploy -o name); do
kube -n "$CONTROL_NS" rollout status "$d" --timeout=180s || warn "rollout not complete: $d"
done
}
# ---------------------------------------------------------------------------
# 9. Summary
# ---------------------------------------------------------------------------
summary() {
export KUBECONFIG=/etc/rancher/k3s/k3s.yaml
echo
ok "Felis control plane deployed."
echo
kube -n "$CONTROL_NS" get pods -o wide || true
echo
log "Web is intentionally NOT enabled yet."
log "Next: run 'sudo felis setup' on this host to create the Owner account."
log "The SysAdmin web surface unlocks only after Web Zero-Trust is configured."
echo
}
main() {
detect_os
detect_node_ip
ensure_swap
install_base
load_or_make_secrets
install_docker
install_k3s
fetch_source
build_image
install_postgres
configure_postgres
run_migrations
deploy_bundle
summary
}
main "$@"
@@ -0,0 +1,391 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.17.3
name: minecraftservers.felis.lolicon.best
spec:
group: felis.lolicon.best
names:
kind: MinecraftServer
listKind: MinecraftServerList
plural: minecraftservers
singular: minecraftserver
scope: Namespaced
versions:
- name: v1alpha1
schema:
openAPIV3Schema:
description: |-
MinecraftServer is the lifecycle source-of-truth for a single managed
Minecraft server (spec §4). The operator reconciles the StatefulSet, Service
and PVC from this object; readiness is gated exclusively on an RCON probe.
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
spec:
description: MinecraftServerSpec is the desired state (spec §4 spec.*).
properties:
args:
description: Args are extra arguments appended after the jar.
items:
type: string
type: array
autostartPolicy:
description: AutostartPolicy controls who may wake the server (spec
§8).
enum:
- public
- allowlist
- ownerOnly
type: string
description:
description: Description is free-form operator/owner notes.
type: string
desiredState:
description: DesiredState toggles the server up or down (default Stopped).
enum:
- Running
- Stopped
type: string
displayName:
description: DisplayName is the human-facing name shown in the panel
and MOTD.
type: string
env:
description: Env are extra environment variables injected into the
server container.
items:
description: |-
EnvVar is a name/value pair injected into the server container. It is a
deliberately narrow subset of corev1.EnvVar (no valueFrom) so the CRD cannot
be used to exfiltrate arbitrary cluster secrets.
properties:
name:
type: string
value:
type: string
required:
- name
type: object
type: array
fallbackServer:
description: |-
FallbackServer is the Velocity server name traffic routes to while this
server is stopped or starting.
type: string
idle:
description: Idle configures empty-server auto-stop (spec §8).
properties:
autoStopEnabled:
description: AutoStopEnabled turns on idle auto-stop.
type: boolean
emptySecondsBeforeStop:
description: |-
EmptySecondsBeforeStop is how long the server may sit empty before the
operator scales it down.
format: int32
type: integer
type: object
image:
description: Image is the fully-qualified container image (loader-agnostic).
type: string
jar:
description: |-
Jar is the server jar path/name inside the image, if the entrypoint
needs it explicitly.
type: string
javaFlags:
description: JavaFlags are additional JVM flags (e.g. Aikar's flags).
items:
type: string
type: array
javaMemory:
description: JavaMemory is the heap sizing passed as -Xmx/-Xms (e.g.
"4G").
type: string
lifecycle:
description: Lifecycle tunes graceful shutdown (spec §7).
properties:
preStopSaveAndStop:
description: PreStopSaveAndStop enables the operator-injected
RCON save+stop preStop.
type: boolean
terminationGracePeriodSeconds:
description: TerminationGracePeriodSeconds is the pod grace period
(default 300).
format: int64
type: integer
type: object
motd:
description: Motd holds the per-phase MOTD strings surfaced to status
pings.
properties:
failed:
type: string
running:
type: string
starting:
type: string
stopped:
type: string
type: object
onlineMode:
description: |-
OnlineMode mirrors server.properties online-mode. Wake/claim semantics
only hold when the Velocity proxy enforces online-mode=true (spec §8).
type: boolean
rcon:
description: |-
Rcon configures the RCON endpoint the operator probes for readiness and
uses for graceful shutdown (spec §5, §7).
properties:
enabled:
description: Enabled must be true for readiness probing and graceful
shutdown.
type: boolean
port:
description: Port is the RCON TCP port (default 25575).
format: int32
type: integer
secretRef:
description: SecretRef points at the Secret holding the RCON password.
properties:
key:
type: string
name:
type: string
required:
- key
- name
type: object
type: object
reaperExempt:
description: ReaperExempt opts this server out of the world reaper
entirely (spec §18).
type: boolean
resources:
description: Resources are the container resource requests/limits.
properties:
claims:
description: |-
Claims lists the names of resources, defined in spec.resourceClaims,
that are used by this container.
This is an alpha field and requires enabling the
DynamicResourceAllocation feature gate.
This field is immutable. It can only be set for containers.
items:
description: ResourceClaim references one entry in PodSpec.ResourceClaims.
properties:
name:
description: |-
Name must match the name of one entry in pod.spec.resourceClaims of
the Pod where this field is used. It makes that resource available
inside a container.
type: string
request:
description: |-
Request is the name chosen for a request in the referenced claim.
If empty, everything from the claim is made available, otherwise
only the result of this request.
type: string
required:
- name
type: object
type: array
x-kubernetes-list-map-keys:
- name
x-kubernetes-list-type: map
limits:
additionalProperties:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
description: |-
Limits describes the maximum amount of compute resources allowed.
More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
type: object
requests:
additionalProperties:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
description: |-
Requests describes the minimum amount of compute resources required.
If Requests is omitted for a container, it defaults to Limits if that is explicitly specified,
otherwise to an implementation-defined value. Requests cannot exceed Limits.
More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
type: object
type: object
startup:
description: Startup bounds how long Starting may last before Failed
(spec §5).
properties:
readinessTimeoutSeconds:
description: ReadinessTimeoutSeconds is the budget for the first
successful RCON probe.
format: int32
type: integer
timeoutSeconds:
description: TimeoutSeconds is the overall budget before the server
is marked Failed.
format: int32
type: integer
type: object
storage:
description: Storage configures the world PVC.
properties:
retainOnDelete:
description: RetainOnDelete keeps the PVC when the MinecraftServer
is deleted.
type: boolean
size:
description: Size is the requested PVC capacity (e.g. "10Gi").
type: string
storageClassName:
description: StorageClassName selects the StorageClass; empty
uses the default.
type: string
type: object
subdomain:
description: |-
Subdomain is the per-server label under the deployment zone. It is the
only routing identity; the operator never hardcodes the parent domain.
type: string
required:
- image
- subdomain
type: object
status:
description: MinecraftServerStatus is the observed state (spec §4 status.*).
properties:
conditions:
description: Conditions are the standard metav1 conditions (Ready,
RconReached, ...).
items:
description: Condition contains details for one aspect of the current
state of this API Resource.
properties:
lastTransitionTime:
description: |-
lastTransitionTime is the last time the condition transitioned from one status to another.
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
format: date-time
type: string
message:
description: |-
message is a human readable message indicating details about the transition.
This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
description: |-
observedGeneration represents the .metadata.generation that the condition was set based upon.
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
with respect to the current state of the instance.
format: int64
minimum: 0
type: integer
reason:
description: |-
reason contains a programmatic identifier indicating the reason for the condition's last transition.
Producers of specific condition types may define expected values and meanings for this field,
and whether the values are considered a guaranteed API.
The value should be a CamelCase string.
This field may not be empty.
maxLength: 1024
minLength: 1
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
type: string
status:
description: status of the condition, one of True, False, Unknown.
enum:
- "True"
- "False"
- Unknown
type: string
type:
description: type of condition in CamelCase or in foo.example.com/CamelCase.
maxLength: 316
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
required:
- lastTransitionTime
- message
- reason
- status
- type
type: object
type: array
endpoint:
description: Endpoint is where the proxy should route traffic.
properties:
address:
description: Address is the host:port the proxy should dial.
type: string
mode:
description: Mode is "direct" or "fallback".
type: string
type: object
liveMotd:
description: LiveMotd is the MOTD currently advertised for the active
phase.
type: string
observedGeneration:
description: ObservedGeneration is the spec generation this status
reflects.
format: int64
type: integer
phase:
description: Phase is the coarse lifecycle phase.
type: string
players:
description: Players is the last observed player count.
properties:
max:
format: int32
type: integer
online:
format: int32
type: integer
required:
- max
- online
type: object
ready:
description: |-
Ready is true only after a successful RCON probe (loader-agnostic; a
status ping is never sufficient — spec §5).
type: boolean
readySignalAt:
description: ReadySignalAt is when the first RCON probe succeeded.
format: date-time
type: string
type: object
type: object
served: true
storage: true
subresources:
status: {}