adjust dir structures
Some checks failed
docker-release / build-and-push (push) Failing after 9m59s

This commit is contained in:
2025-11-14 15:02:33 +08:00
parent 960cf59a1d
commit 83bb6623e8
10 changed files with 344 additions and 9 deletions

View File

@@ -2,6 +2,8 @@
.git .git
.github .github
.codex .codex
.specify
configs
node_modules/ node_modules/
coverage/ coverage/
logs logs

View File

@@ -8,13 +8,10 @@ ARG COMMIT=dev
WORKDIR /src WORKDIR /src
COPY go.mod go.sum ./ COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod go mod download RUN --mount=type=cache,target=/go/pkg/mod go mod download
COPY cmd ./cmd COPY . .
COPY internal ./internal
RUN --mount=type=cache,target=/root/.cache/go-build \ RUN --mount=type=cache,target=/root/.cache/go-build \
cd /src \
ls -l \
CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH \ CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH \
go build -trimpath -ldflags "-s -w -X github.com/rogeecn/any-hub/internal/version.Version=${VERSION} -X github.com/rogeecn/any-hub/internal/version.Commit=${COMMIT}" -o /out/any-hub ./cmd/any-hub go build -trimpath -ldflags "-s -w -X github.com/any-hub/any-hub/internal/version.Version=${VERSION} -X github.com/any-hub/any-hub/internal/version.Commit=${COMMIT}" -o /out/any-hub .
FROM gcr.io/distroless/static-debian12:nonroot FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /out/any-hub /usr/local/bin/any-hub COPY --from=builder /out/any-hub /usr/local/bin/any-hub

View File

@@ -4,10 +4,10 @@ GOCACHE ?= /tmp/go-build
.PHONY: build fmt test test-all run .PHONY: build fmt test test-all run
build: build:
$(GO) build ./cmd/any-hub $(GO) build .
fmt: fmt:
$(GO)fmt ./cmd ./internal ./tests $(GO) fmt ./...
test: test:
$(GO) test ./... $(GO) test ./...
@@ -16,4 +16,4 @@ test-all:
GOCACHE=$(GOCACHE) $(GO) test ./... GOCACHE=$(GOCACHE) $(GO) test ./...
run: run:
$(GO) run ./cmd/any-hub --config ./config.toml $(GO) run . --config ./config.toml

View File

@@ -0,0 +1,52 @@
package main
import (
"bytes"
"fmt"
"os"
"path/filepath"
"strings"
"testing"
)
func TestLoggingFallbackToStdout(t *testing.T) {
dir := t.TempDir()
blocked := filepath.Join(dir, "blocked")
if err := os.Mkdir(blocked, 0o755); err != nil {
t.Fatalf("创建目录失败: %v", err)
}
if err := os.Chmod(blocked, 0o000); err != nil {
t.Fatalf("设置目录权限失败: %v", err)
}
t.Cleanup(func() { _ = os.Chmod(blocked, 0o755) })
logPath := filepath.Join(blocked, "sub", "any-hub.log")
configPath := writeConfigFile(t, fmt.Sprintf(`
LogLevel = "info"
LogFilePath = "%s"
StoragePath = "%s"
ListenPort = 5000
[[Hub]]
Name = "docker"
Domain = "docker.local"
Upstream = "https://registry-1.docker.io"
Type = "docker"
`, logPath, filepath.Join(dir, "storage")))
useBufferWriters(t)
code := run(cliOptions{configPath: configPath, checkOnly: true})
if code != 0 {
t.Fatalf("日志 fallback 不应导致失败,得到 %d", code)
}
t.Log(stdOut.(*bytes.Buffer).String())
}
func writeConfigFile(t *testing.T, content string) string {
t.Helper()
file := filepath.Join(t.TempDir(), "config.toml")
if err := os.WriteFile(file, []byte(strings.TrimSpace(content)), 0o600); err != nil {
t.Fatalf("写入配置失败: %v", err)
}
return file
}

151
main.go Normal file
View File

@@ -0,0 +1,151 @@
package main
import (
"flag"
"fmt"
"io"
"os"
"github.com/sirupsen/logrus"
"github.com/any-hub/any-hub/internal/cache"
"github.com/any-hub/any-hub/internal/config"
"github.com/any-hub/any-hub/internal/logging"
"github.com/any-hub/any-hub/internal/proxy"
"github.com/any-hub/any-hub/internal/server"
"github.com/any-hub/any-hub/internal/version"
)
// cliOptions 汇总 CLI 标志解析后的结果,便于在测试中注入。
type cliOptions struct {
configPath string
checkOnly bool
showVersion bool
}
var (
stdOut io.Writer = os.Stdout
stdErr io.Writer = os.Stderr
)
func main() {
opts, err := parseCLIFlags(os.Args[1:])
if err != nil {
fmt.Fprintln(stdErr, err.Error())
os.Exit(2)
}
os.Exit(run(opts))
}
// run 根据解析到的 CLI 选项执行业务流程,并返回退出码,方便测试。
func run(opts cliOptions) int {
if opts.showVersion {
printVersion()
return 0
}
cfg, err := config.Load(opts.configPath)
if err != nil {
fmt.Fprintf(stdErr, "加载配置失败: %v\n", err)
return 1
}
logger, err := logging.InitLogger(cfg.Global)
if err != nil {
fmt.Fprintf(stdErr, "初始化日志失败: %v\n", err)
return 1
}
if opts.checkOnly {
fields := logging.BaseFields("check_config", opts.configPath)
fields["hubs"] = len(cfg.Hubs)
fields["credentials"] = config.CredentialModes(cfg.Hubs)
fields["result"] = "ok"
logger.WithFields(fields).Info("配置校验通过")
return 0
}
registry, err := server.NewHubRegistry(cfg)
if err != nil {
fmt.Fprintf(stdErr, "构建 Hub 注册表失败: %v\n", err)
return 1
}
// CLI 启动遵循“配置 → HubRegistry → 磁盘缓存 → Fiber server”顺序
// 保证所有请求共享统一的路由与缓存实例,方便观察 cache/log 指标。
store, err := cache.NewStore(cfg.Global.StoragePath)
if err != nil {
fmt.Fprintf(stdErr, "初始化缓存目录失败: %v\n", err)
return 1
}
httpClient := server.NewUpstreamClient(cfg)
proxyHandler := proxy.NewHandler(httpClient, logger, store)
fields := logging.BaseFields("startup", opts.configPath)
fields["hubs"] = len(cfg.Hubs)
fields["listen_port"] = cfg.Global.ListenPort
fields["credentials"] = config.CredentialModes(cfg.Hubs)
fields["version"] = version.Full()
logger.WithFields(fields).Info("配置加载完成")
if err := startHTTPServer(cfg, registry, proxyHandler, logger); err != nil {
fmt.Fprintf(stdErr, "HTTP 服务启动失败: %v\n", err)
return 1
}
return 0
}
// parseCLIFlags 解析 CLI 参数,并结合环境变量计算最终的配置路径。
func parseCLIFlags(args []string) (cliOptions, error) {
fs := flag.NewFlagSet("any-hub", flag.ContinueOnError)
fs.SetOutput(io.Discard)
var (
configFlag string
checkOnly bool
showVer bool
)
fs.StringVar(&configFlag, "config", "", "配置文件路径(默认 ./config.toml可被 ANY_HUB_CONFIG 覆盖)")
fs.BoolVar(&checkOnly, "check-config", false, "仅校验配置后退出")
fs.BoolVar(&showVer, "version", false, "显示版本信息")
if err := fs.Parse(args); err != nil {
return cliOptions{}, fmt.Errorf("解析参数失败: %w", err)
}
path := os.Getenv("ANY_HUB_CONFIG")
if configFlag != "" {
path = configFlag
}
if path == "" {
path = "config.toml"
}
return cliOptions{
configPath: path,
checkOnly: checkOnly,
showVersion: showVer,
}, nil
}
func startHTTPServer(cfg *config.Config, registry *server.HubRegistry, proxyHandler server.ProxyHandler, logger *logrus.Logger) error {
port := cfg.Global.ListenPort
app, err := server.NewApp(server.AppOptions{
Logger: logger,
Registry: registry,
Proxy: proxyHandler,
ListenPort: port,
})
if err != nil {
return err
}
logger.WithFields(logrus.Fields{
"action": "listen",
"port": port,
}).Info("Fiber 服务启动")
return app.Listen(fmt.Sprintf(":%d", port))
}

54
main_test.go Normal file
View File

@@ -0,0 +1,54 @@
package main
import (
"bytes"
"strings"
"testing"
)
func TestParseCLIFlagsPriority(t *testing.T) {
t.Setenv("ANY_HUB_CONFIG", "/tmp/env.toml")
opts, err := parseCLIFlags([]string{})
if err != nil {
t.Fatalf("解析失败: %v", err)
}
if opts.configPath != "/tmp/env.toml" {
t.Fatalf("应优先使用环境变量,得到 %s", opts.configPath)
}
opts, err = parseCLIFlags([]string{"--config", "/tmp/flag.toml"})
if err != nil {
t.Fatalf("解析失败: %v", err)
}
if opts.configPath != "/tmp/flag.toml" {
t.Fatalf("flag 应高于环境变量,得到 %s", opts.configPath)
}
}
func TestRunCheckConfigSuccess(t *testing.T) {
useBufferWriters(t)
code := run(cliOptions{configPath: configFixture(t, "valid.toml"), checkOnly: true})
if code != 0 {
t.Fatalf("期望退出码 0得到 %d", code)
}
}
func TestRunCheckConfigFailure(t *testing.T) {
useBufferWriters(t)
code := run(cliOptions{configPath: configFixture(t, "missing.toml"), checkOnly: true})
if code == 0 {
t.Fatalf("无效配置应返回非零退出码")
}
}
func TestRunVersionOutput(t *testing.T) {
useBufferWriters(t)
code := run(cliOptions{showVersion: true})
if code != 0 {
t.Fatalf("version 模式应成功退出,得到 %d", code)
}
if !strings.Contains(stdOut.(*bytes.Buffer).String(), "any-hub") {
t.Fatalf("version 输出应包含 any-hub 标识")
}
}

38
main_test_helpers.go Normal file
View File

@@ -0,0 +1,38 @@
package main
import (
"bytes"
"testing"
)
// useBufferWriters swaps stdOut/stdErr with in-memory buffers for the duration
// of a test, allowing assertions on CLI output without polluting test logs.
func useBufferWriters(t *testing.T) {
t.Helper()
outBuf := &bytes.Buffer{}
errBuf := &bytes.Buffer{}
prevOut := stdOut
prevErr := stdErr
stdOut = outBuf
stdErr = errBuf
t.Cleanup(func() {
stdOut = prevOut
stdErr = prevErr
})
}
// stdOutBuffer returns the in-use stdout buffer when useBufferWriters is active.
func stdOutBuffer() *bytes.Buffer {
buf, _ := stdOut.(*bytes.Buffer)
return buf
}
// stdErrBuffer returns the in-use stderr buffer when useBufferWriters is active.
func stdErrBuffer() *bytes.Buffer {
buf, _ := stdErr.(*bytes.Buffer)
return buf
}

View File

@@ -10,4 +10,4 @@ if [[ ! -f "${CONFIG}" ]]; then
fi fi
echo "Starting any-hub with ${CONFIG}" echo "Starting any-hub with ${CONFIG}"
exec go run ./cmd/any-hub --config "${CONFIG}" exec go run . --config "${CONFIG}"

29
test_helpers_test.go Normal file
View File

@@ -0,0 +1,29 @@
package main
import (
"path/filepath"
"runtime"
"testing"
)
var repoRoot string
func init() {
_, file, _, ok := runtime.Caller(0)
if ok {
repoRoot = filepath.Join(filepath.Dir(file), "..", "..")
}
}
func projectRoot(t *testing.T) string {
t.Helper()
if repoRoot == "" {
t.Fatal("无法定位项目根目录")
}
return repoRoot
}
func configFixture(t *testing.T, name string) string {
t.Helper()
return filepath.Join(projectRoot(t), "internal", "config", "testdata", name)
}

12
version.go Normal file
View File

@@ -0,0 +1,12 @@
package main
import (
"fmt"
"github.com/any-hub/any-hub/internal/version"
)
// printVersion 输出注入的版本 + 提交信息。
func printVersion() {
fmt.Fprintln(stdOut, version.Full())
}