diff --git a/.github/workflows/dev.yaml b/.github/workflows/dev.yaml new file mode 100644 index 0000000..d5bd85d --- /dev/null +++ b/.github/workflows/dev.yaml @@ -0,0 +1,37 @@ +name: Dev Build +on: + push: + branches: [ dev ] + +env: + APP_NAME: backend + +jobs: + docker: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@master + + - name: Set up QEMU + uses: docker/setup-qemu-action@master + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@master + + - name: Login to DockerHub + uses: docker/login-action@master + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push + id: docker_build + uses: docker/build-push-action@v3 + with: + push: true + tags: | + ${{ secrets.DOCKERHUB_USERNAME }}/${{ env.APP_NAME }}:latest + ${{ secrets.DOCKERHUB_USERNAME }}/${{ env.APP_NAME }}:dev + registry.cn-shanghai.aliyuncs.com/${{ secrets.ACR_NAMESPACE }}/${{ env.APP_NAME }}:latest + registry.cn-shanghai.aliyuncs.com/${{ secrets.ACR_NAMESPACE }}/${{ env.APP_NAME }}:dev \ No newline at end of file diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml new file mode 100644 index 0000000..dd087ca --- /dev/null +++ b/.github/workflows/main.yaml @@ -0,0 +1,38 @@ +name: Dev Build +on: + push: + branches: [ main ] + +env: + APP_NAME: backend + +jobs: + docker: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@master + + - name: Set up QEMU + uses: docker/setup-qemu-action@master + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@master + + - name: Login to DockerHub + uses: docker/login-action@master + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push + id: docker_build + uses: docker/build-push-action@v3 + with: + push: true + tags: | + ${{ secrets.DOCKERHUB_USERNAME }}/${{ env.APP_NAME }}:latest + ${{ secrets.DOCKERHUB_USERNAME }}/${{ env.APP_NAME }}:master + registry.cn-shanghai.aliyuncs.com/${{ secrets.ACR_NAMESPACE }}/${{ env.APP_NAME }}:latest + registry.cn-shanghai.aliyuncs.com/${{ secrets.ACR_NAMESPACE }}/${{ env.APP_NAME }}:master + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..4b8a38e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,28 @@ +FROM golang:1.21-alpine as builder + +WORKDIR /app + +COPY go.mod go.sum ./ +RUN apk add --no-cache --virtual .build-deps \ + ca-certificates \ + gcc \ + g++ && \ + go mod download + +COPY . . + +RUN go build -o treehole -ldflags "-s -w" ./cmd + +FROM alpine + +WORKDIR /app + +COPY --from=builder /app/treehole /app/ +COPY --from=builder /app/config/config_default.json /app/config/ +VOLUME ["/app/data", "/app/config"] + +ENV TZ=Asia/Shanghai MODE=production LOG_LEVEL=info PORT=8000 + +EXPOSE 8000 + +ENTRYPOINT ["./treehole"] \ No newline at end of file diff --git a/README.md b/README.md index 10f5ba9..d85a1cc 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,79 @@ nunu run API 文档详见启动项目之后的 http://localhost:8000/docs +### 生产部署 + +#### 使用 docker 部署 + +```shell +docker run -d \ + --name opentreehole_backend \ + -p 8000:8000 \ + -e MODULES_CURRICULUM_BOARD=true \ + -v opentreehole_data:/app/data \ + -v opentreehole_config:/app/config \ + opentreehole/backend:latest +``` + +#### 使用 docker-compose 部署 + +```yaml +version: '3' + +services: + backend: + image: opentreehole/backend:latest + restart: unless-stopped + environment: + - DB_TYPE=mysql + - DB_URL=opentreehole:${MYSQL_PASSWORD}@tcp(mysql:3306)/opentreehole?parseTime=true&loc=Asia%2FShanghai + - CACHE_TYPE=redis + - CACHE_URL=redis:6379 + - MODULES_CURRICULUM_BOARD=true + volumes: + - data:/app/data + - config:/app/config + + mysql: + image: mysql:8.0.34 + restart: always + environment: + - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD} + - MYSQL_USER=opentreehole + - MYSQL_PASSWORD=${MYSQL_PASSWORD} + - MYSQL_DATABASE=opentreehole + - TZ=Asia/Shanghai + volumes: + - mysql_data:/var/lib/mysql + - mysql_config:/etc/mysql/conf.d + + redis: + container_name: fduhole_redis + image: redis:7.0.11-alpine + restart: always + +volumes: + data: + config: +``` + +环境变量: + +1. `TZ`: 生产环境默认 `Asia/Shanghai` +2. `MODE`: 开发环境默认 `dev`, 生产环境默认 `production`, 可选 `test`, `bench` +3. `LOG_LEVEL`: 开发环境默认 `debug`, 生产环境默认 `info`, 可选 `warn`, `error`, `panic`, `fatal` +4. `PORT`: 默认 8000 +5. `DB_TYPE`: 默认 `sqlite`, 可选 `mysql`, `postgres` +6. `DB_DSN`: 默认 `data/sqlite.db` +7. `MODULES_{AUTH/NOTIFICATION/TREEHOLE/CURRICULUM_BOARD}`: 开启模块,默认为 `false` + +数据卷: + +1. `/app/data`: 数据库文件存放位置 +2. `/app/config`: 配置文件存放位置 + +注:环境变量设置仅在程序启动时生效,后续可以修改 `config/config.json` 动态修改配置 + ### 开发指南 1. 使用 wire 作为依赖注入框架。如果创建了新的依赖项,需要在 `cmd/wire/wire.go` 中注册依赖项的构造函数,之后运行 `nunu wire` diff --git a/go.mod b/go.mod index 2f82c38..b58e435 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ toolchain go1.21.0 require ( github.com/allegro/bigcache/v3 v3.1.0 + github.com/caarlos0/env/v9 v9.0.0 github.com/creasty/defaults v1.7.0 github.com/eko/gocache/lib/v4 v4.1.5 github.com/eko/gocache/store/bigcache/v4 v4.2.1 @@ -14,14 +15,17 @@ require ( github.com/goccy/go-json v0.10.2 github.com/gofiber/fiber/v2 v2.49.2 github.com/gofiber/swagger v0.1.13 + github.com/golang-jwt/jwt/v5 v5.0.0 github.com/google/wire v0.5.0 github.com/hetiansu5/urlquery v1.2.7 + github.com/jinzhu/copier v0.4.0 github.com/redis/go-redis/v9 v9.2.0 github.com/rs/zerolog v1.30.0 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.8.4 github.com/swaggo/swag v1.16.2 github.com/thanhpk/randstr v1.0.6 + github.com/vmihailenco/msgpack/v5 v5.3.5 go.uber.org/zap v1.26.0 golang.org/x/crypto v0.13.0 golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 @@ -47,14 +51,12 @@ require ( github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-sql-driver/mysql v1.7.1 // indirect - github.com/golang-jwt/jwt/v5 v5.0.0 // indirect github.com/golang/mock v1.6.0 // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/google/uuid v1.3.1 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/pgx/v5 v5.3.1 // indirect - github.com/jinzhu/copier v0.4.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/josharian/intern v1.0.0 // indirect @@ -77,7 +79,6 @@ require ( github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.50.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect - github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect go.uber.org/multierr v1.10.0 // indirect golang.org/x/net v0.15.0 // indirect diff --git a/go.sum b/go.sum index 3f8d5ca..b8b7380 100644 --- a/go.sum +++ b/go.sum @@ -46,8 +46,6 @@ github.com/allegro/bigcache/v3 v3.1.0 h1:H2Vp8VOvxcrB91o86fUSVJFqeuz8kpyyB02eH3b github.com/allegro/bigcache/v3 v3.1.0/go.mod h1:aPyh7jEvrog9zAwx5N7+JUQX5dZTSGpxF1LAR4dr35I= github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= -github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= -github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -56,6 +54,8 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/caarlos0/env/v9 v9.0.0 h1:SI6JNsOA+y5gj9njpgybykATIylrRMklbs5ch6wO6pc= +github.com/caarlos0/env/v9 v9.0.0/go.mod h1:ye5mlCVMYh6tZ+vCgrs/B95sj88cg5Tlnc0XIzgZ020= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -372,8 +372,6 @@ go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.25.0 h1:4Hvk6GtkucQ790dqmj7l1eEnRdKm3k3ZUrUMS2d5+5c= -go.uber.org/zap v1.25.0/go.mod h1:JIAUzQIH94IC4fOJQm7gMmBJP5k7wQfdcnYdPoEXJYk= go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= diff --git a/internal/config/config.go b/internal/config/config.go index bd289ec..c34b170 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -6,6 +6,7 @@ import ( "os" "sync/atomic" + "github.com/caarlos0/env/v9" "github.com/creasty/defaults" "github.com/go-playground/validator/v10" "github.com/spf13/pflag" @@ -13,9 +14,37 @@ import ( "github.com/opentreehole/backend/pkg/utils" ) +type EnvConfig struct { + Mode string `env:"MODE" default:"dev" validate:"oneof=dev production test bench"` + + LogLevel string `env:"LOG_LEVEL" default:"debug" validate:"oneof=debug info warn error dpanic panic fatal"` + + Port int `env:"PORT" default:"8000"` + + DBType string `env:"DB_TYPE" default:"sqlite" validate:"oneof=mysql sqlite postgres memory"` + + DBDSN string `env:"DB_DSN" default:"data/sqlite.db"` + + CacheType string `env:"CACHE_TYPE" default:"memory" validate:"oneof=redis memory"` + + CacheUrl string `env:"CACHE_URL" default:"redis:6379"` + + SearchEngineType string `env:"SEARCH_ENGINE_TYPE" default:"elasticsearch" validate:"oneof=elasticsearch meilisearch"` + + SearchEngineUrl string `env:"SEARCH_ENGINE_URL" default:"http://elasticsearch:9200"` + + ModulesAuth bool `env:"MODULES_AUTH" default:"false"` + + ModulesNotification bool `env:"MODULES_NOTIFICATION" default:"false"` + + ModulesTreehole bool `env:"MODULES_TREEHOLE" default:"false"` + + ModulesCurriculumBoard bool `env:"MODULES_CURRICULUM_BOARD" default:"false"` +} + type Config struct { // app mode: dev, production, test, bench, default is dev - Mode string `yaml:"mode" default:"dev" json:"mode"` + Mode string `yaml:"mode" default:"dev" json:"mode" validate:"oneof=dev production test bench"` // LogLevel is the log level, default is debug LogLevel string `yaml:"log_level" default:"debug" json:"log_level" validate:"oneof=debug info warn error dpanic panic fatal"` @@ -217,12 +246,16 @@ func NewConfig() *AtomicAllConfig { defaultIdentifierSaltFile, "identifier salt file path", ) + pflag.Parse() + + // get env config + envConfig := GetEnvConfig() // read config from file - err = config.ReadFromFile(configFilename) - if err != nil { - panic(err) - } + config.ReadFromFile(configFilename) + + // copy env config to config + CopyEnvConfigToConfig(envConfig, &config) err = validator.New().Struct(&config) if err != nil { @@ -230,25 +263,24 @@ func NewConfig() *AtomicAllConfig { } // save config - err = config.WriteIntoFile(configFilename) - if err != nil { - panic(err) - } + config.WriteIntoFile(configFilename) - // parse identifier salt from file - identifierSaltBytes, err := os.ReadFile(identifierSaltFilename) - if err != nil { - if os.IsNotExist(err) { - if config.Mode == "production" { - panic("identifier salt file not found") - } else { - fileConfig.DecryptedIdentifierSalt = []byte("123456") - } - } - } else { - fileConfig.DecryptedIdentifierSalt, err = base64.StdEncoding.DecodeString(utils.BytesToString(identifierSaltBytes)) + if config.Modules.Auth { + // parse identifier salt from file + identifierSaltBytes, err := os.ReadFile(identifierSaltFilename) if err != nil { - panic("decode identifier salt error") + if os.IsNotExist(err) { + if config.Mode == "production" { + panic("identifier salt file not found") + } else { + fileConfig.DecryptedIdentifierSalt = []byte("123456") + } + } + } else { + fileConfig.DecryptedIdentifierSalt, err = base64.StdEncoding.DecodeString(utils.BytesToString(identifierSaltBytes)) + if err != nil { + panic("decode identifier salt error") + } } } @@ -259,20 +291,32 @@ func NewConfig() *AtomicAllConfig { return &allConfig } +func GetEnvConfig() *EnvConfig { + var envConfig EnvConfig + err := env.Parse(&envConfig) + if err != nil { + panic(err) + } + + defer defaults.MustSet(&envConfig) + + return &envConfig +} + // ReadFromFile read config from file // if file not exist, create it with default value; else read it -func (config *Config) ReadFromFile(name string) (err error) { +func (config *Config) ReadFromFile(name string) { var file *os.File // set default value finally defer defaults.MustSet(config) - file, err = os.Open(name) + file, err := os.Open(name) if err != nil { if os.IsNotExist(err) { - return nil + return } - return + panic(err) } defer func(file *os.File) { @@ -284,20 +328,16 @@ func (config *Config) ReadFromFile(name string) (err error) { err = json.NewDecoder(file).Decode(config) if err != nil { - return + panic(err) } - - return } // WriteIntoFile write config into file // if file not exist, create it; else truncate it -func (config *Config) WriteIntoFile(name string) (err error) { - var file *os.File - - file, err = os.OpenFile(name, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0666) +func (config *Config) WriteIntoFile(name string) { + file, err := os.OpenFile(name, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0666) if err != nil { - return + panic(err) } defer func(file *os.File) { @@ -311,8 +351,23 @@ func (config *Config) WriteIntoFile(name string) (err error) { encoder.SetIndent("", " ") err = encoder.Encode(config) if err != nil { - return + panic(err) } - return +} + +func CopyEnvConfigToConfig(envConfig *EnvConfig, config *Config) { + config.Mode = envConfig.Mode + config.LogLevel = envConfig.LogLevel + config.Port = envConfig.Port + config.DB.Type = envConfig.DBType + config.DB.DSN = envConfig.DBDSN + config.Cache.Type = envConfig.CacheType + config.Cache.Url = envConfig.CacheUrl + config.SearchEngine.Type = envConfig.SearchEngineType + config.SearchEngine.Url = envConfig.SearchEngineUrl + config.Modules.Auth = envConfig.ModulesAuth + config.Modules.Notification = envConfig.ModulesNotification + config.Modules.Treehole = envConfig.ModulesTreehole + config.Modules.CurriculumBoard = envConfig.ModulesCurriculumBoard } diff --git a/internal/handler/course.go b/internal/handler/course.go index c622457..046a725 100644 --- a/internal/handler/course.go +++ b/internal/handler/course.go @@ -1,8 +1,6 @@ package handler import ( - "fmt" - "github.com/gofiber/fiber/v2" "github.com/opentreehole/backend/internal/repository" @@ -56,11 +54,10 @@ func (h *courseHandler) ListCoursesV1(c *fiber.Ctx) (err error) { //ctx := context.WithValue(c.Context(), "FiberCtx", c) c.Context().SetUserValue("FiberCtx", c) - user, err := h.accountRepository.GetCurrentUser(c.Context()) + _, err = h.accountRepository.GetCurrentUser(c.Context()) if err != nil { return err } - fmt.Printf("%+v", *user) response, err := h.courseService.ListCoursesV1(c.Context()) if err != nil {