Files
LokiStack/backend/services/image_registry.go
T
opsx f3733f0bfc feat: 品牌统一 LokiStack + 全站中英 i18n + 用户批量删除 + 主题定时切换与浅色侧栏
- 品牌: Go 模块/数据文件/二进制/构建部署资产全部 opsx→lokistack;前端 localStorage 命名空间迁移(读旧写新,含按用户 id 前缀迁移);修复 nginx upstream 与 Makefile 引用已改名服务的断裂
- i18n: react-i18next 落地,common + 11 个页面域命名空间全站中英(菜单/顶栏/登录/列表页/表单/确认弹窗/状态徽章);设置页语言即时切换;修复词典自注册导致的循环导入白屏
- 用户管理: 批量删除(POST /users/batch-delete,不删自己/最后管理员,部分失败不阻断)+ 行复选/全选/批量工具条
- 主题: 浅色侧栏(中性灰激活态)、四态模式(浅色/深色/跟随系统/定时切换,按时区判定)、顶栏太阳月亮二态、设置页紧凑分段选择器
- UI: 登录记住用户名、CardActionButton 统一五页卡片操作钮、shadcn Badge 替换手写徽章、语义色修正
- 清理: 移除主机 Web 终端(前后端)、k8s-helm/Helm 遗留代码与字段、README 重写精简
- 修复: 侧栏与日志告警 order 重复、路由标题 i18n key 未翻译、词典双逗号等
2026-10-27 17:11:38 +08:00

191 lines
6.2 KiB
Go

package services
// 镜像仓库源 — 全局设置(参照 kuboard 的国内/海外镜像选择):
// overseas = docker.io 官方源;cn = daocloud 国内镜像;custom = 用户自填 registry 前缀。
// 影响平台拉取的全部镜像:Loki / Grafana / Promtail / Fluent Bit。
// 每个组件可单独配置仓库与 tag(Loki/Grafana 的 tag 留空则跟随部署版本)。
import (
"encoding/json"
"fmt"
"strings"
"lokistack/models"
"lokistack/repository"
)
const (
imageRegistrySettingKey = "image_registry"
RegistryOverseas = "overseas" // docker.io
RegistryCN = "cn" // docker.m.daocloud.io(国内加速)
RegistryCustom = "custom" // 用户私有仓库前缀
daocloudPrefix = "docker.m.daocloud.io"
)
// ComponentImage 单个组件的镜像配置
type ComponentImage struct {
Repository string `json:"repository"` // 如 grafana/loki
Tag string `json:"tag"` // 留空表示跟随部署版本 / 使用默认
}
// ImageRegistryConfig 镜像源配置
type ImageRegistryConfig struct {
Mode string `json:"mode"` // overseas | cn | custom
CustomHost string `json:"customHost"` // custom 模式下的仓库前缀
Images map[string]ComponentImage `json:"images"` // loki/grafana/promtail/fluentbit
}
type ImageRegistryPublic struct {
Mode string `json:"mode"`
CustomHost string `json:"customHost"`
Images map[string]ComponentImage `json:"images"`
// 示例预览(前端展示生效镜像)
SampleLoki string `json:"sampleLoki"`
SampleGrafana string `json:"sampleGrafana"`
SamplePromtail string `json:"samplePromtail"`
SampleFluentBit string `json:"sampleFluentBit"`
}
var defaultComponentImages = map[string]ComponentImage{
"loki": {Repository: "grafana/loki", Tag: ""}, // 跟随部署版本
"grafana": {Repository: "grafana/grafana", Tag: ""}, // 跟随部署版本
"promtail": {Repository: "grafana/promtail", Tag: "3.4.0"},
"fluentbit": {Repository: "fluent/fluent-bit", Tag: "5.1.5"},
}
func (c *ImageRegistryConfig) fillDefaults() {
if c.Images == nil {
c.Images = map[string]ComponentImage{}
}
for name, def := range defaultComponentImages {
img, ok := c.Images[name]
if !ok || (img.Repository == "" && img.Tag == "") {
c.Images[name] = def
continue
}
if img.Repository == "" {
img.Repository = def.Repository
}
c.Images[name] = img
}
}
func GetImageRegistry() *ImageRegistryConfig {
db := repository.GetDB()
cfg := &ImageRegistryConfig{Mode: RegistryOverseas}
if db != nil {
db.RLock()
var raw string
for i := range db.Settings {
if db.Settings[i].Key == imageRegistrySettingKey {
raw = db.Settings[i].Value
break
}
}
db.RUnlock()
if raw != "" {
_ = json.Unmarshal([]byte(raw), cfg)
}
}
if cfg.Mode == "" {
cfg.Mode = RegistryOverseas
}
cfg.fillDefaults()
return cfg
}
func SaveImageRegistry(mode, customHost string, images map[string]ComponentImage) error {
if mode != RegistryOverseas && mode != RegistryCN && mode != RegistryCustom {
return fmt.Errorf("无效的镜像源模式")
}
if mode == RegistryCustom {
customHost = strings.TrimSuffix(strings.TrimSpace(customHost), "/")
if customHost == "" || !strings.Contains(customHost, ".") {
return fmt.Errorf("自定义镜像仓库地址无效(需含域名,如 swr.cn-east-2.myhuaweicloud.com/your-ns)")
}
}
cfg := &ImageRegistryConfig{Mode: mode, CustomHost: customHost, Images: images}
cfg.fillDefaults()
// 清理空白输入
clean := map[string]ComponentImage{}
for name, img := range cfg.Images {
clean[name] = ComponentImage{Repository: strings.TrimSpace(img.Repository), Tag: strings.TrimSpace(img.Tag)}
}
cfg.Images = clean
data, _ := json.Marshal(cfg)
db := repository.GetDB()
newID := db.NextID("settings")
db.Lock()
defer db.Unlock()
for i := range db.Settings {
if db.Settings[i].Key == imageRegistrySettingKey {
db.Settings[i].Value = string(data)
db.RequestSave()
return nil
}
}
db.Settings = append(db.Settings, models.SystemSetting{ID: newID, Key: imageRegistrySettingKey, Value: string(data)})
db.RequestSave()
return nil
}
// ResolveImage 按当前镜像源解析镜像地址:docker.io/grafana/loki:3.4.0 形式的官方镜像
// 会按 mode 重写前缀;已带私有仓库前缀(含镜像源域名)的地址原样返回。
func ResolveImage(official string) string {
cfg := GetImageRegistry()
switch cfg.Mode {
case RegistryCN:
if strings.HasPrefix(official, "docker.io/") {
return daocloudPrefix + "/" + strings.TrimPrefix(official, "docker.io/")
}
if !strings.Contains(strings.SplitN(official, "/", 2)[0], ".") {
return daocloudPrefix + "/" + official
}
return official
case RegistryCustom:
if strings.HasPrefix(official, "docker.io/") {
return cfg.CustomHost + "/" + strings.TrimPrefix(official, "docker.io/")
}
if !strings.Contains(strings.SplitN(official, "/", 2)[0], ".") {
return cfg.CustomHost + "/" + official
}
return official
default:
return official
}
}
// ResolveImageFor 按组件配置解析镜像;组件 tag 为空时使用 fallbackTag(通常是部署版本)。
func ResolveImageFor(component, fallbackRepo, fallbackTag string) string {
cfg := GetImageRegistry()
img, ok := cfg.Images[component]
repo := fallbackRepo
tag := fallbackTag
if ok {
if strings.TrimSpace(img.Repository) != "" {
repo = strings.TrimSpace(img.Repository)
}
if strings.TrimSpace(img.Tag) != "" {
tag = strings.TrimSpace(img.Tag)
}
}
official := repo
if !strings.Contains(repo, ":") && tag != "" {
official = repo + ":" + tag
}
return ResolveImage(official)
}
// ImageRegistryPublicConfig 对外输出(含示例预览)
func ImageRegistryPublicConfig() *ImageRegistryPublic {
cfg := GetImageRegistry()
return &ImageRegistryPublic{
Mode: cfg.Mode,
CustomHost: cfg.CustomHost,
Images: cfg.Images,
SampleLoki: ResolveImageFor("loki", "grafana/loki", "3.4.0"),
SampleGrafana: ResolveImageFor("grafana", "grafana/grafana", "11.3.0"),
SamplePromtail: ResolveImageFor("promtail", "grafana/promtail", "3.4.0"),
SampleFluentBit: ResolveImageFor("fluentbit", "fluent/fluent-bit", "5.1.5"),
}
}