feat: 支持 Portal YAML 配置监听地址 (#74)
This commit is contained in:
@@ -2,8 +2,8 @@
|
||||
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
|
||||
wiki_page: Local-Development-and-Verification
|
||||
wiki_url: https://git.ilapage.cn/OPC/chorus/wiki/Local-Development-and-Verification.-
|
||||
wiki_revision: 7e5263ea3ca25119464e10b1afcda12e9582229c
|
||||
synchronized_at: 2026-08-28T13:26:42Z
|
||||
wiki_revision: 50810cebc9b77723c4cbcb467786f12915a09b57
|
||||
synchronized_at: 2026-08-28T15:09:16Z
|
||||
<!-- gitea-wiki-mirror:end -->
|
||||
|
||||
# 本地开发与验证
|
||||
@@ -212,6 +212,15 @@ scripts\chorus-dev.bat stop
|
||||
go run ./portal --config portal/config/settings.yml
|
||||
```
|
||||
|
||||
独立运行或 Windows 解压包可直接在 Portal settings 中设置端口:
|
||||
|
||||
```yaml
|
||||
server:
|
||||
listen_address: 127.0.0.1:8080
|
||||
```
|
||||
|
||||
环境变量 `CHORUS_LISTEN_ADDRESS` 可覆盖该值;非法 `host:port`、空 host、端口 0 或超过 65535 会在 HTTP server 启动前被拒绝。
|
||||
|
||||
浏览器验证:
|
||||
|
||||
1. 用种子用户登录,不应出现公开注册链接;
|
||||
@@ -370,7 +379,7 @@ admin_ui:
|
||||
port: 9528
|
||||
```
|
||||
|
||||
相对路径以 `local-services.yml` 所在目录为基准。Portal `environment_file` 保存数据库、Session 等敏感环境变量;`settings_file` 保存 Provider/worker 运维参数,环境变量优先覆盖 YAML。配置严格拒绝未知字段、非 loopback Host、无效/重复端口、缺失文件或错误扩展名。`chorus-local-config` 读取 Admin settings 时只投影 `settings.application.host/port`,不会输出其他配置内容。Admin Host/Port 不在统一配置重复保存,避免两个事实源漂移。
|
||||
相对路径以 `local-services.yml` 所在目录为基准。Portal `environment_file` 保存数据库、Session 等敏感环境变量;`settings_file` 保存 `server.listen_address`、Provider 和 worker 运维参数。监听地址优先级为 `CHORUS_LISTEN_ADDRESS` 环境变量 > YAML `server.listen_address` > 默认 `127.0.0.1:8080`;统一启动脚本仍以 `local-services.yml` 的 Portal Host/Port 显式覆盖 YAML,确保三实例端口只有一个运行事实源。配置严格拒绝未知字段、非 loopback Host、无效/重复端口、缺失文件或错误扩展名。`chorus-local-config` 读取 Admin settings 时只投影 `settings.application.host/port`,不会输出其他配置内容。Admin Host/Port 不在统一配置重复保存,避免两个事实源漂移。
|
||||
|
||||
从仓库根目录执行:
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
|
||||
wiki_page: Deployment-and-Operations
|
||||
wiki_url: https://git.ilapage.cn/OPC/chorus/wiki/Deployment-and-Operations.-
|
||||
wiki_revision: a55afb67eb65d59803990015ac8634af786cd4e2
|
||||
synchronized_at: 2026-08-27T01:59:02Z
|
||||
wiki_revision: fe7f8d0ca98f6c2fbf137316afe833b7f2c1f629
|
||||
synchronized_at: 2026-08-28T15:10:45Z
|
||||
<!-- gitea-wiki-mirror:end -->
|
||||
|
||||
# 部署与运维
|
||||
@@ -119,10 +119,11 @@ MVP-0 的终端用户会话保存在单个 portal 进程内存中,Cookie 只
|
||||
上传先在应用层校验,再在 generation 事务回调中保存带 owner/generation metadata 的文件。事务失败只清理本请求保存的 key;存储根目录不能作为 nginx/static 目录暴露。
|
||||
## MVP-0 worker 已实现配置与退出行为
|
||||
|
||||
portal 单二进制会同时启动 HTTP server 和内嵌 worker。启动必须显式传入 `--config <settings.yml>`。下列 Provider/worker 参数以 YAML 为基础值,同名环境变量可逐项覆盖:
|
||||
portal 单二进制会同时启动 HTTP server 和内嵌 worker。启动必须显式传入 `--config <settings.yml>`。监听地址、Provider 和 worker 参数以 YAML 为基础值,同名环境变量可逐项覆盖;监听地址未在 YAML 或环境变量中设置时默认为 `127.0.0.1:8080`:
|
||||
|
||||
| YAML 字段 | 环境覆盖变量 | 约束 |
|
||||
|---|---|---|
|
||||
| `server.listen_address` | `CHORUS_LISTEN_ADDRESS` | `host:port`;端口范围 1–65535;示例和默认值仅监听 loopback |
|
||||
| `worker.lease_seconds` | `CHORUS_WORKER_LEASE_SECONDS` | 正整数;必须大于 Provider HTTP timeout + 5 |
|
||||
| `worker.poll_milliseconds` | `CHORUS_WORKER_POLL_MILLISECONDS` | 正整数;控制空队列轮询间隔 |
|
||||
| `provider.http_timeout_seconds` | `CHORUS_PROVIDER_HTTP_TIMEOUT_SECONDS` | 正整数;安全 HTTP client 总超时和响应头超时 |
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -82,10 +83,18 @@ func loadFromLookup(lookup func(string) (string, bool), defaults runtimeSettings
|
||||
return Config{}, fmt.Errorf("CHORUS_ENV must be development, test, or production")
|
||||
}
|
||||
|
||||
listenAddress := read("CHORUS_LISTEN_ADDRESS")
|
||||
if listenAddress == "" && defaults.Server.ListenAddress != nil {
|
||||
listenAddress = strings.TrimSpace(*defaults.Server.ListenAddress)
|
||||
}
|
||||
if listenAddress == "" && defaults.Server.ListenAddress == nil {
|
||||
listenAddress = "127.0.0.1:8080"
|
||||
}
|
||||
|
||||
cfg := Config{
|
||||
Environment: environment,
|
||||
DBDSN: read("CHORUS_DSN"),
|
||||
ListenAddress: read("CHORUS_LISTEN_ADDRESS"),
|
||||
ListenAddress: listenAddress,
|
||||
StorageRoot: read("CHORUS_STORAGE_ROOT"),
|
||||
SessionKey: read("CHORUS_SESSION_KEY"),
|
||||
}
|
||||
@@ -101,8 +110,8 @@ func loadFromLookup(lookup func(string) (string, bool), defaults runtimeSettings
|
||||
cfg.SessionKey = base64.RawURLEncoding.EncodeToString(generated)
|
||||
}
|
||||
}
|
||||
if cfg.ListenAddress == "" {
|
||||
cfg.ListenAddress = "127.0.0.1:8080"
|
||||
if err := validateListenAddress(cfg.ListenAddress); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
|
||||
var missing []string
|
||||
@@ -223,6 +232,21 @@ func loadFromLookup(lookup func(string) (string, bool), defaults runtimeSettings
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func validateListenAddress(value string) error {
|
||||
host, portText, err := net.SplitHostPort(value)
|
||||
if err != nil || host == "" || strings.TrimSpace(host) != host {
|
||||
return errors.New("portal listen address must use host:port")
|
||||
}
|
||||
if portText == "" || strings.Trim(portText, "0123456789") != "" {
|
||||
return errors.New("portal listen address contains an invalid port")
|
||||
}
|
||||
port, err := strconv.Atoi(portText)
|
||||
if err != nil || port < 1 || port > 65535 {
|
||||
return errors.New("portal listen address contains an invalid port")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Config) ValidateProduction() error {
|
||||
if c.Environment != Production {
|
||||
return nil
|
||||
|
||||
@@ -10,6 +10,8 @@ import (
|
||||
|
||||
func TestLoadFileUsesYAMLAndEnvironmentOverrides(t *testing.T) {
|
||||
path := writeSettings(t, `schema_version: 1
|
||||
server:
|
||||
listen_address: 127.0.0.1:9080
|
||||
provider:
|
||||
http_timeout_seconds: 120
|
||||
max_response_bytes: 1048576
|
||||
@@ -25,12 +27,16 @@ worker:
|
||||
if cfg.ProviderHTTPTimeout != 120*time.Second || cfg.ProviderMaxResponseBytes != 1048576 || cfg.WorkerLeaseDuration != 150*time.Second || cfg.WorkerPollInterval != 500*time.Millisecond {
|
||||
t.Fatalf("YAML settings were not loaded: %#v", cfg)
|
||||
}
|
||||
if cfg.ListenAddress != "127.0.0.1:9080" {
|
||||
t.Fatalf("listen address = %q", cfg.ListenAddress)
|
||||
}
|
||||
if got := cfg.ProviderAllowedPorts; len(got) != 3 || got[2] != 8080 {
|
||||
t.Fatalf("provider ports = %v", got)
|
||||
}
|
||||
|
||||
overrides := map[string]string{
|
||||
"CHORUS_DSN": "test-dsn",
|
||||
"CHORUS_LISTEN_ADDRESS": "127.0.0.1:9081",
|
||||
"CHORUS_PROVIDER_HTTP_TIMEOUT_SECONDS": "60",
|
||||
"CHORUS_PROVIDER_MAX_RESPONSE_BYTES": "2097152",
|
||||
"CHORUS_PROVIDER_ALLOWED_PORTS": "80,443,8443",
|
||||
@@ -44,6 +50,9 @@ worker:
|
||||
if cfg.ProviderHTTPTimeout != 60*time.Second || cfg.ProviderMaxResponseBytes != 2097152 || cfg.WorkerLeaseDuration != 90*time.Second || cfg.WorkerPollInterval != 100*time.Millisecond || cfg.ProviderAllowedPorts[2] != 8443 {
|
||||
t.Fatalf("environment overrides were not applied: %#v", cfg)
|
||||
}
|
||||
if cfg.ListenAddress != "127.0.0.1:9081" {
|
||||
t.Fatalf("environment listen address override = %q", cfg.ListenAddress)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadFileRejectsInvalidSettings(t *testing.T) {
|
||||
@@ -64,6 +73,16 @@ worker: {lease_seconds: 60, poll_milliseconds: 250}
|
||||
"unsafe lease": `schema_version: 1
|
||||
provider: {http_timeout_seconds: 60, max_response_bytes: 1024, allowed_ports: [80]}
|
||||
worker: {lease_seconds: 65, poll_milliseconds: 250}
|
||||
`,
|
||||
"empty listen address": `schema_version: 1
|
||||
server: {listen_address: ""}
|
||||
provider: {http_timeout_seconds: 45, max_response_bytes: 1024, allowed_ports: [80]}
|
||||
worker: {lease_seconds: 60, poll_milliseconds: 250}
|
||||
`,
|
||||
"invalid listen address": `schema_version: 1
|
||||
server: {listen_address: "http://127.0.0.1:8080"}
|
||||
provider: {http_timeout_seconds: 45, max_response_bytes: 1024, allowed_ports: [80]}
|
||||
worker: {lease_seconds: 60, poll_milliseconds: 250}
|
||||
`,
|
||||
}
|
||||
for name, body := range tests {
|
||||
@@ -75,6 +94,17 @@ worker: {lease_seconds: 65, poll_milliseconds: 250}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadRejectsInvalidListenAddressEnvironmentOverride(t *testing.T) {
|
||||
for _, value := range []string{"127.0.0.1", ":8080", "127.0.0.1:0", "127.0.0.1:65536"} {
|
||||
_, err := LoadFromLookup(lookup(map[string]string{
|
||||
"CHORUS_DSN": "test-dsn", "CHORUS_LISTEN_ADDRESS": value,
|
||||
}))
|
||||
if err == nil || !strings.Contains(err.Error(), "listen address") {
|
||||
t.Fatalf("invalid listen address %q was accepted: %v", value, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func writeSettings(t *testing.T, body string) string {
|
||||
t.Helper()
|
||||
path := filepath.Join(t.TempDir(), "settings.yml")
|
||||
|
||||
@@ -16,10 +16,15 @@ const maxSettingsBytes = 1 << 20
|
||||
|
||||
type runtimeSettings struct {
|
||||
SchemaVersion int `yaml:"schema_version"`
|
||||
Server serverSettings `yaml:"server"`
|
||||
Provider providerSettings `yaml:"provider"`
|
||||
Worker workerSettings `yaml:"worker"`
|
||||
}
|
||||
|
||||
type serverSettings struct {
|
||||
ListenAddress *string `yaml:"listen_address"`
|
||||
}
|
||||
|
||||
type providerSettings struct {
|
||||
HTTPTimeoutSeconds int `yaml:"http_timeout_seconds"`
|
||||
MaxResponseBytes int `yaml:"max_response_bytes"`
|
||||
@@ -32,8 +37,10 @@ type workerSettings struct {
|
||||
}
|
||||
|
||||
func defaultRuntimeSettings() runtimeSettings {
|
||||
listenAddress := "127.0.0.1:8080"
|
||||
return runtimeSettings{
|
||||
SchemaVersion: 1,
|
||||
Server: serverSettings{ListenAddress: &listenAddress},
|
||||
Provider: providerSettings{
|
||||
HTTPTimeoutSeconds: 45,
|
||||
MaxResponseBytes: 32 << 20,
|
||||
@@ -73,6 +80,15 @@ func loadRuntimeSettings(path string) (runtimeSettings, error) {
|
||||
if settings.SchemaVersion != 1 {
|
||||
return runtimeSettings{}, errors.New("portal settings schema_version must be 1")
|
||||
}
|
||||
if settings.Server.ListenAddress != nil {
|
||||
listenAddress := strings.TrimSpace(*settings.Server.ListenAddress)
|
||||
if listenAddress != *settings.Server.ListenAddress {
|
||||
return runtimeSettings{}, errors.New("portal settings server.listen_address must not contain surrounding whitespace")
|
||||
}
|
||||
if err := validateListenAddress(listenAddress); err != nil {
|
||||
return runtimeSettings{}, err
|
||||
}
|
||||
}
|
||||
if settings.Provider.HTTPTimeoutSeconds <= 0 || settings.Provider.MaxResponseBytes <= 0 || settings.Worker.LeaseSeconds <= 0 || settings.Worker.PollMilliseconds <= 0 {
|
||||
return runtimeSettings{}, errors.New("portal settings values must be positive")
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
schema_version: 1
|
||||
|
||||
server:
|
||||
listen_address: 127.0.0.1:8080
|
||||
|
||||
provider:
|
||||
http_timeout_seconds: 45
|
||||
max_response_bytes: 33554432
|
||||
|
||||
Reference in New Issue
Block a user