收紧 Go Agent 安装入口配置
This commit is contained in:
parent
70a2a20006
commit
2e35feb1d8
@ -2,12 +2,14 @@
|
||||
|
||||
`flnxi-agent-go` 是面向私有化部署场景的 Go 版安装与运维 Agent。
|
||||
|
||||
当前定位:Go 版 Agent 更适合作为后续私有化交付的轻量入口;但当前默认安装脚本还没把认证变量和完整版本链路全部接好,不能直接当成“已经可正式交付”的成品。若现场仍使用 Node 版 Agent,先确认既有安装链路和端口占用,再决定是否切换。
|
||||
当前定位:Go 版 Agent 更适合作为后续私有化交付的轻量入口;它已经收紧了 Agent 自身认证、主应用固定版本镜像和许可证运行参数,但管理界面、安装向导、异常恢复和客户现场验收流程仍未完全闭环,不能直接当成“已经可正式交付”的成品。若现场仍使用 Node 版 Agent,先确认既有安装链路和端口占用,再决定是否切换。
|
||||
|
||||
重要现状:
|
||||
|
||||
- `install.sh` 已不再默认拉取旧 Harbor `latest` 镜像,必须通过 `AGENT_IMAGE` 或 `--image` 显式传入完整 Agent 版本镜像。
|
||||
- Go 版 Agent 内部安装和更新素材系统已改为读取完整版本镜像,不再默认拉取旧 Harbor / `latest`。
|
||||
- 安装素材系统时必须提供完整主应用版本镜像,不能使用无版本 tag 或 `latest`。
|
||||
- 安装素材系统时必须提供许可证服务参数;缺少关键参数时会在拉镜像前失败,避免客户装出一个启动不了的半成品。
|
||||
- 版本列表需要通过 `FLNXI_VERSION_LIST_URL` 显式配置来源;未配置时不会伪造 `latest` 或默认版本。
|
||||
- 当前仍建议先作为内部验证入口,等管理界面和客户安装流程联调完成后,再提升为客户正式安装主入口。
|
||||
|
||||
@ -61,17 +63,26 @@ docker run -d \
|
||||
| `FLNXI_POSTGRES_IMAGE` | 安装时使用的 PostgreSQL 镜像;也兼容读取 `POSTGRES_IMAGE` | `postgres:15-alpine` |
|
||||
| `FLNXI_MINIO_IMAGE` | 安装时使用的 MinIO 镜像;也兼容读取 `MINIO_IMAGE` | `minio/minio:RELEASE.2025-04-22T22-12-26Z` |
|
||||
| `FLNXI_VERSION_LIST_URL` | Agent 管理界面可选版本列表来源,返回 Registry tags 结构 | 未设置 |
|
||||
| `LICENSE_SERVER_URL` | 主应用许可证服务地址,安装素材系统时会写入主应用容器 | 未设置 |
|
||||
| `LICENSE_PROJECT_CODE` | 主应用许可证项目编码,安装素材系统时会写入主应用容器 | 未设置 |
|
||||
| `LICENSE_REQUEST_SIGNING_SECRET` | 主应用请求许可证服务的签名密钥,至少 32 字符 | 未设置 |
|
||||
| `LICENSE_PUBLIC_KEY` | 主应用验签用许可证公钥 | 未设置 |
|
||||
| `LOCAL_ENCRYPTION_KEY` | 主应用本地许可证缓存加密密钥,至少 32 字符;未传入时 Agent 会为本次安装生成 | 自动生成 |
|
||||
|
||||
版本说明:
|
||||
|
||||
- 当前 Go 版 Agent 的版本列表不再写死 Harbor;后续正式交付前,需要决定版本列表来自 Registry tags、许可证服务,还是发布管理接口。
|
||||
- 它还没有像 Node 版 Agent 那样完整接入 `LICENSE_SERVER_URL` / `LICENSE_PROJECT_CODE` 的远端版本检查链路。
|
||||
- Go 版 Agent 目前会把主应用启动必需的许可证配置写入容器,但还没有把“许可证服务可安装版本、灰度版本、客户授权版本范围”做成最终客户安装向导。
|
||||
|
||||
安装素材系统前,至少要给 Agent 容器配置:
|
||||
|
||||
```bash
|
||||
-e FLNXI_MATERIAL_IMAGE=registry.example.com/flnxi/material/flnxi-material:1.0.1
|
||||
-e FLNXI_MATERIAL_IMAGE_REPOSITORY=registry.example.com/flnxi/material/flnxi-material
|
||||
-e LICENSE_SERVER_URL=https://license.example.com
|
||||
-e LICENSE_PROJECT_CODE=flnxi-sucai
|
||||
-e LICENSE_REQUEST_SIGNING_SECRET='<at-least-32-characters>'
|
||||
-e LICENSE_PUBLIC_KEY='<license-public-key>'
|
||||
```
|
||||
|
||||
## 安全建议
|
||||
|
||||
23
flnxi-agent-go/__tests__/install-script.test.ts
Normal file
23
flnxi-agent-go/__tests__/install-script.test.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
describe('flnxi-agent-go/install.sh', () => {
|
||||
const script = fs.readFileSync(
|
||||
path.join(process.cwd(), 'flnxi-agent-go', 'install.sh'),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
it('不会把 Agent 镜像当作素材系统镜像兜底', () => {
|
||||
expect(script).toContain('FLNXI_MATERIAL_IMAGE=${FLNXI_MATERIAL_IMAGE:-}')
|
||||
expect(script).not.toContain('FLNXI_MATERIAL_IMAGE=${FLNXI_MATERIAL_IMAGE:-$AGENT_IMAGE}')
|
||||
})
|
||||
|
||||
it('会把素材系统安装必需的许可证配置透传给 Agent 容器', () => {
|
||||
expect(script).toContain('LICENSE_SERVER_URL=${LICENSE_SERVER_URL:-}')
|
||||
expect(script).toContain('LICENSE_PROJECT_CODE=${LICENSE_PROJECT_CODE:-}')
|
||||
expect(script).toContain('LICENSE_REQUEST_SIGNING_SECRET=${LICENSE_REQUEST_SIGNING_SECRET:-}')
|
||||
expect(script).toContain('LICENSE_PUBLIC_KEY=${LICENSE_PUBLIC_KEY:-}')
|
||||
expect(script).toContain('LOCAL_ENCRYPTION_KEY=${LOCAL_ENCRYPTION_KEY:-}')
|
||||
expect(script).toContain('未完整配置许可证服务参数')
|
||||
})
|
||||
})
|
||||
@ -219,7 +219,7 @@ run_agent() {
|
||||
)
|
||||
|
||||
run_args+=(
|
||||
-e "FLNXI_MATERIAL_IMAGE=${FLNXI_MATERIAL_IMAGE:-$AGENT_IMAGE}"
|
||||
-e "FLNXI_MATERIAL_IMAGE=${FLNXI_MATERIAL_IMAGE:-}"
|
||||
-e "APP_IMAGE=${APP_IMAGE:-}"
|
||||
-e "FLNXI_MATERIAL_IMAGE_REPOSITORY=${FLNXI_MATERIAL_IMAGE_REPOSITORY:-}"
|
||||
-e "FLNXI_POSTGRES_IMAGE=${FLNXI_POSTGRES_IMAGE:-}"
|
||||
@ -227,8 +227,21 @@ run_agent() {
|
||||
-e "FLNXI_MINIO_IMAGE=${FLNXI_MINIO_IMAGE:-}"
|
||||
-e "MINIO_IMAGE=${MINIO_IMAGE:-}"
|
||||
-e "FLNXI_VERSION_LIST_URL=${FLNXI_VERSION_LIST_URL:-}"
|
||||
-e "LICENSE_SERVER_URL=${LICENSE_SERVER_URL:-}"
|
||||
-e "LICENSE_PROJECT_CODE=${LICENSE_PROJECT_CODE:-}"
|
||||
-e "LICENSE_REQUEST_SIGNING_SECRET=${LICENSE_REQUEST_SIGNING_SECRET:-}"
|
||||
-e "LICENSE_PUBLIC_KEY=${LICENSE_PUBLIC_KEY:-}"
|
||||
-e "LOCAL_ENCRYPTION_KEY=${LOCAL_ENCRYPTION_KEY:-}"
|
||||
)
|
||||
|
||||
if [ -z "${FLNXI_MATERIAL_IMAGE:-${APP_IMAGE:-}}" ]; then
|
||||
print_warning "未配置 FLNXI_MATERIAL_IMAGE / APP_IMAGE,Agent 可启动,但安装素材系统前必须补充完整主应用版本镜像"
|
||||
fi
|
||||
|
||||
if [ -z "${LICENSE_SERVER_URL:-}" ] || [ -z "${LICENSE_PROJECT_CODE:-}" ] || [ -z "${LICENSE_REQUEST_SIGNING_SECRET:-}" ] || [ -z "${LICENSE_PUBLIC_KEY:-}" ]; then
|
||||
print_warning "未完整配置许可证服务参数,Agent 安装素材系统时会先拦截并提示补齐"
|
||||
fi
|
||||
|
||||
if [ -n "$AGENT_AUTH_USERNAME" ] && [ -n "$AGENT_AUTH_PASSWORD" ]; then
|
||||
run_args+=(
|
||||
-e "AGENT_AUTH_USERNAME=$AGENT_AUTH_USERNAME"
|
||||
|
||||
@ -112,12 +112,82 @@ func resolveInstallImages(req InstallRequest) (string, string, string, error) {
|
||||
if materialImage == "" {
|
||||
return "", "", "", fmt.Errorf("%s or %s is required", envMaterialImage, envMaterialImageCompat)
|
||||
}
|
||||
if err := validateFixedImageTag(materialImage); err != nil {
|
||||
return "", "", "", fmt.Errorf("material image: %w", err)
|
||||
}
|
||||
|
||||
postgresImage := firstNonEmpty(req.PostgresImage, os.Getenv(envPostgresImage), os.Getenv(envPostgresImageCompat), "postgres:15-alpine")
|
||||
minioImage := firstNonEmpty(req.MinioImage, os.Getenv(envMinioImage), os.Getenv(envMinioImageCompat), "minio/minio:RELEASE.2025-04-22T22-12-26Z")
|
||||
return materialImage, postgresImage, minioImage, nil
|
||||
}
|
||||
|
||||
type InstallLicenseConfig struct {
|
||||
ServerURL string
|
||||
ProjectCode string
|
||||
RequestSigningSecret string
|
||||
PublicKey string
|
||||
LocalEncryptionKey string
|
||||
}
|
||||
|
||||
func resolveInstallLicenseConfig(req InstallRequest, secrets InstallSecrets) (InstallLicenseConfig, error) {
|
||||
config := InstallLicenseConfig{
|
||||
ServerURL: firstNonEmpty(req.LicenseServerURL, os.Getenv(envLicenseServerURL)),
|
||||
ProjectCode: firstNonEmpty(req.LicenseProjectCode, os.Getenv(envLicenseProjectCode)),
|
||||
RequestSigningSecret: firstNonEmpty(req.LicenseRequestSigningSecret, os.Getenv(envLicenseRequestSigningSecret)),
|
||||
PublicKey: firstNonEmpty(req.LicensePublicKey, os.Getenv(envLicensePublicKey)),
|
||||
LocalEncryptionKey: firstNonEmpty(req.LocalEncryptionKey, os.Getenv(envLocalEncryptionKey), secrets.LocalEncryptionKey),
|
||||
}
|
||||
|
||||
missing := make([]string, 0)
|
||||
if config.ServerURL == "" {
|
||||
missing = append(missing, envLicenseServerURL)
|
||||
}
|
||||
if config.ProjectCode == "" {
|
||||
missing = append(missing, envLicenseProjectCode)
|
||||
}
|
||||
if len(config.RequestSigningSecret) < 32 {
|
||||
missing = append(missing, envLicenseRequestSigningSecret+"(至少32字符)")
|
||||
}
|
||||
if config.PublicKey == "" {
|
||||
missing = append(missing, envLicensePublicKey)
|
||||
}
|
||||
if len(config.LocalEncryptionKey) < 32 {
|
||||
missing = append(missing, envLocalEncryptionKey+"(至少32字符)")
|
||||
}
|
||||
if len(missing) > 0 {
|
||||
return InstallLicenseConfig{}, fmt.Errorf("missing material license config: %s", strings.Join(missing, ", "))
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func materialAppEnv(req InstallRequest, secrets InstallSecrets, licenseConfig InstallLicenseConfig, localIP string, webPort int) []string {
|
||||
return []string{
|
||||
"LICENSE_KEY=" + req.LicenseKey,
|
||||
"LICENSE_SERVER_URL=" + licenseConfig.ServerURL,
|
||||
"LICENSE_PROJECT_CODE=" + licenseConfig.ProjectCode,
|
||||
"LICENSE_REQUEST_SIGNING_SECRET=" + licenseConfig.RequestSigningSecret,
|
||||
"LICENSE_PUBLIC_KEY=" + licenseConfig.PublicKey,
|
||||
"LOCAL_ENCRYPTION_KEY=" + licenseConfig.LocalEncryptionKey,
|
||||
"DATABASE_URL=postgresql://" + secrets.DbUser + ":" + secrets.DbPassword + "@flnxi-db:5432/" + secrets.DbName,
|
||||
// S3/MinIO 配置(内部访问使用 Docker 网络)
|
||||
"S3_ENDPOINT=http://flnxi-minio:9000",
|
||||
"S3_ACCESS_KEY_ID=" + secrets.MinioRootUser,
|
||||
"S3_SECRET_ACCESS_KEY=" + secrets.MinioRootPassword,
|
||||
"S3_BUCKET=" + secrets.S3Bucket,
|
||||
"S3_REGION=us-east-1",
|
||||
"S3_ENABLE_PATH_STYLE=1",
|
||||
// S3 公开访问配置(浏览器访问使用宿主机 IP)
|
||||
"S3_PUBLIC_DOMAIN=http://" + localIP + ":9000/" + secrets.S3Bucket,
|
||||
"SERVER_IP=" + localIP,
|
||||
"MINIO_API_PORT=9000",
|
||||
// NextAuth 配置
|
||||
"NEXTAUTH_SECRET=" + secrets.NextAuthSecret,
|
||||
"NEXTAUTH_URL=http://" + localIP + ":" + fmt.Sprintf("%d", webPort),
|
||||
"APP_PUBLIC_URL=http://" + localIP + ":" + fmt.Sprintf("%d", webPort),
|
||||
}
|
||||
}
|
||||
|
||||
func resolveUpdateImage(req UpdateRequest) (string, error) {
|
||||
if image := firstNonEmpty(req.Image); image != "" {
|
||||
if err := validateFixedImageTag(image); err != nil {
|
||||
@ -173,14 +243,19 @@ func validateFixedImageTag(image string) error {
|
||||
}
|
||||
|
||||
const (
|
||||
envMaterialImage = "FLNXI_MATERIAL_IMAGE"
|
||||
envMaterialImageCompat = "APP_IMAGE"
|
||||
envMaterialImageRepository = "FLNXI_MATERIAL_IMAGE_REPOSITORY"
|
||||
envPostgresImage = "FLNXI_POSTGRES_IMAGE"
|
||||
envPostgresImageCompat = "POSTGRES_IMAGE"
|
||||
envMinioImage = "FLNXI_MINIO_IMAGE"
|
||||
envMinioImageCompat = "MINIO_IMAGE"
|
||||
envVersionListURL = "FLNXI_VERSION_LIST_URL"
|
||||
envMaterialImage = "FLNXI_MATERIAL_IMAGE"
|
||||
envMaterialImageCompat = "APP_IMAGE"
|
||||
envMaterialImageRepository = "FLNXI_MATERIAL_IMAGE_REPOSITORY"
|
||||
envPostgresImage = "FLNXI_POSTGRES_IMAGE"
|
||||
envPostgresImageCompat = "POSTGRES_IMAGE"
|
||||
envMinioImage = "FLNXI_MINIO_IMAGE"
|
||||
envMinioImageCompat = "MINIO_IMAGE"
|
||||
envVersionListURL = "FLNXI_VERSION_LIST_URL"
|
||||
envLicenseServerURL = "LICENSE_SERVER_URL"
|
||||
envLicenseProjectCode = "LICENSE_PROJECT_CODE"
|
||||
envLicenseRequestSigningSecret = "LICENSE_REQUEST_SIGNING_SECRET"
|
||||
envLicensePublicKey = "LICENSE_PUBLIC_KEY"
|
||||
envLocalEncryptionKey = "LOCAL_ENCRYPTION_KEY"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -206,13 +281,18 @@ type UpdateStatus struct {
|
||||
}
|
||||
|
||||
type InstallRequest struct {
|
||||
LicenseKey string `json:"licenseKey"`
|
||||
DataPath string `json:"dataPath"`
|
||||
WebPort int `json:"webPort"`
|
||||
AuthToken string `json:"authToken,omitempty"`
|
||||
MaterialImage string `json:"materialImage,omitempty"`
|
||||
PostgresImage string `json:"postgresImage,omitempty"`
|
||||
MinioImage string `json:"minioImage,omitempty"`
|
||||
LicenseKey string `json:"licenseKey"`
|
||||
DataPath string `json:"dataPath"`
|
||||
WebPort int `json:"webPort"`
|
||||
AuthToken string `json:"authToken,omitempty"`
|
||||
MaterialImage string `json:"materialImage,omitempty"`
|
||||
PostgresImage string `json:"postgresImage,omitempty"`
|
||||
MinioImage string `json:"minioImage,omitempty"`
|
||||
LicenseServerURL string `json:"licenseServerUrl,omitempty"`
|
||||
LicenseProjectCode string `json:"licenseProjectCode,omitempty"`
|
||||
LicenseRequestSigningSecret string `json:"licenseRequestSigningSecret,omitempty"`
|
||||
LicensePublicKey string `json:"licensePublicKey,omitempty"`
|
||||
LocalEncryptionKey string `json:"localEncryptionKey,omitempty"`
|
||||
}
|
||||
|
||||
type UpdateRequest struct {
|
||||
@ -272,7 +352,14 @@ func GetInstallStatus(dc *docker.Client) gin.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func runInstall(dc *docker.Client, req InstallRequest) {
|
||||
type installDockerClient interface {
|
||||
PullImageWithProgress(ctx context.Context, image string, authStr string, onProgress func(current, total int64, status string)) error
|
||||
ExecOnHost(ctx context.Context, cmd string) error
|
||||
CreateNetworkIfNotExists(ctx context.Context, name string) error
|
||||
CreateAndStartContainer(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, name string) (string, error)
|
||||
}
|
||||
|
||||
func runInstall(dc installDockerClient, req InstallRequest) {
|
||||
ctx := context.Background()
|
||||
setStep := func(step string, progress int, msg string) {
|
||||
statusMutex.Lock()
|
||||
@ -302,6 +389,12 @@ func runInstall(dc *docker.Client, req InstallRequest) {
|
||||
return
|
||||
}
|
||||
|
||||
licenseConfig, err := resolveInstallLicenseConfig(req, secrets)
|
||||
if err != nil {
|
||||
setErr("许可证配置缺失: " + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 拉取素材系统镜像 (0-30%)
|
||||
setStep("拉取镜像", 1, "正在拉取素材系统镜像...")
|
||||
err = dc.PullImageWithProgress(ctx, materialImage, authStr, func(current, total int64, status string) {
|
||||
@ -421,27 +514,9 @@ func runInstall(dc *docker.Client, req InstallRequest) {
|
||||
// 启动素材系统
|
||||
setStep("启动服务", 90, "启动素材系统...")
|
||||
cfg := &container.Config{
|
||||
Image: materialImage,
|
||||
User: "0", // 以 root 用户运行,确保有权限写入挂载目录
|
||||
Env: []string{
|
||||
"LICENSE_KEY=" + req.LicenseKey,
|
||||
"DATABASE_URL=postgresql://" + secrets.DbUser + ":" + secrets.DbPassword + "@flnxi-db:5432/" + secrets.DbName,
|
||||
// S3/MinIO 配置(内部访问使用 Docker 网络)
|
||||
"S3_ENDPOINT=http://flnxi-minio:9000",
|
||||
"S3_ACCESS_KEY_ID=" + secrets.MinioRootUser,
|
||||
"S3_SECRET_ACCESS_KEY=" + secrets.MinioRootPassword,
|
||||
"S3_BUCKET=" + secrets.S3Bucket,
|
||||
"S3_REGION=us-east-1",
|
||||
"S3_ENABLE_PATH_STYLE=1",
|
||||
// S3 公开访问配置(浏览器访问使用宿主机 IP)
|
||||
"S3_PUBLIC_DOMAIN=http://" + localIP + ":9000/" + secrets.S3Bucket,
|
||||
"SERVER_IP=" + localIP,
|
||||
"MINIO_API_PORT=9000",
|
||||
// NextAuth 配置
|
||||
"NEXTAUTH_SECRET=" + secrets.NextAuthSecret,
|
||||
"NEXTAUTH_URL=http://" + localIP + ":" + fmt.Sprintf("%d", webPort),
|
||||
"APP_PUBLIC_URL=http://" + localIP + ":" + fmt.Sprintf("%d", webPort),
|
||||
},
|
||||
Image: materialImage,
|
||||
User: "0", // 以 root 用户运行,确保有权限写入挂载目录
|
||||
Env: materialAppEnv(req, secrets, licenseConfig, localIP, webPort),
|
||||
ExposedPorts: nat.PortSet{"3000/tcp": struct{}{}},
|
||||
}
|
||||
hcfg := &container.HostConfig{
|
||||
|
||||
@ -54,6 +54,121 @@ func TestResolveUpdateImageRejectsLatestVersion(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveInstallImagesRequiresFixedMaterialTag(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
image string
|
||||
wantErr string
|
||||
}{
|
||||
{name: "missing", image: "", wantErr: envMaterialImage},
|
||||
{name: "no tag", image: "registry.local:5000/material/flnxi-material", wantErr: "fixed version tag"},
|
||||
{name: "latest", image: "registry.local:5000/material/flnxi-material:latest", wantErr: "latest is not allowed"},
|
||||
{name: "fixed tag", image: "registry.local:5000/material/flnxi-material:1.0.1"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, _, _, err := resolveInstallImages(InstallRequest{MaterialImage: tt.image})
|
||||
if tt.wantErr != "" {
|
||||
if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
|
||||
t.Fatalf("resolveInstallImages() error = %v, want contains %q", err, tt.wantErr)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("resolveInstallImages() unexpected error: %v", err)
|
||||
}
|
||||
if got != tt.image {
|
||||
t.Fatalf("resolveInstallImages() material = %q, want %q", got, tt.image)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveInstallLicenseConfigRequiresMaterialRuntimeSecrets(t *testing.T) {
|
||||
secrets := InstallSecrets{LocalEncryptionKey: strings.Repeat("b", 32)}
|
||||
|
||||
_, err := resolveInstallLicenseConfig(InstallRequest{}, secrets)
|
||||
if err == nil {
|
||||
t.Fatal("resolveInstallLicenseConfig() expected error")
|
||||
}
|
||||
for _, want := range []string{
|
||||
envLicenseServerURL,
|
||||
envLicenseProjectCode,
|
||||
envLicenseRequestSigningSecret,
|
||||
envLicensePublicKey,
|
||||
} {
|
||||
if !strings.Contains(err.Error(), want) {
|
||||
t.Fatalf("resolveInstallLicenseConfig() error = %q, want %q", err.Error(), want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMaterialAppEnvIncludesLicenseRuntimeConfig(t *testing.T) {
|
||||
req := InstallRequest{LicenseKey: "FLNXI-TEST"}
|
||||
secrets := InstallSecrets{
|
||||
DbUser: "flnxi",
|
||||
DbPassword: "db-pass",
|
||||
DbName: "flnxi",
|
||||
MinioRootUser: "minio",
|
||||
MinioRootPassword: "minio-pass",
|
||||
S3Bucket: "materials",
|
||||
NextAuthSecret: "next-secret",
|
||||
LocalEncryptionKey: strings.Repeat("b", 32),
|
||||
}
|
||||
licenseConfig := InstallLicenseConfig{
|
||||
ServerURL: "https://license.example.test",
|
||||
ProjectCode: "flnxi-sucai",
|
||||
RequestSigningSecret: strings.Repeat("a", 32),
|
||||
PublicKey: "-----BEGIN PUBLIC KEY-----\\nabc\\n-----END PUBLIC KEY-----",
|
||||
LocalEncryptionKey: secrets.LocalEncryptionKey,
|
||||
}
|
||||
|
||||
env := envSliceToMap(materialAppEnv(req, secrets, licenseConfig, "192.168.1.20", 4320))
|
||||
for _, want := range []string{
|
||||
envLicenseServerURL,
|
||||
envLicenseProjectCode,
|
||||
envLicenseRequestSigningSecret,
|
||||
envLicensePublicKey,
|
||||
envLocalEncryptionKey,
|
||||
"DATABASE_URL",
|
||||
"S3_PUBLIC_DOMAIN",
|
||||
"NEXTAUTH_URL",
|
||||
"APP_PUBLIC_URL",
|
||||
} {
|
||||
if env[want] == "" {
|
||||
t.Fatalf("materialAppEnv() missing %s in %v", want, env)
|
||||
}
|
||||
}
|
||||
if env["APP_PUBLIC_URL"] != "http://192.168.1.20:4320" {
|
||||
t.Fatalf("APP_PUBLIC_URL = %q, want host URL", env["APP_PUBLIC_URL"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunInstallStopsBeforePullWhenLicenseConfigMissing(t *testing.T) {
|
||||
resetInstallStatusForTest()
|
||||
|
||||
fake := &fakeInstallDocker{}
|
||||
runInstall(fake, InstallRequest{MaterialImage: "registry.local:5000/material/flnxi-material:1.0.1"})
|
||||
|
||||
statusMutex.RLock()
|
||||
status := *installStatus
|
||||
statusMutex.RUnlock()
|
||||
|
||||
if status.Status != "failed" {
|
||||
t.Fatalf("install status = %q, want failed", status.Status)
|
||||
}
|
||||
if !strings.Contains(status.Error, "许可证配置缺失") {
|
||||
t.Fatalf("install error = %q, want license config error", status.Error)
|
||||
}
|
||||
if len(fake.pulledImages) != 0 {
|
||||
t.Fatalf("pulled images = %v, want none before license config is valid", fake.pulledImages)
|
||||
}
|
||||
if len(fake.createdContainers) != 0 {
|
||||
t.Fatalf("created containers = %v, want none before license config is valid", fake.createdContainers)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunUpdateRestoresOldContainerAfterHealthCheckFailure(t *testing.T) {
|
||||
resetUpdateStatusForTest()
|
||||
|
||||
@ -116,6 +231,50 @@ func resetUpdateStatusForTest() {
|
||||
statusMutex.Unlock()
|
||||
}
|
||||
|
||||
func resetInstallStatusForTest() {
|
||||
statusMutex.Lock()
|
||||
installStatus = &InstallStatus{Status: "running", Step: "初始化", Progress: 0}
|
||||
statusMutex.Unlock()
|
||||
}
|
||||
|
||||
func envSliceToMap(values []string) map[string]string {
|
||||
result := make(map[string]string, len(values))
|
||||
for _, value := range values {
|
||||
parts := strings.SplitN(value, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
result[parts[0]] = parts[1]
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
type fakeInstallDocker struct {
|
||||
pulledImages []string
|
||||
createdContainers []string
|
||||
}
|
||||
|
||||
func (f *fakeInstallDocker) PullImageWithProgress(ctx context.Context, image string, authStr string, onProgress func(current, total int64, status string)) error {
|
||||
f.pulledImages = append(f.pulledImages, image)
|
||||
if onProgress != nil {
|
||||
onProgress(1, 1, "done")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeInstallDocker) ExecOnHost(ctx context.Context, cmd string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeInstallDocker) CreateNetworkIfNotExists(ctx context.Context, name string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeInstallDocker) CreateAndStartContainer(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, name string) (string, error) {
|
||||
f.createdContainers = append(f.createdContainers, name)
|
||||
return name + "-id", nil
|
||||
}
|
||||
|
||||
type fakeUpdateDocker struct {
|
||||
container *docker.ContainerInfo
|
||||
healthErr error
|
||||
@ -197,3 +356,4 @@ func contains(values []string, want string) bool {
|
||||
}
|
||||
|
||||
var _ updateDockerClient = (*fakeUpdateDocker)(nil)
|
||||
var _ installDockerClient = (*fakeInstallDocker)(nil)
|
||||
|
||||
@ -7,13 +7,14 @@ import (
|
||||
)
|
||||
|
||||
type InstallSecrets struct {
|
||||
DbUser string
|
||||
DbPassword string
|
||||
DbName string
|
||||
MinioRootUser string
|
||||
MinioRootPassword string
|
||||
S3Bucket string
|
||||
NextAuthSecret string
|
||||
DbUser string
|
||||
DbPassword string
|
||||
DbName string
|
||||
MinioRootUser string
|
||||
MinioRootPassword string
|
||||
S3Bucket string
|
||||
NextAuthSecret string
|
||||
LocalEncryptionKey string
|
||||
}
|
||||
|
||||
func generateInstallSecrets() (InstallSecrets, error) {
|
||||
@ -32,14 +33,20 @@ func generateInstallSecrets() (InstallSecrets, error) {
|
||||
return InstallSecrets{}, err
|
||||
}
|
||||
|
||||
localEncryptionKey, err := randomHex(32)
|
||||
if err != nil {
|
||||
return InstallSecrets{}, err
|
||||
}
|
||||
|
||||
return InstallSecrets{
|
||||
DbUser: "flnxi",
|
||||
DbPassword: dbPassword,
|
||||
DbName: "flnxi",
|
||||
MinioRootUser: "flnxi",
|
||||
MinioRootPassword: minioPassword,
|
||||
S3Bucket: "flnxi",
|
||||
NextAuthSecret: nextAuthSecret,
|
||||
DbUser: "flnxi",
|
||||
DbPassword: dbPassword,
|
||||
DbName: "flnxi",
|
||||
MinioRootUser: "flnxi",
|
||||
MinioRootPassword: minioPassword,
|
||||
S3Bucket: "flnxi",
|
||||
NextAuthSecret: nextAuthSecret,
|
||||
LocalEncryptionKey: localEncryptionKey,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user