diff --git a/.dockerignore b/.dockerignore index d9fa3c9..fe13c4d 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,12 +1,34 @@ +# VCS / CI .git .gitignore +.gitea + +# Docs (not needed in image) +*.md +web/docs + +# Local runtime / secrets data/ +.env +.env.* + +# Build artifacts luminary +luminary-recover +.docker-bin/ *.db +*.test +coverage/ + +# Frontend (rebuilt in Dockerfile) web/node_modules web/dist + +# IDE / editor .idea .vscode -bifrost .cursor -*.md + +# OS +.DS_Store +Thumbs.db diff --git a/.gitea/README.md b/.gitea/README.md new file mode 100644 index 0000000..5ac4542 --- /dev/null +++ b/.gitea/README.md @@ -0,0 +1,182 @@ +# Gitea Actions CI 模板(Go + Vue) + +复制整个 `.gitea/` 目录到新仓库即可启用 CI:**可选 go test** + **Docker 构建/推送**(Go 编译与 Vue 构建在 Dockerfile 内完成)。 + +## 项目约定 + +| 路径 | 说明 | +|------|------| +| `go.mod` | 仓库根目录 | +| `Dockerfile` | 仓库根目录,多阶段构建(含前端 `web/`) | +| `web/` | 可选;由 Dockerfile 内 `npm run build` 处理,CI 不再单独构建 | + +## Dockerfile 要求 + +工作流的 **Build image** 步骤在仓库根目录(或 Variable `DOCKERFILE` 指定路径)执行 `docker buildx build`,**不会**在 CI 里单独装 Node / 跑 `npm`。因此 Dockerfile 必须能独立完成构建与(运行时)启动。 + +### 基本要求 + +| 项 | 要求 | +|----|------| +| 位置 | 默认仓库根目录 `Dockerfile`;其他路径用 Variable `DOCKERFILE` | +| 构建上下文 | 仓库根目录 `.`(整个仓库会被 `COPY` 进镜像) | +| Go 版本 | `go.mod` 里 `go` 行与 `golang` 基础镜像大版本一致 | +| 多架构 | `go build` 须使用 `GOARCH="$TARGETARCH"`(buildx 注入);推荐 `CGO_ENABLED=0` | +| Go 模块代理 | **必须**在镜像内显式设置 `GOPROXY` / `GOSUMDB`(见下);容器内不会自动读取 `go.env` | +| 基础镜像加速 | 支持构建参数 `IMAGE_PREFIX`(CI 默认 `docker.1panel.live/library/`) | +| 前端(可选) | 存在 `web/` 时在 Dockerfile 内完成 `npm ci` + `npm run build` | + +### CI 自动传入的构建参数 + +| 参数 | 默认 | 说明 | +|------|------|------| +| `IMAGE_PREFIX` | `docker.1panel.live/library/` | 基础镜像前缀;`hub` 表示 Docker Hub | +| `GOPROXY` | `https://goproxy.cn,direct` | 与 workflow / Variable 一致 | +| `GOSUMDB` | `sum.golang.google.cn` | checksum 数据库 | + +buildx 还会注入 `TARGETARCH`、`BUILDPLATFORM` 等,无需在 workflow 里写。 + +### 推荐:Go + Vue 多阶段模板 + +```dockerfile +# syntax=docker/dockerfile:1 +ARG IMAGE_PREFIX= + +# 1) 前端(无 web/ 时可删整个 stage,并去掉 go-builder 里 COPY dist) +FROM --platform=$BUILDPLATFORM ${IMAGE_PREFIX}node:20-alpine AS web-builder +WORKDIR /src/web +COPY web/package.json web/package-lock.json web/.npmrc ./ +RUN npm ci +COPY web/ ./ +RUN npm run build + +# 2) Go 编译 +FROM --platform=$BUILDPLATFORM ${IMAGE_PREFIX}golang:1.25-alpine AS go-builder +ARG TARGETARCH +ARG GOPROXY=https://goproxy.cn,direct +ARG GOSUMDB=sum.golang.google.cn +ENV GOPROXY=${GOPROXY} GOSUMDB=${GOSUMDB} + +WORKDIR /src +COPY go.mod go.sum ./ +RUN --mount=type=cache,target=/go/pkg/mod go mod download + +COPY cmd/ ./cmd/ +COPY internal/ ./internal/ +# 若静态资源嵌入 Go:COPY --from=web-builder /src/web/dist/ ./path/to/static/ + +RUN --mount=type=cache,target=/go/pkg/mod \ + --mount=type=cache,target=/root/.cache/go-build \ + CGO_ENABLED=0 GOOS=linux GOARCH="$TARGETARCH" \ + go build -ldflags="-w -s" -o /app ./cmd/yourapp + +# 3) 运行镜像 +FROM ${IMAGE_PREFIX}alpine:3.20 +RUN apk add --no-cache ca-certificates tzdata +COPY --from=go-builder /app /usr/local/bin/yourapp +ENTRYPOINT ["/usr/local/bin/yourapp"] +``` + +按项目调整:`./cmd/yourapp`、静态资源路径、运行用户、`EXPOSE` / `VOLUME` 等。 + +### 仅 Go(无前端) + +删除 `web-builder` stage;`go-builder` 中不要 `COPY` 前端产物;其余 `GOPROXY` / `TARGETARCH` / `IMAGE_PREFIX` 要求相同。 + +### 前端 npm 源(国内) + +在 `web/.npmrc` 配置 registry,并在 Dockerfile 里与 `package.json` 一并 `COPY`: + +```ini +registry=https://registry.npmmirror.com +``` + +### 本地验证(与 CI 一致) + +```bash +docker buildx build --platform linux/amd64 \ + --build-arg IMAGE_PREFIX=docker.1panel.live/library/ \ + --build-arg GOPROXY=https://goproxy.cn,direct \ + -f Dockerfile -t myapp:test . +``` + +### 常见 Dockerfile 构建错误 + +| 报错 | 原因 | 处理 | +|------|------|------| +| `proxy.golang.org` timeout | 镜像内未设 `ENV GOPROXY` | go-builder 阶段加 `ARG`/`ENV GOPROXY` | +| `node` / Vite 版本不符 | 基础镜像 Node 过旧 | 使用 `node:20-alpine` 及以上 | +| 某架构 build 失败 | 未使用 `TARGETARCH` | `GOARCH="$TARGETARCH"` | +| 拉基础镜像 timeout | Hub 不可达 | `--build-arg IMAGE_PREFIX=docker.1panel.live/library/` 或 1Panel 配镜像加速 | + +## 一次性配置 + +1. **Runner**:部署 act_runner,标签含 `ubuntu-latest`,并挂载 `docker.sock`(见 [act-runner/README.md](act-runner/README.md))。 +2. **Secret**:仓库 Settings → Actions → Secrets,添加 `REGISTRY_TOKEN`(PAT,`write:package` 权限)。 +3. **Variable(推荐)**:若 runner 与 Gitea 同机、或 `gitea.server_url` 为内网地址,必须设置 `REGISTRY=你的公网域名`(如 `git.example.com`,**不要**填 `172.17.0.1:13827`)。 +4. **Gitea Registry**:服务端 `[packages] ENABLED = true`,`ROOT_URL` 正确;穿透场景建议 `PUBLIC_URL_DETECTION = never`(Gitea 1.26+)。 + +`REGISTRY` 未设置时,会从 `GITEA_ROOT_URL` 或 `gitea.server_url` 推断;均为内网地址时 workflow 会提前失败并提示。 + +## 可选 Variables + +仓库 Settings → Actions → Variables(留空则用默认值): + +| 变量 | 默认 | 说明 | +|------|------|------| +| `REGISTRY` | 见下方推断顺序 | **公网** Registry 主机名,如 `git.example.com`(勿用内网 IP:13827) | +| `GITEA_ROOT_URL` | (空) | 当 `gitea.server_url` 为内网时,可设 `https://git.example.com/` | +| `IMAGE_NAME` | 仓库名小写 | 镜像名,非 owner/repo 全路径 | +| `DOCKERFILE` | `Dockerfile` | Dockerfile 路径 | +| `DOCKER_PLATFORMS` | `linux/amd64,linux/arm64` | push 时 buildx 平台 | +| `GO_TEST_SCOPE` | `./...` | `go test` 包路径 | +| `RUN_GO_TEST` | (空,即运行) | 设 `false` 跳过 go test,仅 Docker 构建 | +| `DOCKER_IMAGE_PREFIX` | `1panel` | 基础镜像前缀;可选 `hub` / `daocloud` | +| `GOPROXY` | `https://goproxy.cn,direct` | Go 模块代理 | +| `GOSUMDB` | `sum.golang.google.cn` | Go checksum 数据库 | + +## 触发与镜像 tag + +- **pull_request**:go test(可关)+ 单架构 `docker build` 验证(不推送) +- **push main/master**:go test + 多架构构建并推送 `:latest`、`:sha-xxxxxxx` +- **push tag v\***:额外推送 `:v1.2.3` 等 + +示例(仓库 `rose_cat707/Prism`,Gitea 在 `git.example.com`): + +```text +git.example.com/rose_cat707/prism:latest +git.example.com/rose_cat707/prism:sha-35b3b48 +``` + +## 目录结构 + +```text +.gitea/ +├── README.md # 本文件 +├── workflows/ +│ └── ci.yml # 主工作流 +└── act-runner/ # runner 部署参考(可选) + ├── README.md + ├── config.yaml + ├── docker-compose.yml + └── .env.example +``` + +## 本地验证 Registry + +```bash +host=$(echo "https://你的-gitea-地址/" | sed -e 's|^https://||' -e 's|/.*||') +curl -s -D - "https://${host}/v2/" -o /dev/null | grep -i www-authenticate +docker pull "${host}/owner/image:latest" # 公开 Registry 无需 login +``` + +推送镜像(CI)仍需仓库 Secret `REGISTRY_TOKEN`(`write:package`)。仅拉取公开包不需要登录。 + +`realm` 应指向公网 Gitea 域名,而非 `127.0.0.1`。 + +## 常见 Registry 错误 + +| 报错 | 原因 | 处理 | +|------|------|------| +| `Get "https://172.17.0.1:13827/v2/"` HTTP/HTTPS | Variable `REGISTRY` 或 `gitea.server_url` 为内网地址 | 设 `REGISTRY=git.example.com` | +| token 指向 `127.0.0.1` | Gitea `realm` 配置错误 | `PUBLIC_URL_DETECTION=never` + 正确 `ROOT_URL` | diff --git a/.gitea/act-runner/.env.example b/.gitea/act-runner/.env.example new file mode 100644 index 0000000..06b3c00 --- /dev/null +++ b/.gitea/act-runner/.env.example @@ -0,0 +1,3 @@ +GITEA_INSTANCE_URL=https://git.example.com +GITEA_RUNNER_REGISTRATION_TOKEN=从仓库_Settings_Actions_Runners_复制 +GITEA_RUNNER_NAME=go-vue-ci-runner diff --git a/.gitea/act-runner/README.md b/.gitea/act-runner/README.md new file mode 100644 index 0000000..eb068a4 --- /dev/null +++ b/.gitea/act-runner/README.md @@ -0,0 +1,177 @@ +# act_runner 配置参考 + +工作流 `runs-on: ubuntu-latest`;`docker` job 额外使用 `catthehacker/ubuntu:act-22.04` 容器(含 Docker CLI)。 + +## 构建镜像必须:挂载 Docker Socket + +`docker` job 需要访问宿主机 Docker 引擎。在 **runner 所在机器** 的 `config.yaml` 中配置: + +```yaml +container: + options: -v /var/run/docker.sock:/var/run/docker.sock + valid_volumes: + - /var/run/docker.sock +``` + +推荐 job 镜像(含 Node + Docker CLI): + +```yaml +runner: + labels: + - "ubuntu-latest:docker://catthehacker/ubuntu:act-22.04" + - "ubuntu-22.04:docker://catthehacker/ubuntu:act-22.04" +``` + +修改后 **重启 runner**(例如 `docker restart ` 或重启 Gitea Runner 服务)。 + +## 1Panel 宿主机:配置 Docker 镜像加速(推荐) + +Runner 通过 `docker.sock` 使用宿主机 Docker。在 **1Panel** 中配置加速器后,普通 `docker pull` 会走加速;**buildx 多架构构建**仍建议配合 workflow 内的 `IMAGE_PREFIX`(默认 `docker.1panel.live`)。 + +1. 登录 1Panel → **容器** → **配置** +2. **镜像加速地址** 填入: + ```text + https://docker.1panel.live + ``` +3. 保存并 **重启 Docker** + +验证(在 runner 宿主机): + +```bash +docker info | grep -A5 'Registry Mirrors' +docker pull alpine:3.20 +``` + +等价 `daemon.json`: + +```json +{ + "registry-mirrors": ["https://docker.1panel.live"] +} +``` + +CI 工作流默认 `IMAGE_PREFIX=docker.1panel.live/library/`(buildkit 拉基础镜像)。若 1Panel 加速不可用,可在仓库 Variables 设 `DOCKER_IMAGE_PREFIX=daocloud` 或 `hub`。 + +参考:[1Panel 容器配置文档](https://1panel.cn/docs/user_manual/containers/setting) + +## 验证 + +在 runner 宿主机执行: + +```bash +docker info +ls -l /var/run/docker.sock +``` + +## 从零部署 runner(可选) + +```bash +cd .gitea/act-runner +cp .env.example .env # 填入 Registration Token +docker compose up -d +``` + +## GitHub Actions 镜像(替代 ghfast) + +日志里出现 `git clone 'https://ghfast.top/https://github.com/actions/checkout'` 说明 **runner 宿主机** 的 `config.yaml` 配置了 `github_mirror`,与仓库 workflow 无关。 + +在 runner 的 `config.yaml` 中修改(修改后重启 runner): + +```yaml +runner: + github_mirror: 'https://gitea.com' # 推荐 +``` + +常用替代方案: + +| `github_mirror` 值 | 说明 | +|------------------|------| +| `''`(留空) | 直连 `github.com`,网络可达时最简单 | +| `https://gitea.com` | Gitea 官方 actions 镜像,国内较稳 | +| `https://gitclone.com/github.com` | 第三方 GitHub 克隆镜像 | +| `https://ghfast.top/https://github.com` | 部分环境需代理认证,易报 `Proxy Authentication Required` | + +前提:Gitea `app.ini` 中 `[actions] DEFAULT_ACTIONS_URL = github`(默认)。 + +**不依赖 runner 镜像** 的写法(workflow 内写绝对 URL,仅改写 `github.com` 的请求): + +```yaml +uses: https://gitea.com/actions/checkout@v4 +``` + +本模板 `ci.yml` 已采用此写法。若其他 workflow 仍写 `uses: actions/checkout@v4`,仍需配置 `github_mirror` 或改为绝对 URL。 + +修改 `github_mirror` 后建议清理 runner 缓存目录(如 `/root/.cache/act`),否则旧缓存可能仍指向 ghfast。 + +## 常见错误 + +| 报错 | 原因 | 处理 | +|------|------|------| +| `registry-1.docker.io` i/o timeout | Docker Hub 不可达 | 1Panel 配 `docker.1panel.live`;Variable `DOCKER_IMAGE_PREFIX=1panel` | +| `ghfast.top` Proxy Authentication Required | runner `github_mirror` 指向 ghfast 且需代理认证 | 改 `github_mirror` 或删掉;见上文「GitHub Actions 镜像」 | +| `docker: command not found` | job 容器无 Docker CLI | 工作流已指定 act 镜像;或 runner 改用 catthehacker/ubuntu | +| `Cannot connect to Docker daemon` | 未挂载 docker.sock | 按上文修改 config.yaml 并重启 runner | +| `node not in PATH` | job 镜像无 Node | 标签映射改用 catthehacker/ubuntu:act-22.04 | +| `http: server gave HTTP response to HTTPS client` 且 token 指向 `127.0.0.1` | **Gitea Registry 配置/反代错误** | 见下文「Registry 登录失败」 | + +## Registry 登录失败(127.0.0.1 / HTTP vs HTTPS) + +若 `docker login git.rc707blog.top` 报错类似: + +```text +Get "https://127.0.0.1:xxxxx/v2/token?...": http: server gave HTTP response to HTTPS client +``` + +说明 Gitea 把 **Docker 认证 token 地址** 配成了本机内网地址,CI runner 访问不到。需在 **Gitea 服务器** 修复,而非改 workflow。 + +### 1. 检查 `app.ini` + +```ini +[server] +ROOT_URL = https://git.example.com/ +LOCAL_ROOT_URL = http://127.0.0.1:3000/ +; 内网穿透 / 错误 Host 时(Gitea 1.26+): +; PUBLIC_URL_DETECTION = never + +[packages] +ENABLED = true +``` + +`ROOT_URL` 必须与浏览器访问 Gitea 的 **HTTPS 外网地址** 完全一致(含末尾 `/`)。 + +### 2. 反向代理必须转发 `/v2` 并带上头 + +Container Registry 固定使用根路径 `/v2`。Nginx 示例: + +```nginx +location / { + client_max_body_size 0; + proxy_pass http://127.0.0.1:3000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; +} +``` + +关键:`X-Forwarded-Proto: https` 和正确的 `Host`(与 Gitea `ROOT_URL` 域名一致)。 + +### 3. 在 runner 宿主机验证(修复后) + +```bash +GITEA_HOST=git.example.com # 改成你的 Gitea 域名 +curl -s -D - "https://${GITEA_HOST}/v2/" -o /dev/null | grep -i www-authenticate +echo "$REGISTRY_TOKEN" | docker login "${GITEA_HOST}" -u 你的用户名 --password-stdin +``` + +应返回 `401 Unauthorized`(正常,表示 registry 可达)且 `docker login` 显示 **Login Succeeded**。 + +参考:[Gitea 反向代理文档](https://docs.gitea.com/administration/reverse-proxies) + +## Gitea Runner v0.6.x(个人 runner) + +1. 找到 runner 的配置文件或环境(安装目录 / docker compose) +2. 确保 runner 进程能访问宿主机 `/var/run/docker.sock` +3. Runners 页标签含 `ubuntu-latest` 且状态 **空闲/在线** + +若使用 Gitea 网页注册的个人 runner(docker 模式),通常需在 runner 启动参数或 `config.yaml` 里加入 socket 挂载,具体路径取决于你的安装方式。 diff --git a/.gitea/act-runner/config.yaml b/.gitea/act-runner/config.yaml new file mode 100644 index 0000000..797596b --- /dev/null +++ b/.gitea/act-runner/config.yaml @@ -0,0 +1,34 @@ +# act_runner 配置(可选参考部署) +# 工作流 runs-on: ubuntu-latest + +log: + level: info + +runner: + file: .runner + capacity: 2 + timeout: 3h + insecure: false + fetch_timeout: 5s + fetch_interval: 2s + # 拉取 uses: actions/checkout@v4 等 GitHub Action 时的镜像(替换 https://github.com) + # 需 Gitea app.ini 中 [actions] DEFAULT_ACTIONS_URL = github + # 留空则直连 github.com;第三方镜像不稳定时可改用 workflow 绝对 URL(见 .gitea/README.md) + # + # github_mirror: '' # 直连 GitHub + # github_mirror: 'https://gitea.com' # 推荐:Gitea 官方 actions 镜像 + # github_mirror: 'https://gitclone.com/github.com' # 第三方 GitHub 克隆镜像 + # github_mirror: 'https://ghfast.top/https://github.com' # 需代理认证时易失败,不推荐 + labels: + - "ubuntu-latest:docker://catthehacker/ubuntu:act-22.04" + - "ubuntu-22.04:docker://catthehacker/ubuntu:act-22.04" + +cache: + enabled: false + +container: + network: bridge + privileged: false + options: -v /var/run/docker.sock:/var/run/docker.sock + valid_volumes: + - /var/run/docker.sock diff --git a/.gitea/act-runner/docker-compose.yml b/.gitea/act-runner/docker-compose.yml new file mode 100644 index 0000000..cd192f1 --- /dev/null +++ b/.gitea/act-runner/docker-compose.yml @@ -0,0 +1,22 @@ +# Gitea act_runner — 可选参考部署(标签 default,与工作流一致) +# +# 若已在 Gitea 注册个人/仓库 runner 且标签为 default,无需使用本目录。 + +services: + act-runner: + image: docker.io/gitea/act_runner:0.2.12 + container_name: prism-act-runner + restart: unless-stopped + environment: + CONFIG_FILE: /config.yaml + GITEA_INSTANCE_URL: ${GITEA_INSTANCE_URL:-https://git.rc707blog.top} + GITEA_RUNNER_REGISTRATION_TOKEN: ${GITEA_RUNNER_REGISTRATION_TOKEN} + GITEA_RUNNER_NAME: ${GITEA_RUNNER_NAME:-prism-ci-runner} + volumes: + - ./config.yaml:/config.yaml:ro + - act-runner-data:/data + - /var/run/docker.sock:/var/run/docker.sock + working_dir: /data + +volumes: + act-runner-data: diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..1500876 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,236 @@ +# 通用 Go + Vue 项目 CI(Gitea Actions) +# +# 单 job:可选 go test + Docker 多架构构建/推送 +# 约定:go.mod、Dockerfile(详见 .gitea/README.md「Dockerfile 要求」);前端(可选)在 web/ +# +# 前置:act_runner(ubuntu-latest + docker.sock)、Secret REGISTRY_TOKEN +# Variables:REGISTRY、IMAGE_NAME、DOCKER_IMAGE_PREFIX、RUN_GO_TEST 等 + +name: CI + +on: + push: + branches: [main, master] + tags: ['v*'] + pull_request: + +env: + REGISTRY: ${{ vars.REGISTRY }} + GITEA_ROOT_URL: ${{ vars.GITEA_ROOT_URL }} + IMAGE_NAME: ${{ vars.IMAGE_NAME }} + DOCKERFILE: ${{ vars.DOCKERFILE }} + DOCKER_PLATFORMS: ${{ vars.DOCKER_PLATFORMS }} + GO_TEST_SCOPE: ${{ vars.GO_TEST_SCOPE }} + RUN_GO_TEST: ${{ vars.RUN_GO_TEST }} + DOCKER_IMAGE_PREFIX: ${{ vars.DOCKER_IMAGE_PREFIX }} + GOPROXY: ${{ vars.GOPROXY }} + GOSUMDB: ${{ vars.GOSUMDB }} + +jobs: + docker: + runs-on: ubuntu-latest + container: + image: catthehacker/ubuntu:act-22.04 + steps: + - name: Checkout + uses: https://gitea.com/actions/checkout@v4 + + - name: Run Go tests + if: vars.RUN_GO_TEST != 'false' + shell: bash + run: | + set -euo pipefail + export GOPROXY="${GOPROXY:-https://goproxy.cn,direct}" + export GOSUMDB="${GOSUMDB:-sum.golang.google.cn}" + + GO_VERSION=$(grep '^go ' go.mod | awk '{print $2}') + ARCH=$(uname -m | sed 's/x86_64/amd64/; s/aarch64/arm64/') + TAR="go${GO_VERSION}.linux-${ARCH}.tar.gz" + + if ! command -v go >/dev/null 2>&1 || ! go version | grep -q "go${GO_VERSION} "; then + downloaded=0 + for url in \ + "https://mirrors.aliyun.com/golang/${TAR}" \ + "https://golang.google.cn/dl/${TAR}" \ + "https://go.dev/dl/${TAR}"; do + echo "trying ${url}" + if curl -fsSL --connect-timeout 20 --retry 3 --retry-delay 5 --max-time 600 \ + "$url" -o /tmp/go.tgz; then + downloaded=1 + break + fi + done + [ "$downloaded" -eq 1 ] || { echo "failed to download Go ${GO_VERSION}" >&2; exit 1; } + rm -rf /usr/local/go + tar -C /usr/local -xzf /tmp/go.tgz + export PATH="/usr/local/go/bin:${PATH}" + fi + go version + go test "${GO_TEST_SCOPE:-./...}" + + - name: Setup Docker + run: | + set -eux + if ! command -v docker >/dev/null 2>&1; then + apt-get update + DEBIAN_FRONTEND=noninteractive apt-get install -y docker.io + fi + if [ ! -S /var/run/docker.sock ]; then + echo "ERROR: /var/run/docker.sock 未挂载到 job 容器。" >&2 + echo "请在 act_runner config.yaml 中配置:" >&2 + echo " container.options: -v /var/run/docker.sock:/var/run/docker.sock" >&2 + echo " container.valid_volumes: [/var/run/docker.sock]" >&2 + exit 1 + fi + docker info + + - name: Resolve registry + id: reg + if: gitea.event_name == 'push' + shell: bash + env: + GITEA_SERVER_URL: ${{ gitea.server_url }} + run: | + set -euo pipefail + + url_to_host() { + echo "$1" | sed -e 's|^https://||' -e 's|^http://||' -e 's|/.*||' + } + + is_internal_host() { + local host="${1%%:*}" + case "$host" in + localhost|127.*|10.*|192.168.*) return 0 ;; + 172.*) + local second + second=$(echo "$host" | cut -d. -f2) + [ "$second" -ge 16 ] && [ "$second" -le 31 ] + return + ;; + *) return 1 ;; + esac + } + + registry="${REGISTRY:-}" + if [ -z "$registry" ] && [ -n "${GITEA_ROOT_URL:-}" ]; then + registry=$(url_to_host "${GITEA_ROOT_URL}") + fi + if [ -z "$registry" ]; then + registry=$(url_to_host "${GITEA_SERVER_URL}") + fi + + if is_internal_host "$registry"; then + echo "ERROR: Registry 主机 '${registry}' 是内网地址。" >&2 + echo "请设置 Variable REGISTRY=公网 Gitea 域名(如 git.example.com)。" >&2 + exit 1 + fi + + echo "registry host: ${registry}" + + auth_header="" + if auth_header=$(curl -fsS -D - "https://${registry}/v2/" -o /dev/null 2>&1 | grep -i '^www-authenticate:'); then + if echo "$auth_header" | grep -qiE '127\.0\.0\.1|172\.(1[6-9]|2[0-9]|3[01])\.|localhost|:13827'; then + echo "ERROR: Registry token realm 指向内网地址:${auth_header}" >&2 + exit 1 + fi + fi + + echo "host=${registry}" >> "${GITHUB_OUTPUT}" + + - name: Log in to Container Registry + if: gitea.event_name == 'push' + shell: bash + run: | + set -euo pipefail + echo "${{ secrets.REGISTRY_TOKEN }}" | \ + docker login "${{ steps.reg.outputs.host }}" -u "${{ gitea.actor }}" --password-stdin + + - name: Build image + shell: bash + env: + GITEA_SHA: ${{ gitea.sha }} + GITEA_REF: ${{ gitea.ref }} + GITEA_REPOSITORY: ${{ gitea.repository }} + GITEA_REPOSITORY_OWNER: ${{ gitea.repository_owner }} + GITEA_EVENT_NAME: ${{ gitea.event_name }} + REGISTRY_HOST: ${{ steps.reg.outputs.host }} + run: | + set -euo pipefail + + dockerfile="${DOCKERFILE:-Dockerfile}" + + if [ "${DOCKER_IMAGE_PREFIX:-}" = "hub" ] || [ "${DOCKER_IMAGE_PREFIX:-}" = "docker.io" ]; then + image_prefix="" + elif [ -z "${DOCKER_IMAGE_PREFIX:-}" ] || [ "${DOCKER_IMAGE_PREFIX}" = "1panel" ]; then + image_prefix="docker.1panel.live/library/" + elif [ "${DOCKER_IMAGE_PREFIX}" = "daocloud" ]; then + image_prefix="docker.m.daocloud.io/library/" + else + image_prefix="${DOCKER_IMAGE_PREFIX}" + [[ "${image_prefix}" == */ ]] || image_prefix="${image_prefix}/" + fi + echo "using IMAGE_PREFIX=${image_prefix:-}" + build_args=( + --build-arg "IMAGE_PREFIX=${image_prefix}" + --build-arg "GOPROXY=${GOPROXY:-https://goproxy.cn,direct}" + --build-arg "GOSUMDB=${GOSUMDB:-sum.golang.google.cn}" + ) + + builder_id="gitea-buildx-${GITEA_REPOSITORY//\//-}" + + if [ "${GITEA_EVENT_NAME}" = "push" ]; then + install_binfmt() { + for img in \ + "docker.1panel.live/tonistiigi/binfmt:latest" \ + "docker.m.daocloud.io/tonistiigi/binfmt:latest" \ + "tonistiigi/binfmt:latest"; do + echo "trying binfmt image: ${img}" + if docker run --privileged --rm "${img}" --install all; then + return 0 + fi + done + return 1 + } + install_binfmt || true + + registry="${REGISTRY_HOST}" + image_name="${IMAGE_NAME:-$(echo "${GITEA_REPOSITORY##*/}" | tr '[:upper:]' '[:lower:]')}" + platforms="${DOCKER_PLATFORMS:-linux/amd64,linux/arm64}" + image="${registry}/${GITEA_REPOSITORY_OWNER}/${image_name}" + tags="${image}:sha-${GITEA_SHA:0:7}" + case "${GITEA_REF}" in + refs/heads/main|refs/heads/master) + tags="${tags},${image}:latest" + ;; + refs/tags/v*) + tags="${tags},${image}:${GITEA_REF#refs/tags/}" + ;; + esac + + docker buildx create --name "${builder_id}" --use 2>/dev/null || docker buildx use "${builder_id}" + docker buildx inspect --bootstrap + + tag_args=() + IFS=',' read -r -a tag_list <<< "$tags" + for t in "${tag_list[@]}"; do + tag_args+=(-t "$t") + done + + docker buildx build \ + --platform "${platforms}" \ + -f "${dockerfile}" \ + "${build_args[@]}" \ + "${tag_args[@]}" \ + --push \ + . + else + # PR:仅单架构构建验证 Dockerfile,不推送 + docker buildx create --name "${builder_id}" --use 2>/dev/null || docker buildx use "${builder_id}" + docker buildx inspect --bootstrap + docker buildx build \ + --platform linux/amd64 \ + -f "${dockerfile}" \ + "${build_args[@]}" \ + --load \ + . + fi diff --git a/.gitignore b/.gitignore index 7bca6a7..632e180 100644 --- a/.gitignore +++ b/.gitignore @@ -1,41 +1,42 @@ -# ---> Go -# If you prefer the allow list template instead of the deny list, see community template: -# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore -# -# Binaries for programs and plugins +# Binaries +/luminary +/luminary-recover *.exe *.exe~ *.dll *.so *.dylib - -# Test binary, built with `go test -c` *.test -# Output of the go coverage tool, specifically when used with LiteIDE -*.out - -# Dependency directories (remove the comment below to include it) -# vendor/ - -# Go workspace file +# Go go.work go.work.sum +*.out +coverage/ +*.coverprofile -# env file -.env - -# Luminary runtime +# Luminary runtime data +data/ /data/ -/luminary -.docker-bin/ *.db -# Node -web/node_modules/ -web/dist/assets/ +# Docker local prebuilt +.docker-bin/ -# IDE +# Frontend build output +web/dist/ +web/node_modules/ + +# Environment +.env +.env.* +!.env.example + +# IDE / editor .idea/ .vscode/ +.cursor/ +# OS +.DS_Store +Thumbs.db diff --git a/Dockerfile b/Dockerfile index 127e0f6..10abea8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,43 +1,68 @@ -# Multi-stage: build frontend + Go binaries at image build time; runtime only executes. -ARG NODE_MAJOR=20 -ARG DOCKER_REGISTRY=docker.1panel.live/library/ -ARG APK_MIRROR=mirrors.aliyun.com +# Luminary AI Gateway(Go + Vue 管理台) +# +# 构建: +# docker build -t luminary:latest . +# +# 运行: +# docker run -d --name luminary \ +# -p 8293:8293 \ +# -v luminary-data:/data \ +# luminary:latest +# +# 生产镜像由 Gitea Actions 构建,见 README 与 .gitea/README.md + +# syntax=docker/dockerfile:1 + +# 基础镜像默认 Docker Hub;CI 默认 1Panel 加速(docker.1panel.live),本地可传: +# docker build --build-arg IMAGE_PREFIX=docker.1panel.live/library/ -t luminary:latest . +ARG IMAGE_PREFIX= + +FROM --platform=$BUILDPLATFORM ${IMAGE_PREFIX}node:20-alpine AS web-builder -FROM ${DOCKER_REGISTRY}node:${NODE_MAJOR}-alpine AS web WORKDIR /src/web -ARG NPM_REGISTRY=https://registry.npmmirror.com -COPY web/package.json web/package-lock.json ./ -RUN npm ci --registry="${NPM_REGISTRY}" -COPY web/ ./ +COPY web/package.json web/package-lock.json web/.npmrc ./ +RUN npm ci +COPY web/src web/src +COPY web/public web/public +COPY web/index.html web/tsconfig.json web/vite.config.ts ./ RUN npm run build -ARG DOCKER_REGISTRY=docker.1panel.live/library/ -FROM ${DOCKER_REGISTRY}golang:1.25-alpine AS gobuild -WORKDIR /src +FROM --platform=$BUILDPLATFORM ${IMAGE_PREFIX}golang:1.25-alpine AS go-builder + +ARG TARGETARCH=amd64 ARG GOPROXY=https://goproxy.cn,direct ARG GOSUMDB=sum.golang.google.cn -ENV CGO_ENABLED=0 \ - GOPROXY=${GOPROXY} \ - GOSUMDB=${GOSUMDB} -COPY go.mod go.sum ./ -RUN go mod download -COPY . . -COPY --from=web /src/web/dist ./web/dist -RUN go build -trimpath -ldflags="-s -w" -o /out/luminary ./cmd/luminary \ - && go build -trimpath -ldflags="-s -w" -o /out/luminary-recover ./cmd/luminary-recover +ENV CGO_ENABLED=0 GOPROXY=${GOPROXY} GOSUMDB=${GOSUMDB} -ARG DOCKER_REGISTRY=docker.1panel.live/library/ +WORKDIR /src +COPY go.mod go.sum ./ +RUN --mount=type=cache,target=/go/pkg/mod \ + go mod download + +COPY cmd/ ./cmd/ +COPY internal/ ./internal/ +COPY web/static_embed.go ./web/ +COPY --from=web-builder /src/web/dist ./web/dist + +RUN --mount=type=cache,target=/go/pkg/mod \ + --mount=type=cache,target=/root/.cache/go-build \ + GOOS=linux GOARCH="$TARGETARCH" \ + go build -trimpath -ldflags="-w -s" -o /luminary ./cmd/luminary \ + && go build -trimpath -ldflags="-w -s" -o /luminary-recover ./cmd/luminary-recover + +ARG IMAGE_PREFIX= ARG APK_MIRROR=mirrors.aliyun.com -FROM ${DOCKER_REGISTRY}alpine:3.20 AS runtime +FROM ${IMAGE_PREFIX}alpine:3.20 + ARG APK_MIRROR RUN sed -i "s/dl-cdn.alpinelinux.org/${APK_MIRROR}/g" /etc/apk/repositories \ - && apk add --no-cache ca-certificates tini + && apk add --no-cache ca-certificates tzdata tini ENV LUMINARY_DATA=/data \ LUMINARY_ADDR=:8293 -COPY --from=gobuild /out/luminary /usr/local/bin/luminary -COPY --from=gobuild /out/luminary-recover /usr/local/bin/luminary-recover +COPY --from=go-builder /luminary /usr/local/bin/luminary +COPY --from=go-builder /luminary-recover /usr/local/bin/luminary-recover COPY docker/entrypoint.sh /entrypoint.sh COPY scripts/luminary-recover.sh /usr/local/bin/luminary-recover.sh RUN chmod +x /entrypoint.sh /usr/local/bin/luminary-recover.sh diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..22f4732 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Luminary Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile index 6547830..795bca8 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ export GOPROXY ?= https://goproxy.cn,direct export GOSUMDB ?= sum.golang.google.cn export GOPRIVATE ?= git.rc707blog.top -export DOCKER_REGISTRY ?= docker.1panel.live/library/ +export IMAGE_PREFIX ?= docker.1panel.live/library/ export APK_MIRROR ?= mirrors.aliyun.com export DOCKER_PLATFORM ?= linux/amd64 export LUMINARY_IMAGE ?= registry.rc707blog.top/rose_cat707/luminary:latest @@ -33,7 +33,7 @@ test: docker-build: docker buildx build --platform $(DOCKER_PLATFORM) \ -t $(LUMINARY_IMAGE) \ - --build-arg DOCKER_REGISTRY=$(DOCKER_REGISTRY) \ + --build-arg IMAGE_PREFIX=$(IMAGE_PREFIX) \ --build-arg APK_MIRROR=$(APK_MIRROR) \ --build-arg GOPROXY=$(GOPROXY) \ --build-arg GOSUMDB=$(GOSUMDB) \ @@ -43,7 +43,7 @@ docker-build: docker-push: docker buildx build --platform $(DOCKER_PLATFORM) \ -t $(LUMINARY_IMAGE) \ - --build-arg DOCKER_REGISTRY=$(DOCKER_REGISTRY) \ + --build-arg IMAGE_PREFIX=$(IMAGE_PREFIX) \ --build-arg APK_MIRROR=$(APK_MIRROR) \ --build-arg GOPROXY=$(GOPROXY) \ --build-arg GOSUMDB=$(GOSUMDB) \ @@ -57,7 +57,7 @@ docker-push-prebuilt: build-web docker buildx build --platform $(DOCKER_PLATFORM) \ -f docker/Dockerfile.prebuilt \ -t $(LUMINARY_IMAGE) \ - --build-arg DOCKER_REGISTRY=$(DOCKER_REGISTRY) \ + --build-arg IMAGE_PREFIX=$(IMAGE_PREFIX) \ --build-arg APK_MIRROR=$(APK_MIRROR) \ --push \ . diff --git a/README.md b/README.md index 42f187b..c01ac0d 100644 --- a/README.md +++ b/README.md @@ -133,7 +133,7 @@ cd web && npm install && npm run dev | `-data-dir` / `LUMINARY_DATA` | SQLite 数据目录 | `./data` | | `-addr` / `LUMINARY_ADDR` | HTTP 监听地址 | `:8293` | -> 若存在旧版 `configs/config.yaml` 或 `/data/config.yaml`,**仅在首次初始化系统设置时**会尝试导入其中的加密密钥等项(导入失败不阻塞启动)。 +可选环境变量 `LUMINARY_ENCRYPTION_KEY`:仅在首次初始化系统设置时写入加密密钥(一般无需设置,管理台可改)。 ## Docker 部署 @@ -152,7 +152,34 @@ LUMINARY_IMAGE=registry.example.com/luminary:v1.0.0 make docker-push docker compose up -d ``` -`docker-compose.yml` 通过环境变量 `LUMINARY_IMAGE` 指定镜像名(默认 `luminary:latest`)。更新版本需重新 `docker build` / `docker push` 后重启容器。 +`docker-compose.yml` 通过环境变量 `LUMINARY_IMAGE` 指定镜像名(默认 `registry.rc707blog.top/rose_cat707/luminary:latest`)。更新版本需重新 `docker build` / `docker push` 后重启容器。 + +### Gitea Actions CI + +生产镜像由 Gitea Actions 自动构建并推送到 Container Registry,工作流见 [`.gitea/workflows/ci.yml`](.gitea/workflows/ci.yml),详细配置见 [`.gitea/README.md`](.gitea/README.md)。 + +**一次性配置**(仓库 Settings → Actions): + +| 类型 | 名称 | 说明 | +|------|------|------| +| Secret | `REGISTRY_TOKEN` | Gitea PAT,需 `write:package` 权限 | +| Variable | `REGISTRY` | 公网 Gitea 域名,如 `git.rc707blog.top`(勿用内网 IP) | + +**触发与镜像 tag**: + +- `pull_request`:go test + 单架构 Docker 构建验证(不推送) +- `push main/master`:多架构构建并推送 `:latest`、`:sha-xxxxxxx` +- `push tag v*`:额外推送 `:v1.2.3` + +示例镜像地址(仓库 `rose_cat707/luminary`,Gitea 在 `git.rc707blog.top`): + +```text +git.rc707blog.top/rose_cat707/luminary:latest +git.rc707blog.top/rose_cat707/luminary:sha-35b3b48 +git.rc707blog.top/rose_cat707/luminary:v1.0.0 +``` + +部署时设置 `LUMINARY_IMAGE` 指向 CI 推送的镜像即可。 ## 应急恢复(容器内) @@ -189,7 +216,7 @@ go run ./cmd/luminary-recover --all | `--clear-ip` | 删除全部 IP 规则 | | `--clear-sessions` | 删除全部管理员 Session | | `--restart` | 向 PID 1 发送 SIGTERM,由容器重启策略拉起新进程(清除登录冷却) | -| `--password` | 新密码(默认 `admin123`;若存在旧版 config 则尝试读取其中 `admin.password`) | +| `--password` | 新密码(默认 `admin123`) | | `--username` | 管理员用户名(默认 `admin`) | | `-data-dir` / `LUMINARY_DATA` | 数据目录(默认 `./data`,容器内为 `/data`) | @@ -201,7 +228,6 @@ cmd/luminary-recover/ 应急恢复 CLI(容器内 docker exec 使用) scripts/ 运维脚本(含 luminary-recover.sh) internal/ Go 后端(gateway、store、handler...) web/ Vue 3 前端(构建产物嵌入二进制) -configs/ 旧版配置示例(可选,仅用于首次迁移导入) ``` ## License diff --git a/configs/config.example.yaml b/configs/config.example.yaml deleted file mode 100644 index a70e3be..0000000 --- a/configs/config.example.yaml +++ /dev/null @@ -1,31 +0,0 @@ -# 已废弃:Luminary 不再依赖配置文件启动,请在管理台「系统设置」维护参数。 -# 本文件仅用于从旧版部署首次迁移时自动导入(若存在且数据库尚无 system_settings 记录)。 -server: - addr: ":8293" - -database: - path: "./data/luminary.db" - -admin: - username: "admin" - password: "admin123" - -cache: - usage_flush_interval: 5s - -health_check: - interval: 30s - -security: - # 生产环境必须修改;容器首次启动会自动生成随机密钥 - # 也可通过环境变量覆盖:encryption_key: "env.LUMINARY_ENCRYPTION_KEY" - encryption_key: "change-me-in-production-32bytes!!" - session_ttl: 24h - login_max_attempts: 5 - login_lockout: 15m - # 仅当直连来源为以下 IP/CIDR 时才信任 X-Forwarded-For / X-Real-IP(反代部署时配置) - trusted_proxies: [] - -gateway: - max_retries: 3 - retry_backoff_ms: 200 diff --git a/docker-compose.yml b/docker-compose.yml index 64d535c..7b0697e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,10 +4,10 @@ services: context: . dockerfile: Dockerfile args: - DOCKER_REGISTRY: docker.1panel.live/library/ + IMAGE_PREFIX: docker.1panel.live/library/ APK_MIRROR: mirrors.aliyun.com - NPM_REGISTRY: https://registry.npmmirror.com GOPROXY: https://goproxy.cn,direct + GOSUMDB: sum.golang.google.cn image: ${LUMINARY_IMAGE:-registry.rc707blog.top/rose_cat707/luminary:latest} container_name: luminary restart: unless-stopped diff --git a/docker/Dockerfile.prebuilt b/docker/Dockerfile.prebuilt index 5871557..bf7a53a 100644 --- a/docker/Dockerfile.prebuilt +++ b/docker/Dockerfile.prebuilt @@ -1,7 +1,7 @@ # Runtime image using locally cross-compiled linux/amd64 binaries (avoids QEMU segfault in buildx). -ARG DOCKER_REGISTRY=docker.1panel.live/library/ +ARG IMAGE_PREFIX=docker.1panel.live/library/ ARG APK_MIRROR=mirrors.aliyun.com -FROM ${DOCKER_REGISTRY}alpine:3.20 AS runtime +FROM ${IMAGE_PREFIX}alpine:3.20 AS runtime ARG APK_MIRROR RUN sed -i "s/dl-cdn.alpinelinux.org/${APK_MIRROR}/g" /etc/apk/repositories \ && apk add --no-cache ca-certificates tini diff --git a/web/docs/UI-DESIGN-SYSTEM.md b/docs/UI-DESIGN-SYSTEM.md similarity index 100% rename from web/docs/UI-DESIGN-SYSTEM.md rename to docs/UI-DESIGN-SYSTEM.md diff --git a/go.mod b/go.mod index 7aa75ea..4a388c5 100644 --- a/go.mod +++ b/go.mod @@ -5,8 +5,9 @@ go 1.25.0 require ( github.com/gin-gonic/gin v1.12.0 github.com/glebarez/sqlite v1.11.0 + github.com/google/uuid v1.3.0 + github.com/gorilla/websocket v1.5.3 golang.org/x/crypto v0.53.0 - gopkg.in/yaml.v3 v3.0.1 gorm.io/gorm v1.31.1 ) @@ -24,8 +25,6 @@ require ( github.com/go-playground/validator/v10 v10.30.1 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-yaml v1.19.2 // indirect - github.com/google/uuid v1.3.0 // indirect - github.com/gorilla/websocket v1.5.3 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/json-iterator/go v1.1.12 // indirect diff --git a/go.sum b/go.sum index dfafabf..47e3498 100644 --- a/go.sum +++ b/go.sum @@ -50,10 +50,6 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= @@ -74,8 +70,6 @@ github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRC github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= -github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= -github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -109,8 +103,6 @@ golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/config/config.go b/internal/config/config.go deleted file mode 100644 index 842b969..0000000 --- a/internal/config/config.go +++ /dev/null @@ -1,113 +0,0 @@ -package config - -import ( - "fmt" - "os" - "strings" - "time" - - "gopkg.in/yaml.v3" -) - -type Config struct { - Server ServerConfig `yaml:"server"` - Database DatabaseConfig `yaml:"database"` - Admin AdminConfig `yaml:"admin"` - Cache CacheConfig `yaml:"cache"` - HealthCheck HealthCheckConfig `yaml:"health_check"` - Security SecurityConfig `yaml:"security"` - Gateway GatewayConfig `yaml:"gateway"` -} - -type GatewayConfig struct { - MaxRetries int `yaml:"max_retries"` - RetryBackoffMs int `yaml:"retry_backoff_ms"` -} - -type ServerConfig struct { - Addr string `yaml:"addr"` -} - -type DatabaseConfig struct { - Path string `yaml:"path"` -} - -type AdminConfig struct { - Username string `yaml:"username"` - Password string `yaml:"password"` -} - -type CacheConfig struct { - UsageFlushInterval time.Duration `yaml:"usage_flush_interval"` -} - -type HealthCheckConfig struct { - Interval time.Duration `yaml:"interval"` -} - -type SecurityConfig struct { - EncryptionKey string `yaml:"encryption_key"` - SessionTTL time.Duration `yaml:"session_ttl"` - IPAllowlist []string `yaml:"ip_allowlist"` - TrustedProxies []string `yaml:"trusted_proxies"` - LoginMaxAttempts int `yaml:"login_max_attempts"` - LoginLockout time.Duration `yaml:"login_lockout"` -} - -func Load(path string) (*Config, error) { - data, err := os.ReadFile(path) - if err != nil { - return nil, fmt.Errorf("read config: %w", err) - } - cfg := Default() - if err := yaml.Unmarshal(data, cfg); err != nil { - return nil, fmt.Errorf("parse config: %w", err) - } - resolveEnv(cfg) - return cfg, nil -} - -func Default() *Config { - return &Config{ - Server: ServerConfig{Addr: ":8080"}, - Database: DatabaseConfig{ - Path: "./data/luminary.db", - }, - Admin: AdminConfig{ - Username: "admin", - Password: "admin123", - }, - Cache: CacheConfig{ - UsageFlushInterval: 5 * time.Second, - }, - HealthCheck: HealthCheckConfig{ - Interval: 30 * time.Second, - }, - Security: SecurityConfig{ - EncryptionKey: "change-me-in-production-32bytes!!", - SessionTTL: 24 * time.Hour, - LoginMaxAttempts: 5, - LoginLockout: 15 * time.Minute, - }, - Gateway: GatewayConfig{ - MaxRetries: 3, - RetryBackoffMs: 200, - }, - } -} - -func resolveEnv(cfg *Config) { - cfg.Admin.Username = resolveEnvValue(cfg.Admin.Username) - cfg.Admin.Password = resolveEnvValue(cfg.Admin.Password) - cfg.Security.EncryptionKey = resolveEnvValue(cfg.Security.EncryptionKey) -} - -func resolveEnvValue(v string) string { - if strings.HasPrefix(v, "env.") { - key := strings.TrimPrefix(v, "env.") - if val := os.Getenv(key); val != "" { - return val - } - } - return v -} diff --git a/internal/settings/encryption.go b/internal/settings/encryption.go index cf30645..9651b92 100644 --- a/internal/settings/encryption.go +++ b/internal/settings/encryption.go @@ -6,7 +6,6 @@ import ( "strings" "github.com/rose_cat707/luminary/internal/auth" - "github.com/rose_cat707/luminary/internal/config" "github.com/rose_cat707/luminary/internal/model" "gorm.io/gorm" ) @@ -25,13 +24,6 @@ func encryptionKeyCandidates() []string { } add(os.Getenv("LUMINARY_ENCRYPTION_KEY")) - for _, path := range legacyConfigPaths() { - cfg, err := config.Load(path) - if err != nil { - continue - } - add(cfg.Security.EncryptionKey) - } add(legacyDefaultEncryptionKey) return out } diff --git a/internal/settings/legacy.go b/internal/settings/legacy.go index a828d25..6eee5d9 100644 --- a/internal/settings/legacy.go +++ b/internal/settings/legacy.go @@ -1,83 +1,22 @@ package settings import ( - "encoding/json" "os" "strings" - "github.com/rose_cat707/luminary/internal/config" "github.com/rose_cat707/luminary/internal/model" ) const legacyDefaultEncryptionKey = "change-me-in-production-32bytes!!" -func legacyConfigPaths() []string { - var paths []string - if p := os.Getenv("LUMINARY_CONFIG"); p != "" { - paths = append(paths, p) - } - paths = append(paths, "configs/config.yaml", "/data/config.yaml") - return paths -} - func mergeLegacySettings(s model.SystemSettings) model.SystemSettings { - for _, path := range legacyConfigPaths() { - cfg, err := config.Load(path) - if err != nil { - continue - } - key := strings.TrimSpace(cfg.Security.EncryptionKey) - if key != "" { - s.EncryptionKey = key - } - if cfg.Security.SessionTTL > 0 { - s.SessionTTLSeconds = int64(cfg.Security.SessionTTL.Seconds()) - } - if cfg.Security.LoginMaxAttempts > 0 { - s.LoginMaxAttempts = cfg.Security.LoginMaxAttempts - } - if cfg.Security.LoginLockout > 0 { - s.LoginLockoutSeconds = int64(cfg.Security.LoginLockout.Seconds()) - } - if len(cfg.Security.TrustedProxies) > 0 { - b, _ := json.Marshal(cfg.Security.TrustedProxies) - s.TrustedProxiesJSON = string(b) - } - if cfg.Cache.UsageFlushInterval > 0 { - s.UsageFlushSeconds = int64(cfg.Cache.UsageFlushInterval.Seconds()) - } - if cfg.HealthCheck.Interval > 0 { - s.HealthCheckSeconds = int64(cfg.HealthCheck.Interval.Seconds()) - } - if cfg.Gateway.MaxRetries > 0 { - s.GatewayMaxRetries = cfg.Gateway.MaxRetries - } - if cfg.Gateway.RetryBackoffMs >= 0 { - s.GatewayRetryBackoffMs = cfg.Gateway.RetryBackoffMs - } - return s - } if key := strings.TrimSpace(os.Getenv("LUMINARY_ENCRYPTION_KEY")); key != "" { s.EncryptionKey = key } return s } -// LegacyAdminCredentials returns admin bootstrap credentials from an optional legacy config file. +// LegacyAdminCredentials returns default bootstrap credentials for first admin. func LegacyAdminCredentials() (username, password string) { - username, password = "admin", "admin123" - for _, path := range legacyConfigPaths() { - cfg, err := config.Load(path) - if err != nil { - continue - } - if u := strings.TrimSpace(cfg.Admin.Username); u != "" { - username = u - } - if p := strings.TrimSpace(cfg.Admin.Password); p != "" { - password = p - } - return username, password - } - return username, password + return "admin", "admin123" } diff --git a/web/dist/index.html b/web/dist/index.html deleted file mode 100644 index 11e8810..0000000 --- a/web/dist/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - Luminary AI Gateway - - - - -
- - diff --git a/web/index.html b/web/index.html index b0e9e88..24662d8 100644 --- a/web/index.html +++ b/web/index.html @@ -3,6 +3,7 @@ + Luminary AI Gateway diff --git a/web/public/favicon.svg b/web/public/favicon.svg new file mode 100644 index 0000000..38df95c --- /dev/null +++ b/web/public/favicon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/web/src/components/BrandIcon.vue b/web/src/components/BrandIcon.vue new file mode 100644 index 0000000..0498832 --- /dev/null +++ b/web/src/components/BrandIcon.vue @@ -0,0 +1,20 @@ + + + diff --git a/web/src/layouts/MainLayout.vue b/web/src/layouts/MainLayout.vue index 24a9dc8..305be22 100644 --- a/web/src/layouts/MainLayout.vue +++ b/web/src/layouts/MainLayout.vue @@ -3,11 +3,7 @@