app #0: cube.famzheng.me 入口门户 + 平台脚手架
deploy cube / build-and-deploy (push) Has been cancelled

monorepo 第一刀:
- workspace + crates/cube-core(base router / healthz / ServeDir SPA fallback / JSON tracing / SIGTERM shutdown)
- apps/cube:axum 主程序 + Vite + Vue 3 + TS 门户(暗色调 + 渐变 logo + app 卡片网格)
- Dockerfile:scratch + musl 静态二进制,镜像 2.6MB
- k8s/:cube-cube ns + Deployment + Service + Ingress(cube.famzheng.me,traefik LE 自动签)
- registry:新增 registry.famzheng.me ingress 反代到 gitea 内置 container registry,
  自动化身份用 mochi(registry.famzheng.me/mochi/cube)
- CI:.gitea/workflows/deploy-cube.yml,host shell runner(gnoc),
  build → push → kubectl rollout 五步流水
- README:把宪法段改成 monorepo 模式 + monorepo 目录结构
- 新增宪法条款:前端视图状态走 URL(path + query)保证可 bookmark
This commit is contained in:
Fam Zheng
2026-05-04 11:22:59 +01:00
parent 011e7ddb98
commit 93b6fa3061
28 changed files with 3018 additions and 29 deletions
+52
View File
@@ -0,0 +1,52 @@
name: deploy cube
# app #0cube.famzheng.me 入口门户。host shell runnergnoc 用户)。
on:
push:
branches: [master]
paths:
- 'apps/cube/**'
- 'crates/cube-core/**'
- 'Cargo.toml'
- 'Cargo.lock'
- '.gitea/workflows/deploy-cube.yml'
workflow_dispatch:
jobs:
build-and-deploy:
runs-on: ubuntu-latest:host # host shell executorPATH 需要手动加 ~/.cargo/bin
env:
APP: cube
IMAGE: registry.famzheng.me/mochi/cube
steps:
- uses: actions/checkout@v4
- name: Resolve image tag
id: tag
run: |
echo "sha=$(git rev-parse --short=12 HEAD)" >> "$GITHUB_OUTPUT"
- name: Build rust (musl static)
run: |
export PATH="$HOME/.cargo/bin:$PATH"
cargo build --release --target x86_64-unknown-linux-musl -p "$APP"
- name: Build frontend
run: |
cd "apps/$APP/frontend"
npm ci
npm run build
- name: Build & push image
env:
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
run: |
echo "$REGISTRY_TOKEN" | docker login registry.famzheng.me -u mochi --password-stdin
docker build -f "apps/$APP/Dockerfile" -t "$IMAGE:${{ steps.tag.outputs.sha }}" .
docker push "$IMAGE:${{ steps.tag.outputs.sha }}"
- name: Roll out to k3s
# runner 是 gnoc 用户 host shell 模式,直接用 ~/.kube/config(已配好),无需 secret
run: |
kubectl -n "cube-$APP" set image "deploy/$APP" "$APP=$IMAGE:${{ steps.tag.outputs.sha }}"
kubectl -n "cube-$APP" rollout status "deploy/$APP" --timeout=120s
+4
View File
@@ -0,0 +1,4 @@
/target
**/node_modules
**/dist
.DS_Store
Generated
+805
View File
@@ -0,0 +1,805 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "aho-corasick"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [
"memchr",
]
[[package]]
name = "async-trait"
version = "0.1.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "atomic-waker"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]]
name = "axum"
version = "0.7.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f"
dependencies = [
"async-trait",
"axum-core",
"bytes",
"futures-util",
"http",
"http-body",
"http-body-util",
"hyper",
"hyper-util",
"itoa",
"matchit",
"memchr",
"mime",
"percent-encoding",
"pin-project-lite",
"rustversion",
"serde",
"serde_json",
"serde_path_to_error",
"serde_urlencoded",
"sync_wrapper",
"tokio",
"tower",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "axum-core"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199"
dependencies = [
"async-trait",
"bytes",
"futures-util",
"http",
"http-body",
"http-body-util",
"mime",
"pin-project-lite",
"rustversion",
"sync_wrapper",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "bitflags"
version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
[[package]]
name = "bytes"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "cube"
version = "0.1.0"
dependencies = [
"cube-core",
"tokio",
]
[[package]]
name = "cube-core"
version = "0.1.0"
dependencies = [
"axum",
"tokio",
"tower-http",
"tracing",
"tracing-subscriber",
]
[[package]]
name = "errno"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys",
]
[[package]]
name = "form_urlencoded"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
dependencies = [
"percent-encoding",
]
[[package]]
name = "futures-channel"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
dependencies = [
"futures-core",
]
[[package]]
name = "futures-core"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
[[package]]
name = "futures-sink"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
[[package]]
name = "futures-task"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
[[package]]
name = "futures-util"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
dependencies = [
"futures-core",
"futures-task",
"pin-project-lite",
"slab",
]
[[package]]
name = "http"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
dependencies = [
"bytes",
"itoa",
]
[[package]]
name = "http-body"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
dependencies = [
"bytes",
"http",
]
[[package]]
name = "http-body-util"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
dependencies = [
"bytes",
"futures-core",
"http",
"http-body",
"pin-project-lite",
]
[[package]]
name = "http-range-header"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c"
[[package]]
name = "httparse"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
[[package]]
name = "httpdate"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "hyper"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca"
dependencies = [
"atomic-waker",
"bytes",
"futures-channel",
"futures-core",
"http",
"http-body",
"httparse",
"httpdate",
"itoa",
"pin-project-lite",
"smallvec",
"tokio",
]
[[package]]
name = "hyper-util"
version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
dependencies = [
"bytes",
"http",
"http-body",
"hyper",
"pin-project-lite",
"tokio",
"tower-service",
]
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
name = "lock_api"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
dependencies = [
"scopeguard",
]
[[package]]
name = "log"
version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "matchers"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
dependencies = [
"regex-automata",
]
[[package]]
name = "matchit"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
[[package]]
name = "memchr"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]]
name = "mime"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "mime_guess"
version = "2.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
dependencies = [
"mime",
"unicase",
]
[[package]]
name = "mio"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
dependencies = [
"libc",
"wasi",
"windows-sys",
]
[[package]]
name = "nu-ansi-term"
version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
"windows-sys",
]
[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "parking_lot"
version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
dependencies = [
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.9.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
"windows-link",
]
[[package]]
name = "percent-encoding"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "pin-project-lite"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
[[package]]
name = "redox_syscall"
version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
dependencies = [
"bitflags",
]
[[package]]
name = "regex-automata"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]]
name = "rustversion"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "ryu"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.149"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "serde_path_to_error"
version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
dependencies = [
"itoa",
"serde",
"serde_core",
]
[[package]]
name = "serde_urlencoded"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
dependencies = [
"form_urlencoded",
"itoa",
"ryu",
"serde",
]
[[package]]
name = "sharded-slab"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
dependencies = [
"lazy_static",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
dependencies = [
"errno",
"libc",
]
[[package]]
name = "slab"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "smallvec"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "socket2"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
dependencies = [
"libc",
"windows-sys",
]
[[package]]
name = "syn"
version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "sync_wrapper"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
[[package]]
name = "thread_local"
version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
dependencies = [
"cfg-if",
]
[[package]]
name = "tokio"
version = "1.52.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6"
dependencies = [
"bytes",
"libc",
"mio",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2",
"tokio-macros",
"windows-sys",
]
[[package]]
name = "tokio-macros"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tokio-util"
version = "0.7.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
dependencies = [
"bytes",
"futures-core",
"futures-sink",
"pin-project-lite",
"tokio",
]
[[package]]
name = "tower"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
dependencies = [
"futures-core",
"futures-util",
"pin-project-lite",
"sync_wrapper",
"tokio",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "tower-http"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
dependencies = [
"bitflags",
"bytes",
"futures-core",
"futures-util",
"http",
"http-body",
"http-body-util",
"http-range-header",
"httpdate",
"mime",
"mime_guess",
"percent-encoding",
"pin-project-lite",
"tokio",
"tokio-util",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "tower-layer"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
[[package]]
name = "tower-service"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
[[package]]
name = "tracing"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
dependencies = [
"log",
"pin-project-lite",
"tracing-attributes",
"tracing-core",
]
[[package]]
name = "tracing-attributes"
version = "0.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tracing-core"
version = "0.1.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
dependencies = [
"once_cell",
"valuable",
]
[[package]]
name = "tracing-log"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
dependencies = [
"log",
"once_cell",
"tracing-core",
]
[[package]]
name = "tracing-serde"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1"
dependencies = [
"serde",
"tracing-core",
]
[[package]]
name = "tracing-subscriber"
version = "0.3.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
dependencies = [
"matchers",
"nu-ansi-term",
"once_cell",
"regex-automata",
"serde",
"serde_json",
"sharded-slab",
"smallvec",
"thread_local",
"tracing",
"tracing-core",
"tracing-log",
"tracing-serde",
]
[[package]]
name = "unicase"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "valuable"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "zmij"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
+26
View File
@@ -0,0 +1,26 @@
[workspace]
resolver = "2"
members = [
"crates/cube-core",
"apps/cube",
]
[workspace.package]
edition = "2021"
license = "MIT"
authors = ["Fam Zheng <fam@euphon.net>"]
[workspace.dependencies]
axum = "0.7"
tokio = { version = "1", features = ["full"] }
tower = "0.5"
tower-http = { version = "0.6", features = ["fs", "trace"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
[profile.release]
opt-level = "z"
lto = true
codegen-units = 1
strip = true
panic = "abort"
+51 -24
View File
@@ -16,10 +16,35 @@ Fam 的小 app 平台。
> 这是 cube 上所有 app 的"宪法",所有 app 必须遵守,破例必须改这份文档并经 Fam 拍板。 > 这是 cube 上所有 app 的"宪法",所有 app 必须遵守,破例必须改这份文档并经 Fam 拍板。
### 仓库结构:monorepo + cargo workspace
整个 cube 是**一个 git 仓库** `fam/cube`cargo workspace 模式。
```
cube/
├── Cargo.toml # workspace manifest
├── crates/
│ └── cube-core/ # 共享样板:base router / healthz / tracing / shutdown / config
├── apps/
│ ├── cube/ # app #0 = cube.famzheng.me 入口门户
│ │ ├── Cargo.toml # path 依赖 cube-core
│ │ ├── src/main.rs
│ │ ├── frontend/ # vite + vue 3 + ts
│ │ ├── Dockerfile
│ │ └── k8s/ # deployment / service / ingress
│ ├── portfolio/
│ ├── repo-vis/
│ └── ...
├── doc/
└── .gitea/workflows/ # CI;按 path 触发,per-app build/deploy
```
`cube-core`**path 依赖**`cube-core = { path = "../../crates/cube-core" }`),不发布、不打 tag、不走 git 依赖。改 `cube-core` 时所有 app 一起 rebuildmonorepo 的整个意义就在于"一次重构,全员同步"。
### 部署目标 ### 部署目标
单一目标:famzheng.me 节点的 k3s`kubectl context default`),不双轨。 单一目标:famzheng.me 节点的 k3s`kubectl context default`),不双轨。
- 每 app 一个 k8s namespacens 名 = app 名,不加 cube- 前缀) - 每 app 一个 k8s namespacens 名 = `cube-<app>`(如 `cube-cube` / `cube-portfolio`),便于跟其他 ns 隔离 + 一眼可见归属
- traefik ingress + 通配符 LE 证书自动签 - traefik ingress + 通配符 LE 证书自动签
### 域名 ### 域名
@@ -28,23 +53,24 @@ Fam 的小 app 平台。
- 不嵌 `cube` 子域(冲突检查 = 起新 ingress 时 traefik 自然报错,不需要额外心智) - 不嵌 `cube` 子域(冲突检查 = 起新 ingress 时 traefik 自然报错,不需要额外心智)
- 旧域名(如 `portfolio.oci.euphon.net`)让 oci ingress 兜底 308 redirect 到新地址,过渡期后下掉 - 旧域名(如 `portfolio.oci.euphon.net`)让 oci ingress 兜底 308 redirect 到新地址,过渡期后下掉
### 后端:Rust + Axum,每 app 独立仓库 ### 后端:Rust + Axum
- gitea repo `fam/<app>`,单 axum 服务 每个 app 是 workspace 里的一个 bin crate,单 axum 服务
- 公共代码通过 `cube-core` crate 复用:`cube-core = { git = "https://famzheng.me/gitea/fam/cube-core", tag = "v0.x" }`
- `cube-core` 提供: `cube-core` 提供:
- `/healthz` router200 = ok - `/healthz` router200 = ok
- `ServeDir` 静态前端 fallback 到 `index.html` - `ServeDir` 静态前端 fallback 到 `index.html`SPA 路由兼容)
- `tracing` 配 JSON stdout - `tracing` 配 JSON stdout
- SIGTERM graceful shutdown - SIGTERM graceful shutdown
- env → struct 配置加载`envy` crate - env → struct 配置加载
- 业务 app 只写 `/api/*` 路由 + handler
业务 app 只写 `/api/*` 路由 + handler
```rust ```rust
Router::new() let app = cube_core::base("dist")
.merge(cube_core::base("dist")) .nest("/api", api_routes());
.nest("/api", api_routes()) cube_core::serve(app).await
``` ```
- 不上 cargo workspace —— 每 app 一个 repo 彻底分
### 前端:Vite + Vue 3 ### 前端:Vite + Vue 3
@@ -55,19 +81,19 @@ Fam 的小 app 平台。
### 构建:host musl + scratch 容器 ### 构建:host musl + scratch 容器
host 上直接 cargo build,不在容器里跑 cargo host 上直接 cargo build,不在容器里跑 cargo。每个 app 的 build 命令固定
```bash ```bash
cargo build --release --target x86_64-unknown-linux-musl cargo build --release --target x86_64-unknown-linux-musl -p <app>
(cd frontend && npm ci && npm run build) (cd apps/<app>/frontend && npm ci && npm run build)
``` ```
Dockerfile Dockerfile(每个 app 自带一份,结构一致)
```dockerfile ```dockerfile
FROM scratch FROM scratch
COPY target/x86_64-unknown-linux-musl/release/<app> /app COPY target/x86_64-unknown-linux-musl/release/<app> /app
COPY frontend/dist /dist COPY apps/<app>/frontend/dist /dist
ENTRYPOINT ["/app"] ENTRYPOINT ["/app"]
``` ```
@@ -80,20 +106,20 @@ ENTRYPOINT ["/app"]
-**gitea 自带** Container RegistryPackages 功能,gitea 1.20+ 自带) -**gitea 自带** Container RegistryPackages 功能,gitea 1.20+ 自带)
- **坑**gitea 挂在 `/gitea/` 子路径下,docker daemon 默认拼 `https://famzheng.me/v2/...` 会 404 - **坑**gitea 挂在 `/gitea/` 子路径下,docker daemon 默认拼 `https://famzheng.me/v2/...` 会 404
- 方案:加一条 ingress `registry.famzheng.me` 反代 `/v2/*``gitea-svc:3000/v2/*`,复用 gitea token 鉴权 - 方案:加一条 ingress `registry.famzheng.me` 反代 `/v2/*``gitea-svc:3000/v2/*`,复用 gitea token 鉴权
- 镜像命名:`registry.famzheng.me/fam/<app>:<sha>` - 镜像命名:`registry.famzheng.me/mochi/<app>:<sha>`
- **未实施**:第一个 app 上线时再搞 ingress;过渡期可本地 build 直接 `docker save` + `k3s ctr image import` - **未实施**:第一个 app 上线时再搞 ingress;过渡期可本地 build 直接 `docker save` + `k3s ctr image import`
### CI/CD ### CI/CD
走 gitea Actions,复用现有 instance-level act_runnergnoc 用户,host shell 模式,labels `*:host`**新 repo 不用注册 runner**)。 走 gitea Actions,复用现有 instance-level act_runnergnoc 用户,host shell 模式,labels `*:host`**新 repo 不用注册 runner**)。
每个 app repo 一份 `.gitea/workflows/deploy.yml`固定 5 步: monorepo 每 app 一份 workflow `.gitea/workflows/deploy-<app>.yml`,按 `paths` 触发(只改 `apps/<app>/**``crates/cube-core/**` 才跑)。固定 5 步:
1. `cargo build --release --target x86_64-unknown-linux-musl` 1. `cargo build --release --target x86_64-unknown-linux-musl -p <app>`
2. `(cd frontend && npm ci && npm run build)` 2. `(cd apps/<app>/frontend && npm ci && npm run build)`
3. `docker build -t registry.famzheng.me/fam/<app>:$GITHUB_SHA .` 3. `docker build -f apps/<app>/Dockerfile -t registry.famzheng.me/mochi/<app>:$GITHUB_SHA .`
4. `docker push registry.famzheng.me/fam/<app>:$GITHUB_SHA` 4. `docker push registry.famzheng.me/mochi/<app>:$GITHUB_SHA`
5. `kubectl -n <app> set image deploy/<app> <app>=registry.famzheng.me/fam/<app>:$GITHUB_SHA` 5. `kubectl -n cube-<app> set image deploy/<app> <app>=registry.famzheng.me/mochi/<app>:$GITHUB_SHA`
host shell PATH 注意:workflow 第一行 `export PATH="$HOME/.cargo/bin:$PATH"`gnoc 的 rustup 装在 `~/.cargo`)。 host shell PATH 注意:workflow 第一行 `export PATH="$HOME/.cargo/bin:$PATH"`gnoc 的 rustup 装在 `~/.cargo`)。
@@ -106,6 +132,7 @@ host shell PATH 注意:workflow 第一行 `export PATH="$HOME/.cargo/bin:$PATH
- 配置走 env var + k8s Secret**禁止** config.yaml 文件挂载(partiverse 那个"空目录顶替 config 文件"的坑别再踩) - 配置走 env var + k8s Secret**禁止** config.yaml 文件挂载(partiverse 那个"空目录顶替 config 文件"的坑别再踩)
- 数据持久化只走 PVC + 每天 minio backup CronJob`cube-core` 提供 base 模板) - 数据持久化只走 PVC + 每天 minio backup CronJob`cube-core` 提供 base 模板)
- 镜像 tag 用 git SHA,不用 `latest` - 镜像 tag 用 git SHA,不用 `latest`
- **前端视图状态走 URL**path + query params),保证任意视图(筛选、分页、tab、详情)都能被 bookmark / share。可收藏的 state 必须在 URL 里,不准只活在 Pinia / `ref()` 里。Vue Router 的 `useRoute()` + `router.replace({ query: ... })` 是默认搭配。
--- ---
+11
View File
@@ -0,0 +1,11 @@
[package]
name = "cube"
version = "0.1.0"
edition.workspace = true
license.workspace = true
authors.workspace = true
description = "cube.famzheng.me — cube 平台入口门户(app #0"
[dependencies]
cube-core = { path = "../../crates/cube-core" }
tokio = { workspace = true }
+8
View File
@@ -0,0 +1,8 @@
# cube app #0 — 入口门户
# Build context = repo root(不是 apps/cube/),所以路径都是 apps/cube/...
# build 流程在 host 上跑(不在容器里),见 README"构建:host musl + scratch 容器"
FROM scratch
COPY target/x86_64-unknown-linux-musl/release/cube /cube
COPY apps/cube/frontend/dist /dist
EXPOSE 8080
ENTRYPOINT ["/cube"]
+14
View File
@@ -0,0 +1,14 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>cube — Fam's small app platform</title>
<meta name="description" content="Fam 的小 app 平台 · cube.famzheng.me" />
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
+20
View File
@@ -0,0 +1,20 @@
{
"name": "cube-portal",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.5.13"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.1",
"typescript": "~5.7.2",
"vite": "^6.0.5",
"vue-tsc": "^2.2.0"
}
}
+10
View File
@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<defs>
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#7c3aed"/>
<stop offset="100%" stop-color="#06b6d4"/>
</linearGradient>
</defs>
<rect x="8" y="8" width="48" height="48" rx="10" fill="url(#g)"/>
<path d="M20 20 L44 20 L44 44 L20 44 Z M20 20 L32 14 L56 14 L44 20 M44 20 L56 14 L56 38 L44 44" fill="none" stroke="white" stroke-width="2.4" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 499 B

+99
View File
@@ -0,0 +1,99 @@
<script setup lang="ts">
import AppCard from './components/AppCard.vue'
import { apps } from './apps'
</script>
<template>
<main class="page">
<header class="hero">
<div class="logo">
<svg viewBox="0 0 64 64" aria-hidden="true">
<defs>
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#7c3aed" />
<stop offset="100%" stop-color="#06b6d4" />
</linearGradient>
</defs>
<rect x="8" y="8" width="48" height="48" rx="10" fill="url(#g)" />
<path
d="M20 20 L44 20 L44 44 L20 44 Z M20 20 L32 14 L56 14 L44 20 M44 20 L56 14 L56 38 L44 44"
fill="none" stroke="white" stroke-width="2.4" stroke-linejoin="round"
/>
</svg>
</div>
<h1>cube</h1>
<p class="tagline">Fam 的小 app 平台 · <code>*.famzheng.me</code></p>
</header>
<section class="grid">
<AppCard v-for="app in apps" :key="app.slug" :app="app" />
</section>
<footer class="foot">
<span>cube · monorepo at</span>
<a href="https://famzheng.me/gitea/fam/cube" target="_blank" rel="noopener">famzheng.me/gitea/fam/cube</a>
</footer>
</main>
</template>
<style scoped>
.page {
max-width: 960px;
margin: 0 auto;
padding: 4rem 1.5rem 3rem;
}
.hero {
text-align: center;
margin-bottom: 3rem;
}
.logo {
width: 88px;
height: 88px;
margin: 0 auto 1.25rem;
filter: drop-shadow(0 8px 24px rgba(124, 58, 237, 0.35));
}
.logo svg { width: 100%; height: 100%; }
h1 {
font-size: 3.25rem;
font-weight: 700;
letter-spacing: -0.04em;
background: linear-gradient(135deg, #fff 0%, #b8c0d6 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.tagline {
margin-top: 0.5rem;
color: var(--fg-dim);
font-size: 1rem;
}
.tagline code {
background: var(--bg-soft);
border: 1px solid var(--border);
padding: 0.1rem 0.45rem;
border-radius: 6px;
font-size: 0.9em;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 1rem;
margin-bottom: 3rem;
}
.foot {
text-align: center;
color: var(--fg-dim);
font-size: 0.85rem;
}
.foot a {
color: var(--accent-2);
margin-left: 0.35rem;
}
.foot a:hover { text-decoration: underline; }
</style>
+54
View File
@@ -0,0 +1,54 @@
export type AppStatus = 'live' | 'pending' | 'tbd'
export interface App {
slug: string
name: string
description: string
url: string
status: AppStatus
}
export const apps: App[] = [
{
slug: 'cube',
name: 'cube',
description: '你正在看的这个门户。cube 平台本身的入口。',
url: 'https://cube.famzheng.me',
status: 'live',
},
{
slug: 'portfolio',
name: 'portfolio',
description: '投资组合追踪。从 oci 迁移中(原 portfolio.oci.euphon.net)。',
url: 'https://portfolio.famzheng.me',
status: 'pending',
},
{
slug: 'repo-vis',
name: 'repo-vis',
description: 'git 仓库可视化。从 oci 迁移中。',
url: 'https://repo-vis.famzheng.me',
status: 'pending',
},
{
slug: 'simpleasm',
name: 'simpleasm',
description: '汇编教学/玩具。从 oci 迁移中(原 asm.oci.euphon.net)。',
url: 'https://simpleasm.famzheng.me',
status: 'pending',
},
{
slug: 'guitar',
name: 'guitar',
description: '吉他 player。从 oci 迁移中(原 player.oci.euphon.net)。',
url: 'https://guitar.famzheng.me',
status: 'pending',
},
{
slug: 'pyroblem',
name: 'pyroblem',
description: '详情待补。',
url: 'https://pyroblem.famzheng.me',
status: 'tbd',
},
]
@@ -0,0 +1,94 @@
<script setup lang="ts">
import type { App } from '../apps'
defineProps<{ app: App }>()
const statusLabel: Record<App['status'], string> = {
live: '运行中',
pending: '迁移中',
tbd: '待规划',
}
</script>
<template>
<component
:is="app.status === 'live' ? 'a' : 'div'"
:href="app.status === 'live' ? app.url : undefined"
:target="app.status === 'live' ? '_blank' : undefined"
rel="noopener"
class="card"
:class="`is-${app.status}`"
>
<div class="row">
<h2>{{ app.name }}</h2>
<span class="status">{{ statusLabel[app.status] }}</span>
</div>
<p>{{ app.description }}</p>
<span class="url">{{ app.url.replace(/^https?:\/\//, '') }}</span>
</component>
</template>
<style scoped>
.card {
display: flex;
flex-direction: column;
gap: 0.6rem;
padding: 1.1rem 1.2rem;
background: var(--bg-soft);
border: 1px solid var(--border);
border-radius: 12px;
transition: border-color 0.15s ease, transform 0.15s ease, box-shadow 0.15s ease;
}
.card.is-live {
cursor: pointer;
}
.card.is-live:hover {
border-color: var(--accent-2);
transform: translateY(-2px);
box-shadow: 0 10px 30px -10px rgba(6, 182, 212, 0.3);
}
.card.is-pending,
.card.is-tbd {
opacity: 0.7;
}
.row {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 0.5rem;
}
h2 {
font-size: 1.15rem;
font-weight: 600;
letter-spacing: -0.01em;
}
.status {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.06em;
padding: 0.15rem 0.5rem;
border-radius: 999px;
border: 1px solid var(--border);
}
.is-live .status { color: var(--green); border-color: rgba(52, 211, 153, 0.4); }
.is-pending .status { color: var(--amber); border-color: rgba(251, 191, 36, 0.4); }
.is-tbd .status { color: var(--rose); border-color: rgba(251, 113, 133, 0.4); }
p {
color: var(--fg-dim);
font-size: 0.9rem;
line-height: 1.45;
}
.url {
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 0.78rem;
color: var(--fg-dim);
margin-top: auto;
}
</style>
+5
View File
@@ -0,0 +1,5 @@
import { createApp } from 'vue'
import App from './App.vue'
import './style.css'
createApp(App).mount('#app')
+39
View File
@@ -0,0 +1,39 @@
:root {
--bg: #0b0d14;
--bg-soft: #11141d;
--fg: #e6e8ee;
--fg-dim: #8b93a7;
--accent: #7c3aed;
--accent-2: #06b6d4;
--border: #1f2433;
--green: #34d399;
--amber: #fbbf24;
--rose: #fb7185;
font-family: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', system-ui, 'Helvetica Neue', sans-serif;
color-scheme: dark;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html, body, #app {
min-height: 100%;
}
body {
background: var(--bg);
color: var(--fg);
background-image:
radial-gradient(at 12% 8%, rgba(124, 58, 237, 0.18) 0px, transparent 45%),
radial-gradient(at 88% 92%, rgba(6, 182, 212, 0.14) 0px, transparent 45%);
background-attachment: fixed;
-webkit-font-smoothing: antialiased;
}
a {
color: inherit;
text-decoration: none;
}
+21
View File
@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "preserve",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src/**/*.ts", "src/**/*.vue", "vite-env.d.ts"]
}
+1
View File
@@ -0,0 +1 @@
{"root":["./src/apps.ts","./src/main.ts","./src/App.vue","./src/components/AppCard.vue","./vite-env.d.ts"],"version":"5.7.3"}
+7
View File
@@ -0,0 +1,7 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
+9
View File
@@ -0,0 +1,9 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
build: {
target: 'es2020',
},
})
+22
View File
@@ -0,0 +1,22 @@
# registry.famzheng.me — 反代到 gitea container registry
# Docker daemon 期望 https://<host>/v2/...gitea 内置 registry 在 gitea pod 的 /v2/ 下,
# 所以这条 ingress 不 strip 任何路径,全部 pass-through 到 gitea-svc:3000。
# 不属于 cube app #0 本身,但平台基础设施先放在 app #0 目录里,未来可以挪到独立的 platform/ ns。
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: registry
namespace: gnoc-gitea
spec:
ingressClassName: traefik
rules:
- host: registry.famzheng.me
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: gitea
port:
number: 3000
+46
View File
@@ -0,0 +1,46 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: cube
namespace: cube-cube
labels:
app: cube
spec:
replicas: 1
selector:
matchLabels:
app: cube
template:
metadata:
labels:
app: cube
spec:
imagePullSecrets:
- name: registry-creds
containers:
- name: cube
# tag 由 CI 通过 `kubectl set image` 替换;初次 apply 由 README 部署 runbook 指定
image: registry.famzheng.me/mochi/cube:latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
name: http
readinessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 1
periodSeconds: 5
livenessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 5
periodSeconds: 15
resources:
requests:
cpu: 10m
memory: 16Mi
limits:
cpu: 200m
memory: 64Mi
+18
View File
@@ -0,0 +1,18 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: cube
namespace: cube-cube
spec:
ingressClassName: traefik
rules:
- host: cube.famzheng.me
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: cube
port:
number: 80
+4
View File
@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: cube-cube
+12
View File
@@ -0,0 +1,12 @@
apiVersion: v1
kind: Service
metadata:
name: cube
namespace: cube-cube
spec:
selector:
app: cube
ports:
- name: http
port: 80
targetPort: 8080
+9
View File
@@ -0,0 +1,9 @@
//! cube.famzheng.me — 入口门户。纯静态 SPA,没有自己的 /api 路由。
#[tokio::main]
async fn main() -> std::io::Result<()> {
cube_core::init_tracing();
let dist = std::env::var("CUBE_DIST_DIR").unwrap_or_else(|_| "/dist".into());
let app = cube_core::base(dist);
cube_core::serve(app, 8080).await
}
+14
View File
@@ -0,0 +1,14 @@
[package]
name = "cube-core"
version = "0.1.0"
edition.workspace = true
license.workspace = true
authors.workspace = true
description = "Shared scaffolding for cube apps: base router, healthz, tracing, graceful shutdown"
[dependencies]
axum = { workspace = true }
tokio = { workspace = true }
tower-http = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
+58
View File
@@ -0,0 +1,58 @@
//! cube-core: 共享脚手架。所有 cube app 通过这个 crate 拿基础 router、tracing、shutdown。
use std::path::Path;
use axum::{routing::get, Router};
use tokio::net::TcpListener;
use tokio::signal;
use tower_http::services::{ServeDir, ServeFile};
use tower_http::trace::TraceLayer;
/// 拼一个带 healthz + 静态前端 SPA fallback 的基础 router。
///
/// `dist_dir` 是前端 vite build 输出目录(容器内一般是 `/dist`)。
pub fn base(dist_dir: impl AsRef<Path>) -> Router {
let dist = dist_dir.as_ref().to_path_buf();
let index = dist.join("index.html");
let static_svc = ServeDir::new(&dist).fallback(ServeFile::new(index));
Router::new()
.route("/healthz", get(healthz))
.fallback_service(static_svc)
.layer(TraceLayer::new_for_http())
}
async fn healthz() -> &'static str {
"ok"
}
/// 初始化 tracingJSON to stdout,吃 RUST_LOG env。
pub fn init_tracing() {
use tracing_subscriber::{fmt, EnvFilter};
let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
fmt().json().with_env_filter(filter).init();
}
/// 起服务,绑定 0.0.0.0:port,挂 SIGTERM/SIGINT 优雅 shutdown。
pub async fn serve(app: Router, port: u16) -> std::io::Result<()> {
let addr = format!("0.0.0.0:{port}");
let listener = TcpListener::bind(&addr).await?;
tracing::info!(%addr, "cube app listening");
axum::serve(listener, app)
.with_graceful_shutdown(shutdown_signal())
.await
}
async fn shutdown_signal() {
let ctrl_c = async { signal::ctrl_c().await.expect("install ctrl_c handler") };
let term = async {
signal::unix::signal(signal::unix::SignalKind::terminate())
.expect("install SIGTERM handler")
.recv()
.await;
};
tokio::select! {
_ = ctrl_c => tracing::info!("ctrl-c received, shutting down"),
_ = term => tracing::info!("SIGTERM received, shutting down"),
}
}