封裝格式
Town OS 的套件是一份 YAML 定義,描述如何執行一個容器化服務, 包含它的映像檔、網路、儲存,以及面向使用者的設定問答。 套件也可以執行虛擬機器,或透過 Proton 執行 Windows 應用程式。
儲存庫結構
套件庫就是任何一個含有 packages/ 目錄的 git 儲存庫。
任何人都可以建立——替家人、朋友或客製化部署放你自己的套件。
結構很簡單:
packages/
<package-name>/
<version>.yaml
featured.json # 選用
# 範例:
packages/
nginx/
1.0.yaml
2.0.yaml
postgres/
1.0.yaml
featured.json packages/ 下的每個套件目錄都含有一個以上帶版本號的 YAML 檔。
版本以句點分段比較——數字段以數值比較,其餘則以字典序比較。儲存系統支援在版本之間升級,
也支援暫時解除安裝後再還原。Town OS 內附一個
預設套件庫,
但你可以視需要加入任意多個額外的套件庫。
套件定義
一份包含所有可用欄位的完整容器套件定義:
image:
url: nginx:1.26-alpine
description: Lightweight high-performance web server and reverse proxy
supplies: ["http"]
command: ["optional", "command", "override"]
environment:
NGINX_HOST: "@hostname@"
network:
external:
"@port@": "80"
internal:
"5432": "5432"
domains:
- "@hostname@"
volumes:
html:
mountpoint: /usr/share/nginx/html
quota: 2gb
uid: 1000
gid: 1000
data:
mountpoint: /data
archive: seed-data.tar.gz
questions:
hostname:
query: "What hostname should nginx serve?"
type: hostname
port:
query: "What external port should nginx listen on?"
type: port
default: "8080"
archives:
- image: nginx:latest
directory: /usr/share/nginx/html
volume: html
git_sources:
- url: "https://github.com/example/config.git"
branch: main
volume: data
templates:
config:
volume: data
path: config.yaml
content: |
host: {{ .Responses.hostname }}
port: {{ .Responses.port }}
version: {{ .Package.Version }}
notes:
URL:
value: "http://@hostname@:@port@"
type: url
Support:
value: "+1 (555) 123-4567"
type: phone image 欄位也接受簡寫的字串形式:image: nginx:1.26-alpine。
頂層欄位
| 欄位 | 說明 |
|---|---|
image | 必填(容器執行環境)。容器映像檔參照。可以是字串,也可以是帶 url 與選用 type 的物件。與 vm 互斥。 |
description | 這個套件的簡短易讀說明。 |
supplies | 此套件所提供能力的語意標籤清單(例如 ["database"]、["http"])。 |
command | 選用的容器指令覆寫。與 proton 互斥。 |
environment | 傳給容器的環境變數。鍵必須符合 ^[a-zA-Z_][a-zA-Z0-9_]*$。值中可包含 @variable@ 範本標記。 |
network | 連接埠對應與網域設定。 |
volumes | 具名磁碟區及其掛載設定。 |
questions | 安裝期間顯示的互動式問題。名稱必須是英數字(^[a-zA-Z0-9]+$)。 |
archives | 封存擷取規格,用於從容器映像檔預先填入磁碟區。 |
git_sources | 要複製進磁碟區的 git 儲存庫。 |
templates | 以 Go text/template 算繪、並在安裝時寫入磁碟區的檔案範本。 |
notes | 安裝後顯示的鍵值資訊。支援範本替換。 |
vm | 虛擬機器設定。與 image 及 proton 互斥。 |
proton | 透過 Proton 執行 Windows 應用程式的設定。與 vm 及 command 互斥。 |
執行環境有兩種型別:container(預設)與 vm。
套件為容器執行環境指定 image 或 proton,
為虛擬機器執行環境指定 vm。Proton 是容器執行環境的一種特化——
它底層同樣使用 Podman,但會自動產生指令,並從另一個容器映像檔擷取 Windows 應用程式的檔案。
這些欄位彼此互斥:一個套件必須剛好包含
image/proton 或 vm 其中之一。
image 欄位
image 欄位指出要執行的容器映像檔。它接受兩種寫法:
# 物件寫法
image:
url: nginx:1.26-alpine
type: oci # 選用,預設為 "oci"
# 簡寫字串寫法
image: nginx:1.26-alpine 簡短的映像檔名稱會在編譯期間自動正規化:
| 輸入 | 正規化為 |
|---|---|
nginx | docker.io/library/nginx:latest |
myuser/myapp | docker.io/myuser/myapp:latest |
ghcr.io/org/app | ghcr.io/org/app:latest |
nginx:1.26-alpine | docker.io/library/nginx:1.26-alpine |
type 唯一有效的值是 oci(預設值)。映像檔 URL 只能包含
英數字、@、.、_、:、
/ 與 -——shell 特殊字元會被拒絕。
supplies 標籤
supplies 欄位宣告套件提供了哪些能力,以便分類與篩選。
標籤是自由格式的字串——以下這些是慣用的:
| 標籤 | 用於 | 範例 |
|---|---|---|
http | 網頁伺服器、CMS 平台、網頁應用程式 | nginx、wordpress、gitea |
database | 關聯式與 NoSQL 資料庫 | postgres、mysql、mongo |
cache | 快取與鍵值儲存 | redis、memcached、valkey |
search | 搜尋與分析引擎 | elasticsearch、opensearch、solr |
messaging | 訊息代理與佇列 | rabbitmq、nats、kafka |
monitoring | 指標、警示與視覺化 | prometheus、grafana、telegraf |
storage | 物件儲存與檔案代管 | minio、registry、nextcloud |
網路設定
| 欄位 | 說明 |
|---|---|
network.external | 直接開放在主機上的連接埠對應。鍵是主機連接埠,值是容器連接埠。兩者都是 "port" 字串,並可包含 @variable@ 範本。 |
network.internal | 只能從該套件自己的網路(以及共用的 HTTP 入口)連到的連接埠對應。鍵是主機側/轉送的連接埠,值是容器連接埠。 |
network.domains | 選用清單,列出面向網際網路的額外 FQDN,HTTP 入口會為它們取得公開受信任的(Let’s Encrypt / ACME)憑證。可包含 @variable@ 範本。 |
連接埠值在範本替換之後必須是 1 到 65535 之間的整數。
用不到的話,請整個省略 external、internal 或 domains——不要寫成空的對應或清單。
連接埠項目的鍵可以是數字連接埠字串
("2222": "22"),也可以是符合
^[a-zA-Z][a-zA-Z0-9_]*$ 的語意名稱(http: "3000")。
替連接埠命名之後,上層套件就能依角色參照它(@dep_KEY_port_http@)而不必寫號碼;
而對特殊名稱 http 而言,這也表示讓該連接埠加入下面介紹的共用 HTTP 入口。
HTTP 入口(名為 http 的連接埠)
Town OS 執行著一個共用的 :443 入口,
它為每個套件終結 TLS,並反向代理到該套件的純 HTTP 容器連接埠。
套件只要把某個 internal 連接埠命名為
http,就加入了這個入口:
network:
internal:
http: "3000" # 容器的純 HTTP 連接埠 這麼做之後:
-
服務可透過
https://<PACKAGE_DNS>/連上—— 網址中不帶連接埠。沒有主機連接埠需要選;也不要再把 HTTP 連接埠 對應到external之下。 -
入口會為
<PACKAGE_DNS>(例如gitea.default.home)提供一張本地受信任的終端憑證 (由內建的 Rolodex CA 簽發),而且 Rolodex 會在_443上發布 DANETLSA記錄,讓支援 DANE 的用戶端可以釘選該憑證。 -
由於入口占用了
:443,應用程式產生的任何網址都必須是 不帶連接埠的 HTTPS。請據此設定應用程式的對外網址(例如 gitea 的GITEA__server__ROOT_URL: "https://@PACKAGE_DNS@/"), 並讓應用程式在容器內繼續以純 HTTP 監聽。
非 HTTP 的通訊協定(SSH、資料庫等)不可使用
http 這個名稱——它們不是 HTTP,入口無法為其終結 TLS。
請改用數字對應原樣轉送:
network:
internal:
http: "3000" # 由 :443 入口承接,終結 TLS
"@sshport@": "22" # 純 TCP 轉送,絕不包上 TLS 面向網際網路的名稱(domains)
預設情況下,入口只服務本地受信任的 <PACKAGE_DNS> 名稱。
若還想以公開受信任的憑證把服務開放到公開網際網路,請把真正的 FQDN 列在
network.domains 之下,並把該名稱的公開 DNS 指向這台主機。
入口會為列出的每個網域取得 ACME(Let’s Encrypt)憑證:
network:
internal:
http: "3000"
domains:
- git.example.com 磁碟區
| 欄位 | 說明 |
|---|---|
mountpoint | 必填。此磁碟區在容器內掛載的絕對路徑(必須以 / 開頭)。 |
quota | 選用的容量上限(例如 512mb、2gb、1tb)。支援 mb、gb、tb 後綴。可包含 @variable@ 範本。 |
archive | 選用的封存檔名,用來預先填入此磁碟區。 |
git | 選用的 git 儲存庫 URL,會被複製進此磁碟區。 |
uid | 選用的數字使用者 ID,用於磁碟區擁有權。 |
gid | 選用的數字群組 ID,用於磁碟區擁有權。 |
磁碟區名稱必須以英數字開頭,且只能包含英數字、句點、連字號與底線
(樣式:^[a-zA-Z0-9][a-zA-Z0-9._-]*$)。
若套件沒有磁碟區,請整個省略 volumes。
封存檔
archives 欄位會在安裝時把容器映像檔中的檔案擷取到磁碟區裡:
archives:
- image: nginx:latest
directory: /usr/share/nginx/html
volume: html | 欄位 | 說明 |
|---|---|
image | 必填。要從中擷取檔案的容器映像檔。 |
directory | 必填。容器映像檔中要擷取的絕對路徑。 |
volume | 必填。擷取目標磁碟區的名稱,必須是本套件中已定義的磁碟區。 |
若目標磁碟區在安裝或調節時是空的,Podman 會拉取映像檔、建立一個暫時容器, 並把指定的目錄複製進該磁碟區。
Git 來源
git_sources 欄位會把 git 儲存庫複製進磁碟區:
git_sources:
- url: "https://github.com/example/config.git"
branch: main
volume: config | 欄位 | 說明 |
|---|---|
url | 必填。git 儲存庫 URL(http、https 或 ssh)。可包含 @variable@ 範本。 |
branch | 要複製的分支。可包含 @variable@ 範本。 |
volume | 必填。複製目標磁碟區的名稱,必須是本套件中已定義的磁碟區。 |
問答
問答定義了套件安裝期間顯示的互動式提示。使用者的回答會取代定義中各處的
@name@ 範本標記。
questions:
port:
query: "What external port should nginx listen on?"
type: port
default: "8080" | 欄位 | 說明 |
|---|---|
query | 必填。顯示給使用者的提示文字。 |
type | 選用的驗證型別。自由文字請省略。 |
default | 選用的預設值,作為建議提供給使用者。 |
optional | 設為 true 即允許此問題留白。其餘每個問題都必須給出非空的回答。 |
oauth | type: oauth 時必填,且只在該型別下有效。安裝對話框為取得權杖所執行的裝置流程。 |
show_if | 指向同一套件中的某個 boolean 問題。在那個核取方塊被勾選之前,本問題在安裝對話框中會隱藏;未勾選時它會編譯成空字串(並免除必填檢查)— 讓套件能把一組進階選項收在一個開關之後。被參照的問題必須存在、必須是布林型別,且本身不能是條件式問題。 |
問題型別
| 型別 | 驗證內容 |
|---|---|
hostname | 以小寫字母開頭,後接小寫英數字與連字號(樣式:^[a-z][a-z0-9-]*$)。留白時自動產生 <package-name>-<4-char-hex>。 |
port | 1 到 65535 之間的整數。留白或設為 "auto" 時,會在 10000-60000 範圍內自動挑一個可用的連接埠。 |
bytes | 整數,或帶 tb、gb、mb 後綴的數值(不分大小寫) |
volume | 英數字、連字號與底線(樣式:^[a-zA-Z0-9-_]+$) |
archive | 任何非空字串 |
secret | 留白或設為 "auto" 時自動產生 64 位十六進位字串(256 位元)。也可以用明確的值覆寫。 |
duration | 整數,或帶 d、h、m、s 後綴的數值(不分大小寫)。會換算成秒。 |
boolean | true、false、t、f、1 或 0(不分大小寫)。正規化為字串 true 或 false。在安裝對話框中呈現為核取方塊;未作答的問題會取 default,若未宣告預設值則取 false。 |
oauth | 不靠打字輸入,而是從安裝對話框執行 OAuth 裝置流程所取得的權杖。呈現為一個連線按鈕;回傳的權杖就是答案。其儲存與顯示方式與 secret 相同。 |
| (省略) | 任何字串——不做驗證 |
布林問題
boolean 問題會顯示為核取方塊而非文字欄位,其答案會以字面字串
true 或 false 替換進去。由於沒勾選同樣是一個真實的答案,
帶 default: "true" 的問題也可以被使用者關掉——明確的
false 會勝過預設值。
environment:
REGISTRATION_OPEN: "@open@"
METRICS_ENABLED: "@metrics@"
questions:
open:
query: "Allow open registration?"
type: boolean # 未作答 -> "false"
metrics:
query: "Enable metrics?"
type: boolean
default: "true" # 未作答 -> "true"
檔案範本看到的也是正規化之後的值,因此
{{ .Responses.metrics }} 會算繪成 true 或
false,並且可以用
{{ if eq .Responses.metrics "true" }} 判斷。
選填問題
除非宣告了 optional: true,否則每個問題都必須給出非空的回答。
少了它,那些應用程式確實可以不要的設定——SMTP 轉送、API 金鑰——就沒有誠實的表達方式:
作者只好編一個佔位的預設值,然後祈禱操作者會把它改掉。
environment:
SMTP_HOST: "@smtp_host@"
SMTP_PORT: "@smtp_port@"
questions:
smtp_host:
query: "SMTP server hostname"
optional: true
smtp_port:
query: "SMTP server port"
type: port
optional: true
default: "587"
留白時,選填問題會在它的 @marker@ 處替換成空字串,
因此應用程式看到的是一個空的變數,而不是某個沒人挑過的值。它絕不會被自動產生:
留白的選填 secret 會保持空白,而不會變成一串隨機字串,
讓應用程式老老實實拿去嘗試驗證。
optional 可以和 type 搭配——已作答的選填連接埠仍會依連接埠驗證,
而留白的則會被編譯掉、什麼都不留。它對 boolean 沒有意義,
因為核取方塊總會落在它的兩個值之一。
OAuth 問題
有些應用程式需要一份只有其廠商才能核發的憑證來設定——Plex 帳號權杖、GitHub 個人權杖——
而慣常的取得方式是在終端機裡跑個指令稿,再把印出來的東西貼過去。oauth 問題
改為從安裝對話框執行那套流程:操作者按下連線,在瀏覽器分頁中核准,
回傳的權杖就成為答案。
這裡沒有供應商登錄表。問題自帶一個 oauth 區塊,
寫明供應商自己的各個網址,因此任何具備裝置式流程的廠商,都不必改動 Town OS 就能支援。
environment:
PLEX_TOKEN: "@plextoken@"
questions:
plextoken:
query: "Plex account"
type: oauth
oauth:
start:
method: POST
url: "https://plex.tv/api/v2/pins?strong=true"
headers:
X-Plex-Client-Identifier: "{{client_id}}"
extract:
id: id
code: code
approve: "https://app.plex.tv/auth#?clientID={{client_id}}&code={{code}}"
poll:
url: "https://plex.tv/api/v2/pins/{{id}}"
headers:
X-Plex-Client-Identifier: "{{client_id}}"
token: authToken
interval: 2s
timeout: 10m | 欄位 | 說明 |
|---|---|
start | 必填。開啟流程的請求:method(預設 GET)、url,以及選用的 headers 與 form 內容。 |
extract | 要從開始回應中取出、並提供給下列各範本使用的 JSON 欄位,寫成 name: json_field。 |
approve | 必填。操作者用來核准的網址。它會在新分頁開啟,同時也會顯示成連結,以防被彈出視窗封鎖程式擋掉。 |
user_code | 選用範本,用於操作者必須在核准頁面上輸入的短碼。GitHub 會給一組;Plex 不會。 |
poll | 必填。在核准前重複發出的請求。結構與 start 相同。 |
token | 必填。輪詢回應中存放權杖的 JSON 欄位。該欄位不存在或為 null,就是供應商在說“尚未核准”。 |
interval | 輪詢頻率。預設為 5s;請遵守供應商公告的速率限制。 |
timeout | 放棄前要持續輪詢多久。預設為 5m。 |
網址、標頭與表單值中的 {{...}} 佔位符,
會依 extract 所命名的內容解析,另外還有
{{client_id}}——這是 Town OS 為每次流程產生、
並在每一步都送出的隨機識別碼,Plex 正是把它的 pin 綁在這上面。
權杖是一份憑證,因此也就按憑證對待:在套件資訊面板中會被遮罩,可以複製但絕不列印。 它像其他回答一樣會被快取,因此重新安裝或升級時會直接重用,不必再把操作者送回供應商那邊。
由於呼叫這些網址的是系統控制器而非瀏覽器,它們必須是 https,
而且不能解析到回送、私有、連結本機或 CGNAT 位址。這項檢查會在實際建立連線時執行,
每一次重新導向也會再檢查一次,因此套件無法藉由某個流程,讓控制器伸手進主機自己的網路。
範本
templates 欄位定義了一些檔案,它們會以 Go text/template 算繪,
並在安裝期間寫入磁碟區。只有在目標檔案尚不存在時才會寫入,因此升級時能保留使用者的修改。
templates:
config:
volume: data
path: config.yaml
content: |
host: {{ .Responses.hostname }}
port: {{ .Responses.port }}
name: {{ .Package.Name }}
system: {{ .System.Hostname }} | 欄位 | 說明 |
|---|---|
volume | 必填。本套件中已定義的磁碟區名稱。 |
path | 必填。磁碟區內的相對路徑。不可以 / 開頭,也不可包含 ..。 |
content | 必填。要算繪的 Go text/template 內容。 |
範本資料脈絡
在 {{ }} 運算式中可以使用下列資料:
| 運算式 | 說明 |
|---|---|
.Responses.<name> | 使用者對指定問題的回答 |
.Package.Name | 套件名稱 |
.Package.Version | 套件版本 |
.Package.Repo | 套件庫名稱 |
.Package.Image | 編譯後的容器映像檔參照 |
.Package.Description | 套件描述 |
.System.Hostname | 系統主機名稱 |
.System.ExternalIP | 外部 IP 位址(若已知) |
.System.InternalIP | 內部/區網 IP 位址(若已知) |
範本名稱遵循與磁碟區名稱相同的規則。檔案以 0600 模式寫入,
上層目錄以 0750 模式建立。
註記
註記(notes)提供安裝後顯示的鍵值資訊。
notes:
URL:
value: "http://localhost:@port@"
type: url
Info:
value: "Default admin credentials are admin/admin" | 欄位 | 說明 |
|---|---|
value | 必填。註記文字。支援 @variable@ 範本替換。 |
type | 選用的驗證型別:url、phone 或 email。純文字請省略。 |
相依關係
套件可以宣告對其他套件的相依關係。相依套件會共用上層套件的 podman 網路, 讓同一相依樹中的容器能透過 podman 內建的 DNS,直接以容器名稱通訊。
dependencies:
db:
package: postgres
responses:
password: "@dbpass@"
user: "mattermost"
database: "mattermost"
port: "5432" | 欄位 | 說明 |
|---|---|
package | 必填。要安裝的相依套件名稱。 |
repo | 含有該相依套件的套件庫。預設與上層套件所在的套件庫相同。 |
version | 要安裝的版本。預設為可用的最新版本。 |
responses | 給該相依套件的問題回答。值支援來自上層套件問題的 @variable@ 語法。 |
上層套件在執行階段會為每個相依套件取得對應的環境變數:
TOWNOS_DEP_{KEY}_HOST(容器名稱)與
TOWNOS_DEP_{KEY}_PORT_{port}(容器側連接埠號)。
上層套件也可以在自己的環境變數值中使用 @dep_KEY_host@ 與
@dep_KEY_port_N@ 範本變數(見範本系統)。
範例:一個對 PostgreSQL 宣告了 db 相依關係的 Mattermost 套件,
可以在它的資料來源網址中參照資料庫主機:
environment:
MM_SQLSETTINGS_DATASOURCE: "postgres://mattermost:@dbpass@@@dep_db_host@:@dep_db_port_5432@/mattermost?sslmode=disable" 範本系統
Town OS 有兩套範本系統,分別作用在編譯的不同階段。
@variable@ 替換
@variable@ 語法會在套件編譯期間被替換,作用於所有可設定的欄位:
環境變數值、網路連接埠對應、網路網域、磁碟區掛載點、磁碟區配額、磁碟區的 git URL、
git 來源的 URL 與分支、範本的 volume 與 path 欄位、虛擬機器的映像檔與記憶體、
Proton 設定,以及註記值。
每個問題名稱都會成為一個變數。若要寫出字面的 @ 字元
(例如 git SSH 網址 git@@domain@),請使用 @@——
兩個連續的 @ 會產生一個字面的 @。
無法解析的變數(參照到沒有對應問題回答的名稱)會原樣保留在輸出中。
內建變數
| 變數 | 說明 |
|---|---|
@LOCAL_EXTERNAL_HOST@ | Town OS 主機的外部主機名稱或 IP |
@LOCAL_INTERNAL_HOST@ | Town OS 主機的內部主機名稱或 IP |
@PACKAGE_DNS@ | 此套件在內部網路上被指派的 DNS 名稱 |
@dep_KEY_host@ | 相依項目 KEY 的容器主機名稱(可透過共用網路上的 podman DNS 解析)。僅在套件宣告了相依關係時可用。 |
@dep_KEY_port_N@ | 相依項目 KEY 的容器連接埠 N。僅在套件宣告了相依關係時可用。 |
內建變數會先於使用者問題的回答被替換,因此優先權較高。
相依範本變數(@dep_*@)在相依套件安裝完成後解析,並套用到上層套件的環境變數值。
其中 KEY 是小寫的相依鍵名,N 則是容器連接埠號。
Go 範本(templates 欄位中)
templates 欄位使用 Go text/template 語法
({{ .Responses.name }})來算繪寫入磁碟區的檔案。
可用的資料脈絡見範本一節。
虛擬機器執行環境
套件可以執行虛擬機器而非容器,只要指定 vm 欄位來取代
image:
vm:
image: "https://example.com/my-vm.qcow2"
memory: 2gb
cpus: 2
description: A virtual machine package | 欄位 | 說明 |
|---|---|
image | 必填。虛擬機器磁碟映像檔的網址(http/https)或檔名。可包含 @variable@ 範本。 |
memory | 以位元組後綴表示的記憶體配置(例如 1gb、512mb)。預設為 1gb。可包含 @variable@ 範本。 |
cpus | 虛擬 CPU 數量。預設為 1。必須為非負數。 |
vm 欄位與 image 及 proton 互斥。
虛擬機器映像檔不會像容器映像檔那樣被正規化。
Proton 執行環境
套件可以透過指定 proton 欄位,以 Proton 執行 Windows 應用程式:
image:
type: oci
proton:
app_image: "mycompany/windows-app:1.0"
app_directory: /app
volume: app
exe: /app/myapp.exe
args: ["-fullscreen", "-config", "/app/config.ini"]
volumes:
app:
mountpoint: /app | 欄位 | 說明 |
|---|---|
app_image | 必填。含有該 Windows 應用程式的容器映像檔。 |
app_directory | 必填。應用程式所在的絕對路徑。 |
volume | 必填。本套件中已定義的磁碟區名稱。 |
exe | 必填。Windows 執行檔的路徑。 |
args | 選用的命令列引數清單。 |
設定了 proton 之後,容器指令會自動產生為
["proton", "run", <exe>, ...<args>]。容器映像檔預設取用
全系統的 proton_image 設定
(quay.io/town/proton:latest),可透過設定 image
在套件層級覆寫。app_image 欄位在編譯期間會依與一般容器映像檔參照
相同的規則正規化。proton 欄位與 vm 及
command 互斥。
編譯
編譯會把套件定義與使用者的回答,轉換成一份完全解析、可直接執行的設定。 編譯管線包含下列步驟:
- 依各問題所宣告的型別驗證全部回答
- 套用型別專屬的驗證(連接埠範圍、主機名稱樣式、位元組剖析等)
- 把所有
@variable@範本標記替換成解析後的值 - 把容器映像檔網址正規化為完整限定的參照
- 產出一份可供安裝、已解析的套件
對虛擬機器套件而言,記憶體字串(例如 2gb)會被剖析成位元組數,
並套用 CPU 的預設值。各欄位的驗證錯誤會一併蒐集後一起回傳,
因此使用者可以一次改完,而不必一個一個碰壁。
回答的保存
回答依版本保存於
responses/<repo>/<pkg>/<version>.json。
同時也會在 responses/last/<repo>/<pkg>.json
存一份 last 副本,供升級以及從已解除安裝的磁碟區重新安裝時重用。
安裝成功後,last 回答會被清除。這表示如果一個套件被解除安裝、之後又重新安裝, 先前的答案會作為預設值提供給你。一旦新的安裝成功,快取的副本就會被移除。
安裝流程
安裝一個套件會走過一連串既定的步驟。 整個流程涵蓋從檔案準備到服務啟動的一切:
- 從套件庫中的套件檔案到已安裝目錄建立硬連結
- 保存回答(版本專屬的以及
last副本) - 建立磁碟區,套用配額與選用的 UID/GID 擁有權
- 從封存檔與 git 來源填入磁碟區(僅限容器執行環境)
- 套用範本(把檔案算繪進磁碟區)
- 產生 systemd unit(容器用 Podman,虛擬機器用 QEMU)
- 建立網路狀態檔
- 啟動服務
- 成功後清除 last 回答
有兩個選用旗標可控制磁碟區的行為:
reuse_volumes——重用同一套件先前已解除安裝版本的磁碟區import_from_version——從指定的舊版本匯入磁碟區
解除安裝
解除安裝套件時,預設會保留磁碟區中的資料。
磁碟區只是從 installed/ 前綴搬到 uninstalled/ 前綴,
而不是被刪除。這樣重新安裝時,原有的資料仍然完整。
解除安裝流程也會移除網路狀態檔,並停止、停用並解除安裝相關的 systemd unit。
若想立即刪除磁碟區而不是保留它們,請使用 purge_volumes 旗標。
被清除的磁碟區無法復原。
安裝預覽
在真正開始安裝之前,你可以先預覽會建立哪些東西。
安裝預覽端點(POST /packages/install-preview)會回傳一份預定安裝的摘要,
而且不做任何變更:
- 將會建立的磁碟區
- 將會對應的連接埠
- 升級資訊(若是從舊版本升級)
- 執行環境型別(容器或虛擬機器)
- 該套件是否有需要回答的問題
- 虛擬機器套件的虛擬機器設定細節(映像檔、記憶體、CPU 數)
精選套件
套件庫可以在根目錄(與 packages/ 目錄並列)放一個
featured.json 檔案,用來在介面上突顯特定套件。
該檔案內含一個由套件名稱字串組成的 JSON 陣列——不帶版本或套件庫前綴:
["wordpress", "nextcloud", "postgres"]
列在 featured.json 中的套件,會在 API 的套件清單回應中帶上
featured: true,讓介面據此加以突顯。這個檔案是選用的——
若不存在,就沒有任何套件被標為精選。
匯入套件庫
建立好套件庫之後,可以透過網頁介面或直接調整檔案系統設定, 把它加進你的 Town OS 執行個體。
透過介面
登入 Town OS 儀表板,前往 套件,選擇 套件庫 分頁,按下 新增套件庫。填入名稱、 git 網址與選用的憑證。按 重新整理 可立即拉取套件中繼資料。
透過檔案系統
編輯 btrfs 套件資料目錄中的 repositories.json。這個檔案是一個
由物件組成的 JSON 陣列:
[
{"name": "default", "url": "https://github.com/town-os/default-packages"},
{"name": "my-packages", "url": "https://github.com/myuser/my-packages"}
] 順序是有意義的——當套件名稱衝突時,排在後面的會覆蓋前面的。 私人套件庫請把憑證嵌在網址中。變更需要重新啟動服務,或在介面上按 重新整理 才會生效。
驗證規則
套件驗證與編譯期間會強制執行以下限制:
| 欄位 | 規則 |
|---|---|
| 映像檔 URL | 非空;字元必須符合 ^[a-zA-Z0-9@][a-zA-Z0-9._:/@-]*$ |
| 映像檔 type | 留空(預設為 oci)或 oci |
| 環境變數鍵 | 必須符合 ^[a-zA-Z_][a-zA-Z0-9_]*$(POSIX 慣例) |
| 問題名稱 | 必須符合 ^[a-zA-Z0-9]+$ |
| 磁碟區名稱 | 必須符合 ^[a-zA-Z0-9][a-zA-Z0-9._-]*$ |
| 掛載點 | 必須以 / 開頭 |
| 範本名稱 | 與磁碟區名稱規則相同 |
| 範本路徑 | 非空、相對路徑(不帶開頭的 /)、不含 .. 穿越 |
| 封存目錄 | 必須是絕對路徑 |
| 封存/git 的 volume | 必須參照該套件中已定義的磁碟區 |
| Git URL | 必須有有效的協定與主機(file:// 除外) |
| 虛擬機器 CPU 數 | 必須為非負數 |
| Proton 的 app_directory | 必須是絕對路徑 |
| 執行環境 | image/proton(容器)與 vm 之中剛好取其一 |
驗證發生在範本替換之前。含有 @variable@ 標記的欄位,
會跳過路徑與網址驗證,直到編譯把範本解析完之後再檢查。
風格指引
- 省略空的對應(
environment:、internal:、volumes:)——乾脆整個不寫。 - 接受自由文字的問題請省略
type——不要寫一個沒有值的type:。 - 寫上
description,簡短說明這個套件是什麼。 - 當套件提供的是眾所周知的服務時,寫上帶有相應能力標籤的
supplies。 - 寫上
notes,包含連線網址以及任何重要的安裝後資訊。 - 用
@variable@範本,讓使用者在安裝時自訂連接埠、主機名稱與憑證。 - 需要 Go 範本邏輯的設定檔請使用
templates——它們只會寫入一次,並保留使用者的修改。