refactor: 统一 DB/Redis 配置为 DSN 模式 + 移除 Helm/K8s 部署
- DatabaseConfig: Host/Port/User/Password/Name/DBName/SSLMode → 单个 DSN 字段 - RedisConfig: Host/Port/Password/DB/URL → 单个 DSN 字段 - 环境变量: GOCHAT_DATABASE_* (7个) → GOCHAT_DATABASE_DSN, GOCHAT_REDIS_* (5个) → GOCHAT_REDIS_DSN - validator.go: DSN URL 解析校验 (scheme + host) - redis.go: redis.ParseURL(cfg.DSN) 直连 - 所有 docker-compose / CI / shell 脚本 / .env 同步更新 - 删除 deploy/helm/ 整个目录 (20个文件) - CI 删除 helm-validate / deploy-staging / deploy-production 三个 job - 文档同步更新 (README, 架构设计, PRD, 滚动升级)
This commit is contained in:
+2
-7
@@ -12,18 +12,13 @@ GOCHAT_SERVER_PORT=3000
|
||||
GOCHAT_SERVER_MODE=release # debug, release, test
|
||||
|
||||
# ---- Database ----
|
||||
GOCHAT_DATABASE_HOST=localhost
|
||||
GOCHAT_DATABASE_PORT=5432
|
||||
GOCHAT_DATABASE_USER=gochat
|
||||
GOCHAT_DATABASE_PASSWORD=CHANGE_ME_TO_A_STRONG_PASSWORD
|
||||
GOCHAT_DATABASE_DBNAME=gochat_production
|
||||
GOCHAT_DATABASE_SSLMODE=disable
|
||||
GOCHAT_DATABASE_DSN=postgres://gochat:CHANGE_ME_TO_A_STRONG_PASSWORD@localhost:5432/gochat_production?sslmode=disable
|
||||
GOCHAT_DATABASE_MAX_IDLE_CONNS=10
|
||||
GOCHAT_DATABASE_MAX_OPEN_CONNS=100
|
||||
GOCHAT_DATABASE_CONN_MAX_LIFETIME=3600
|
||||
|
||||
# ---- Redis ----
|
||||
GOCHAT_REDIS_URL=redis://localhost:6379
|
||||
GOCHAT_REDIS_DSN=redis://localhost:6379
|
||||
GOCHAT_REDIS_POOL_SIZE=50
|
||||
|
||||
# ---- JWT ----
|
||||
|
||||
+6
-108
@@ -68,13 +68,8 @@ jobs:
|
||||
env:
|
||||
GOCHAT_ENV: test
|
||||
GOCHAT_TEST_DB: ${{ matrix.db-mode }}
|
||||
POSTGRES_HOST: localhost
|
||||
POSTGRES_PORT: 5432
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: gochat_test
|
||||
REDIS_HOST: localhost
|
||||
REDIS_PORT: 6379
|
||||
GOCHAT_DATABASE_DSN: postgres://postgres:postgres@localhost:5432/gochat_test?sslmode=disable
|
||||
GOCHAT_REDIS_DSN: redis://localhost:6379
|
||||
GOPROXY: https://goproxy.cn,direct
|
||||
run: go test -v -race -coverprofile=coverage.out -timeout 180s ./internal/... ./pkg/... ./cmd/...
|
||||
|
||||
@@ -84,13 +79,8 @@ jobs:
|
||||
working-directory: backend
|
||||
env:
|
||||
GOCHAT_ENV: test
|
||||
POSTGRES_HOST: localhost
|
||||
POSTGRES_PORT: 5432
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: gochat_test
|
||||
REDIS_HOST: localhost
|
||||
REDIS_PORT: 6379
|
||||
GOCHAT_DATABASE_DSN: postgres://postgres:postgres@localhost:5432/gochat_test?sslmode=disable
|
||||
GOCHAT_REDIS_DSN: redis://localhost:6379
|
||||
run: go test -v -timeout 120s ./tests/e2e/...
|
||||
|
||||
# Benchmark (quick sanity check, not full bench)
|
||||
@@ -99,11 +89,7 @@ jobs:
|
||||
env:
|
||||
GOCHAT_ENV: test
|
||||
GOCHAT_TEST_DB: ${{ matrix.db-mode }}
|
||||
POSTGRES_HOST: localhost
|
||||
POSTGRES_PORT: 5432
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: gochat_test
|
||||
GOCHAT_DATABASE_DSN: postgres://postgres:postgres@localhost:5432/gochat_test?sslmode=disable
|
||||
run: go test -bench=. -benchtime=1s -run=^$ -timeout 60s ./internal/service/... ./pkg/crypto/...
|
||||
|
||||
# Coverage report
|
||||
@@ -210,92 +196,4 @@ jobs:
|
||||
image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
|
||||
format: 'table'
|
||||
exit-code: '1'
|
||||
severity: 'CRITICAL,HIGH'
|
||||
|
||||
# ---- Stage 4: Validate Helm Chart ----
|
||||
helm-validate:
|
||||
name: Validate Helm Chart
|
||||
needs: test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Helm
|
||||
uses: azure/setup-helm@v3
|
||||
with:
|
||||
version: 'v3.14.0'
|
||||
|
||||
- name: Set up kubeval
|
||||
run: |
|
||||
curl -sfL https://github.com/yannh/kubeconform/releases/download/v0.6.4/kubeconform-linux-amd64.tar.gz | tar -xz
|
||||
chmod +x kubeconform && mv kubeconform /usr/local/bin/
|
||||
|
||||
- name: Lint Helm chart
|
||||
run: helm lint deploy/helm/gochat/
|
||||
|
||||
- name: Template and validate Helm chart
|
||||
run: |
|
||||
helm template gochat deploy/helm/gochat/ \
|
||||
--values deploy/helm/gochat/values.yaml \
|
||||
--values deploy/helm/gochat/values-production.yaml \
|
||||
| kubeconform -summary -kubernetes-version 1.29.0
|
||||
|
||||
# ---- Stage 5: Deploy to Staging ----
|
||||
deploy-staging:
|
||||
name: Deploy to Staging
|
||||
needs: [build, helm-validate]
|
||||
runs-on: ubuntu-latest
|
||||
if: github.ref == 'refs/heads/develop'
|
||||
environment: staging
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Deploy to staging
|
||||
uses: stefanprodan/helm-gh-action@v1.7.0
|
||||
with:
|
||||
context: ./deploy/helm/gochat
|
||||
chart-ref: ./deploy/helm/gochat
|
||||
cluster-config: staging-k8s-config
|
||||
kube-config: ${{ secrets.KUBE_CONFIG_STAGING }}
|
||||
namespace: gochat-staging
|
||||
values: values.yaml
|
||||
value-files: >-
|
||||
values-staging.yaml
|
||||
release-name: gochat-staging
|
||||
atomic: true
|
||||
|
||||
# Post-deploy health check
|
||||
- name: Health check
|
||||
run: |
|
||||
chmod +x backend/scripts/health_check.sh
|
||||
GOCHAT_HOST=gochat-staging GOCHAT_PORT=3000 ./backend/scripts/health_check.sh --full --timeout 30
|
||||
|
||||
# ---- Stage 6: Deploy to Production ----
|
||||
deploy-production:
|
||||
name: Deploy to Production
|
||||
needs: [build, helm-validate]
|
||||
runs-on: ubuntu-latest
|
||||
if: startsWith(github.ref, 'refs/heads/release/')
|
||||
environment: production
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Deploy to production
|
||||
uses: stefanprodan/helm-gh-action@v1.7.0
|
||||
with:
|
||||
context: ./deploy/helm/gochat
|
||||
chart-ref: ./deploy/helm/gochat
|
||||
cluster-config: production-k8s-config
|
||||
kube-config: ${{ secrets.KUBE_CONFIG_PRODUCTION }}
|
||||
namespace: gochat-production
|
||||
values: values.yaml
|
||||
value-files: >-
|
||||
values-production.yaml
|
||||
release-name: gochat-production
|
||||
atomic: true
|
||||
|
||||
# Post-deploy health check
|
||||
- name: Health check
|
||||
run: |
|
||||
chmod +x backend/scripts/health_check.sh
|
||||
GOCHAT_HOST=gochat-production GOCHAT_PORT=3000 ./backend/scripts/health_check.sh --full --timeout 30
|
||||
severity: 'CRITICAL,HIGH'
|
||||
@@ -185,13 +185,11 @@ PG-only功能(vector搜索等)使用 `skipIfSQLite` 自动跳过。
|
||||
|
||||
## CI/CD
|
||||
|
||||
GitHub Actions 5阶段流水线:
|
||||
GitHub Actions 3阶段流水线:
|
||||
|
||||
1. **Test & Lint**: lint + vet + 单元/集成/e2e/benchmark(PG+SQLite矩阵)
|
||||
2. **Security Scan**: gosec + govulncheck + Trivy
|
||||
3. **Build**: Docker多平台镜像 + GHCR推送
|
||||
4. **Helm Validate**: K8s Helm chart lint + kubeconform
|
||||
5. **Deploy**: Staging(develop分支)+ Production(release分支)
|
||||
|
||||
详见 [.github/workflows/ci.yml](.github/workflows/ci.yml)
|
||||
|
||||
@@ -211,7 +209,7 @@ GitHub Actions 5阶段流水线:
|
||||
| 限流 | golang.org/x/time/rate |
|
||||
| LLM | OpenAI / Ollama / volcengine |
|
||||
| 容器 | Docker + Buildx |
|
||||
| K8s | Helm chart |
|
||||
| 部署 | Docker Compose |
|
||||
|
||||
## 文档
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ server:
|
||||
mode: "debug"
|
||||
|
||||
database:
|
||||
name: "gochat_dev"
|
||||
dsn: "postgres://postgres:xiha02@localhost:5444/gochat_dev?sslmode=disable"
|
||||
log_level: "info"
|
||||
|
||||
log:
|
||||
|
||||
@@ -14,7 +14,7 @@ server:
|
||||
max_age: 86400
|
||||
|
||||
database:
|
||||
name: "gochat_production"
|
||||
dsn: "postgres://gochat:CHANGE_ME@localhost:5432/gochat_production?sslmode=require"
|
||||
pool_max: 20
|
||||
log_level: "warn"
|
||||
|
||||
|
||||
@@ -14,13 +14,7 @@ server:
|
||||
max_age: 86400 # preflight cache duration in seconds
|
||||
|
||||
database:
|
||||
host: "localhost"
|
||||
port: 5444
|
||||
user: "postgres"
|
||||
password: "xiha02"
|
||||
name: "gochat_dev"
|
||||
dbname: "gochat_dev"
|
||||
sslmode: "disable"
|
||||
dsn: "postgres://postgres:xiha02@localhost:5444/gochat_dev?sslmode=disable"
|
||||
max_idle_conns: 10
|
||||
max_open_conns: 100
|
||||
conn_max_lifetime: 3600 # seconds
|
||||
@@ -28,11 +22,7 @@ database:
|
||||
migrations_path: "migrations"
|
||||
|
||||
redis:
|
||||
url: "redis://:xiha02@localhost:6397/0"
|
||||
host: "localhost"
|
||||
port: 6397
|
||||
password: "xiha02"
|
||||
db: 0
|
||||
dsn: "redis://:xiha02@localhost:6397/0"
|
||||
pool_size: 50
|
||||
|
||||
jwt:
|
||||
|
||||
@@ -142,7 +142,7 @@ func initDB(cfg *config.Config) (*gorm.DB, error) {
|
||||
logger.Config{LogLevel: logger.Info},
|
||||
)
|
||||
|
||||
db, err := gorm.Open(postgres.Open(cfg.Database.DSN()), &gorm.Config{
|
||||
db, err := gorm.Open(postgres.Open(cfg.Database.DSN), &gorm.Config{
|
||||
Logger: gormLogger,
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
// NewDatabase creates a GORM database connection.
|
||||
// Pattern follows Chatwoot's ActiveRecord PostgreSQL setup in config/database.yml
|
||||
func NewDatabase(cfg *config.DatabaseConfig, logLevel string) (*gorm.DB, error) {
|
||||
dsn := cfg.DSN()
|
||||
dsn := cfg.DSN
|
||||
|
||||
// Map log level to GORM logger level
|
||||
var gormLogLevel gormlogger.LogLevel
|
||||
@@ -54,8 +54,7 @@ func NewDatabase(cfg *config.DatabaseConfig, logLevel string) (*gorm.DB, error)
|
||||
// Set default statement timeout (ref: Chatwoot POSTGRES_STATEMENT_TIMEOUT)
|
||||
db.Set("gorm:statement_timeout", "5000ms")
|
||||
|
||||
applogger.L().Infof("Connected to PostgreSQL: %s@%s:%d/%s (pool=%d)",
|
||||
cfg.User, cfg.Host, cfg.Port, cfg.DBName, cfg.MaxOpenConns)
|
||||
applogger.L().Infof("Connected to PostgreSQL (pool=%d)", cfg.MaxOpenConns)
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
@@ -3,8 +3,6 @@ package app
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gochat/gochat/internal/config"
|
||||
@@ -15,24 +13,11 @@ import (
|
||||
// NewRedisClient creates a Redis client for caching, Pub/Sub, and sessions.
|
||||
// Reference: Chatwoot config/cable.yml Redis adapter configuration
|
||||
func NewRedisClient(cfg *config.RedisConfig) (*redis.Client, error) {
|
||||
var opts *redis.Options
|
||||
var err error
|
||||
if cfg.Host != "" && cfg.Port > 0 {
|
||||
opts = &redis.Options{
|
||||
Addr: fmt.Sprintf("%s:%d", cfg.Host, cfg.Port),
|
||||
Password: cfg.Password,
|
||||
DB: cfg.DB,
|
||||
}
|
||||
} else {
|
||||
opts, err = parseRedisURL(cfg.URL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse Redis URL: %w", err)
|
||||
}
|
||||
opts, err := redis.ParseURL(cfg.DSN)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse Redis DSN: %w", err)
|
||||
}
|
||||
|
||||
if cfg.Password != "" {
|
||||
opts.Password = cfg.Password
|
||||
}
|
||||
poolSize := cfg.PoolSize
|
||||
if poolSize <= 0 {
|
||||
poolSize = 50
|
||||
@@ -53,24 +38,3 @@ func NewRedisClient(cfg *config.RedisConfig) (*redis.Client, error) {
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// parseRedisURL parses a Redis URL into redis.Options.
|
||||
// Handles both "redis://host:port" and "redis://user:password@host:port/db" formats.
|
||||
func parseRedisURL(urlStr string) (*redis.Options, error) {
|
||||
opts, err := redis.ParseURL(urlStr)
|
||||
if err != nil {
|
||||
// Fallback: simple host:port parsing
|
||||
parts := strings.Split(strings.TrimPrefix(urlStr, "redis://"), ":")
|
||||
if len(parts) < 2 {
|
||||
return nil, fmt.Errorf("invalid Redis URL format: %s", urlStr)
|
||||
}
|
||||
port, err := strconv.Atoi(parts[1])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid Redis port: %s", parts[1])
|
||||
}
|
||||
opts = &redis.Options{
|
||||
Addr: fmt.Sprintf("%s:%d", parts[0], port),
|
||||
}
|
||||
}
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strconv"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/alicebob/miniredis/v2"
|
||||
@@ -10,18 +9,12 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewRedisClientPrefersHostAndPortOverURL(t *testing.T) {
|
||||
func TestNewRedisClientFromDSN(t *testing.T) {
|
||||
server := miniredis.RunT(t)
|
||||
host, portText, err := net.SplitHostPort(server.Addr())
|
||||
require.NoError(t, err)
|
||||
port, err := strconv.Atoi(portText)
|
||||
require.NoError(t, err)
|
||||
|
||||
dsn := fmt.Sprintf("redis://%s/0", server.Addr())
|
||||
client, err := NewRedisClient(&config.RedisConfig{
|
||||
URL: "redis://localhost:6397/0",
|
||||
Host: host,
|
||||
Port: port,
|
||||
DB: 0,
|
||||
DSN: dsn,
|
||||
PoolSize: 2,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -95,13 +95,9 @@ type CORSConfig struct {
|
||||
}
|
||||
|
||||
type DatabaseConfig struct {
|
||||
Host string `mapstructure:"host"`
|
||||
Port int `mapstructure:"port"`
|
||||
User string `mapstructure:"user"`
|
||||
Password string `mapstructure:"password"`
|
||||
Name string `mapstructure:"name"`
|
||||
DBName string `mapstructure:"dbname"`
|
||||
SSLMode string `mapstructure:"sslmode"`
|
||||
// DSN is the full PostgreSQL connection string, e.g.
|
||||
// "postgres://user:password@host:port/dbname?sslmode=disable"
|
||||
DSN string `mapstructure:"dsn"`
|
||||
MaxIdleConns int `mapstructure:"max_idle_conns"`
|
||||
MaxOpenConns int `mapstructure:"max_open_conns"`
|
||||
ConnMaxLifetime int `mapstructure:"conn_max_lifetime"` // seconds
|
||||
@@ -109,24 +105,11 @@ type DatabaseConfig struct {
|
||||
MigrationsPath string `mapstructure:"migrations_path"` // path to migration files (default: "migrations")
|
||||
}
|
||||
|
||||
func (d DatabaseConfig) DSN() string {
|
||||
dbname := d.DBName
|
||||
if dbname == "" {
|
||||
dbname = d.Name
|
||||
}
|
||||
return fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
|
||||
d.Host, d.Port, d.User, d.Password, dbname, d.SSLMode)
|
||||
}
|
||||
|
||||
// MigrateDSN returns a PostgreSQL connection URL in the format
|
||||
// expected by golang-migrate: postgres://user:password@host:port/dbname?sslmode=mode
|
||||
// MigrateDSN returns the connection string for golang-migrate.
|
||||
// Same as DSN — both GORM (pgx) and golang-migrate accept the
|
||||
// "postgres://user:password@host:port/dbname?sslmode=mode" URL format.
|
||||
func (d DatabaseConfig) MigrateDSN() string {
|
||||
dbname := d.DBName
|
||||
if dbname == "" {
|
||||
dbname = d.Name
|
||||
}
|
||||
return fmt.Sprintf("postgres://%s:%s@%s:%d/%s?sslmode=%s",
|
||||
d.User, d.Password, d.Host, d.Port, dbname, d.SSLMode)
|
||||
return d.DSN
|
||||
}
|
||||
|
||||
// GetMigrationsPath returns the migrations path, defaulting to "migrations" if not set.
|
||||
@@ -138,11 +121,10 @@ func (d DatabaseConfig) GetMigrationsPath() string {
|
||||
}
|
||||
|
||||
type RedisConfig struct {
|
||||
Host string `mapstructure:"host"`
|
||||
Port int `mapstructure:"port"`
|
||||
Password string `mapstructure:"password"`
|
||||
DB int `mapstructure:"db"`
|
||||
URL string `mapstructure:"url"`
|
||||
// DSN is the full Redis connection string, e.g.
|
||||
// "redis://:password@host:port/db"
|
||||
// "rediss://host:port" (TLS)
|
||||
DSN string `mapstructure:"dsn"`
|
||||
PoolSize int `mapstructure:"pool_size"`
|
||||
}
|
||||
|
||||
@@ -469,7 +451,7 @@ func (r *ConfigReloader) Stop() {
|
||||
func LoadWithEnv(env string) (*Config, error) {
|
||||
v := viper.New()
|
||||
|
||||
// Env key replacer: GOCHAT_DATABASE_HOST → database.host
|
||||
// Env key replacer: GOCHAT_DATABASE_DSN → database.dsn
|
||||
v.SetEnvPrefix("GOCHAT")
|
||||
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||
v.AutomaticEnv()
|
||||
@@ -480,23 +462,13 @@ func LoadWithEnv(env string) (*Config, error) {
|
||||
"GOCHAT_SERVER_HOST": "server.host",
|
||||
"GOCHAT_SERVER_PORT": "server.port",
|
||||
"GOCHAT_SERVER_MODE": "server.mode",
|
||||
"GOCHAT_DATABASE_HOST": "database.host",
|
||||
"GOCHAT_DATABASE_PORT": "database.port",
|
||||
"GOCHAT_DATABASE_USER": "database.user",
|
||||
"GOCHAT_DATABASE_PASSWORD": "database.password",
|
||||
"GOCHAT_DATABASE_NAME": "database.name",
|
||||
"GOCHAT_DATABASE_DBNAME": "database.dbname",
|
||||
"GOCHAT_DATABASE_SSLMODE": "database.sslmode",
|
||||
"GOCHAT_DATABASE_DSN": "database.dsn",
|
||||
"GOCHAT_DATABASE_MAX_IDLE_CONNS": "database.max_idle_conns",
|
||||
"GOCHAT_DATABASE_MAX_OPEN_CONNS": "database.max_open_conns",
|
||||
"GOCHAT_DATABASE_CONN_MAX_LIFETIME": "database.conn_max_lifetime",
|
||||
"GOCHAT_DATABASE_RUN_MIGRATIONS": "database.run_migrations",
|
||||
"GOCHAT_DATABASE_MIGRATIONS_PATH": "database.migrations_path",
|
||||
"GOCHAT_REDIS_URL": "redis.url",
|
||||
"GOCHAT_REDIS_HOST": "redis.host",
|
||||
"GOCHAT_REDIS_PORT": "redis.port",
|
||||
"GOCHAT_REDIS_PASSWORD": "redis.password",
|
||||
"GOCHAT_REDIS_DB": "redis.db",
|
||||
"GOCHAT_REDIS_DSN": "redis.dsn",
|
||||
"GOCHAT_REDIS_POOL_SIZE": "redis.pool_size",
|
||||
"GOCHAT_JWT_SECRET": "jwt.secret",
|
||||
"JWT_SECRET": "jwt.secret", // Alias for compatibility (no prefix)
|
||||
@@ -672,18 +644,12 @@ func setDefaults(v *viper.Viper) {
|
||||
v.SetDefault("server.port", 3000)
|
||||
v.SetDefault("server.mode", "debug")
|
||||
|
||||
v.SetDefault("database.host", "localhost")
|
||||
v.SetDefault("database.port", 5432)
|
||||
v.SetDefault("database.sslmode", "disable")
|
||||
v.SetDefault("database.max_idle_conns", 10)
|
||||
v.SetDefault("database.max_open_conns", 100)
|
||||
v.SetDefault("database.conn_max_lifetime", 3600)
|
||||
v.SetDefault("database.run_migrations", false)
|
||||
v.SetDefault("database.migrations_path", "migrations")
|
||||
|
||||
v.SetDefault("redis.host", "localhost")
|
||||
v.SetDefault("redis.port", 6379)
|
||||
v.SetDefault("redis.db", 0)
|
||||
v.SetDefault("redis.pool_size", 50)
|
||||
|
||||
v.SetDefault("jwt.expiry_hours", 72)
|
||||
|
||||
@@ -12,24 +12,20 @@ func TestValidate_ValidConfig(t *testing.T) {
|
||||
cfg := &Config{
|
||||
Server: ServerConfig{Host: "localhost", Port: 8080, Mode: "debug"},
|
||||
Database: DatabaseConfig{
|
||||
Host: "localhost",
|
||||
Port: 5432,
|
||||
User: "gochat",
|
||||
Password: "secret",
|
||||
Name: "gochat_db",
|
||||
DSN: "postgres://gochat:secret@localhost:5432/gochat_db?sslmode=disable",
|
||||
},
|
||||
Redis: RedisConfig{
|
||||
URL: "redis://localhost:6379",
|
||||
DSN: "redis://localhost:6379",
|
||||
},
|
||||
JWT: JWTConfig{
|
||||
Secret: "test-secret-key-min-32-chars!!",
|
||||
ExpiryHours: 24,
|
||||
RefreshExpiryHours: 168,
|
||||
},
|
||||
Log: LogConfig{Level: "info", Format: "json"},
|
||||
Worker: WorkerConfig{Concurrency: 4, BlockTimeoutS: 5, SweepIntervalS: 30},
|
||||
OAuth: OAuthConfig{},
|
||||
Search: SearchConfig{Engine: "meilisearch", Host: "http://localhost:7700", IndexPrefix: "gochat_", TimeoutSeconds: 5},
|
||||
Log: LogConfig{Level: "info", Format: "json"},
|
||||
Worker: WorkerConfig{Concurrency: 4, BlockTimeoutS: 5, SweepIntervalS: 30},
|
||||
OAuth: OAuthConfig{},
|
||||
Search: SearchConfig{Engine: "meilisearch", Host: "http://localhost:7700", IndexPrefix: "gochat_", TimeoutSeconds: 5},
|
||||
}
|
||||
|
||||
err := Validate(cfg)
|
||||
@@ -39,8 +35,8 @@ func TestValidate_ValidConfig(t *testing.T) {
|
||||
func TestValidate_InvalidPort(t *testing.T) {
|
||||
cfg := &Config{
|
||||
Server: ServerConfig{Host: "localhost", Port: 0, Mode: "debug"},
|
||||
Database: DatabaseConfig{Host: "localhost", Name: "db", User: "user"},
|
||||
Redis: RedisConfig{URL: "redis://localhost:6379"},
|
||||
Database: DatabaseConfig{DSN: "postgres://user@localhost:5432/db?sslmode=disable"},
|
||||
Redis: RedisConfig{DSN: "redis://localhost:6379"},
|
||||
JWT: JWTConfig{Secret: "test-secret-key-min-32-chars!!"},
|
||||
}
|
||||
|
||||
@@ -52,8 +48,8 @@ func TestValidate_InvalidPort(t *testing.T) {
|
||||
func TestValidate_InvalidMode(t *testing.T) {
|
||||
cfg := &Config{
|
||||
Server: ServerConfig{Host: "localhost", Port: 8080, Mode: "invalid"},
|
||||
Database: DatabaseConfig{Host: "localhost", Name: "db", User: "user"},
|
||||
Redis: RedisConfig{URL: "redis://localhost:6379"},
|
||||
Database: DatabaseConfig{DSN: "postgres://user@localhost:5432/db?sslmode=disable"},
|
||||
Redis: RedisConfig{DSN: "redis://localhost:6379"},
|
||||
JWT: JWTConfig{Secret: "test-secret-key-min-32-chars!!"},
|
||||
}
|
||||
|
||||
@@ -62,62 +58,62 @@ func TestValidate_InvalidMode(t *testing.T) {
|
||||
assert.Contains(t, err.Error(), "invalid server mode")
|
||||
}
|
||||
|
||||
func TestValidate_MissingDBHost(t *testing.T) {
|
||||
func TestValidate_MissingDBDSN(t *testing.T) {
|
||||
cfg := &Config{
|
||||
Server: ServerConfig{Host: "localhost", Port: 8080, Mode: "debug"},
|
||||
Database: DatabaseConfig{Name: "db", User: "user"},
|
||||
Redis: RedisConfig{URL: "redis://localhost:6379"},
|
||||
Database: DatabaseConfig{},
|
||||
Redis: RedisConfig{DSN: "redis://localhost:6379"},
|
||||
JWT: JWTConfig{Secret: "test-secret-key-min-32-chars!!"},
|
||||
}
|
||||
|
||||
err := Validate(cfg)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "database host is required")
|
||||
assert.Contains(t, err.Error(), "database DSN is required")
|
||||
}
|
||||
|
||||
func TestValidate_MissingDBName(t *testing.T) {
|
||||
func TestValidate_InvalidDBDSN(t *testing.T) {
|
||||
cfg := &Config{
|
||||
Server: ServerConfig{Host: "localhost", Port: 8080, Mode: "debug"},
|
||||
Database: DatabaseConfig{Host: "localhost", User: "user"},
|
||||
Redis: RedisConfig{URL: "redis://localhost:6379"},
|
||||
Database: DatabaseConfig{DSN: "mysql://user@localhost/db"},
|
||||
Redis: RedisConfig{DSN: "redis://localhost:6379"},
|
||||
JWT: JWTConfig{Secret: "test-secret-key-min-32-chars!!"},
|
||||
}
|
||||
|
||||
err := Validate(cfg)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "database name is required")
|
||||
assert.Contains(t, err.Error(), "invalid database DSN")
|
||||
}
|
||||
|
||||
func TestValidate_MissingRedisURL(t *testing.T) {
|
||||
func TestValidate_MissingRedisDSN(t *testing.T) {
|
||||
cfg := &Config{
|
||||
Server: ServerConfig{Host: "localhost", Port: 8080, Mode: "debug"},
|
||||
Database: DatabaseConfig{Host: "localhost", Name: "db", User: "user"},
|
||||
Database: DatabaseConfig{DSN: "postgres://user@localhost:5432/db?sslmode=disable"},
|
||||
JWT: JWTConfig{Secret: "test-secret-key-min-32-chars!!"},
|
||||
}
|
||||
|
||||
err := Validate(cfg)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "redis url is required")
|
||||
assert.Contains(t, err.Error(), "redis DSN is required")
|
||||
}
|
||||
|
||||
func TestValidate_InvalidRedisURL(t *testing.T) {
|
||||
func TestValidate_InvalidRedisDSN(t *testing.T) {
|
||||
cfg := &Config{
|
||||
Server: ServerConfig{Host: "localhost", Port: 8080, Mode: "debug"},
|
||||
Database: DatabaseConfig{Host: "localhost", Name: "db", User: "user"},
|
||||
Redis: RedisConfig{URL: "not-a-valid-url://::"},
|
||||
Database: DatabaseConfig{DSN: "postgres://user@localhost:5432/db?sslmode=disable"},
|
||||
Redis: RedisConfig{DSN: "not-a-valid-url://::"},
|
||||
JWT: JWTConfig{Secret: "test-secret-key-min-32-chars!!"},
|
||||
}
|
||||
|
||||
err := Validate(cfg)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "invalid redis URL")
|
||||
assert.Contains(t, err.Error(), "invalid redis DSN")
|
||||
}
|
||||
|
||||
func TestValidate_JWTSecretInProduction(t *testing.T) {
|
||||
cfg := &Config{
|
||||
Server: ServerConfig{Host: "localhost", Port: 8080, Mode: "release"},
|
||||
Database: DatabaseConfig{Host: "localhost", Name: "db", User: "user"},
|
||||
Redis: RedisConfig{URL: "redis://localhost:6379"},
|
||||
Database: DatabaseConfig{DSN: "postgres://user@localhost:5432/db?sslmode=disable"},
|
||||
Redis: RedisConfig{DSN: "redis://localhost:6379"},
|
||||
JWT: JWTConfig{Secret: "change-me-in-production"},
|
||||
}
|
||||
|
||||
@@ -129,8 +125,8 @@ func TestValidate_JWTSecretInProduction(t *testing.T) {
|
||||
func TestValidate_InvalidLogLevel(t *testing.T) {
|
||||
cfg := &Config{
|
||||
Server: ServerConfig{Host: "localhost", Port: 8080, Mode: "debug"},
|
||||
Database: DatabaseConfig{Host: "localhost", Name: "db", User: "user"},
|
||||
Redis: RedisConfig{URL: "redis://localhost:6379"},
|
||||
Database: DatabaseConfig{DSN: "postgres://user@localhost:5432/db?sslmode=disable"},
|
||||
Redis: RedisConfig{DSN: "redis://localhost:6379"},
|
||||
JWT: JWTConfig{Secret: "test-secret-key-min-32-chars!!"},
|
||||
Log: LogConfig{Level: "invalid"},
|
||||
}
|
||||
@@ -143,8 +139,8 @@ func TestValidate_InvalidLogLevel(t *testing.T) {
|
||||
func TestValidate_InvalidWorkerConcurrency(t *testing.T) {
|
||||
cfg := &Config{
|
||||
Server: ServerConfig{Host: "localhost", Port: 8080, Mode: "debug"},
|
||||
Database: DatabaseConfig{Host: "localhost", Name: "db", User: "user"},
|
||||
Redis: RedisConfig{URL: "redis://localhost:6379"},
|
||||
Database: DatabaseConfig{DSN: "postgres://user@localhost:5432/db?sslmode=disable"},
|
||||
Redis: RedisConfig{DSN: "redis://localhost:6379"},
|
||||
JWT: JWTConfig{Secret: "test-secret-key-min-32-chars!!"},
|
||||
Log: LogConfig{Level: "info"},
|
||||
Worker: WorkerConfig{Concurrency: 0},
|
||||
@@ -157,13 +153,13 @@ func TestValidate_InvalidWorkerConcurrency(t *testing.T) {
|
||||
|
||||
func TestValidate_SearchMeilisearchRequiresValidHost(t *testing.T) {
|
||||
cfg := &Config{
|
||||
Server: ServerConfig{Host: "localhost", Port: 8080, Mode: "debug"},
|
||||
Database: DatabaseConfig{Host: "localhost", Name: "db", User: "user"},
|
||||
Redis: RedisConfig{URL: "redis://localhost:6379"},
|
||||
JWT: JWTConfig{Secret: "test-secret-key-min-32-chars!!"},
|
||||
Log: LogConfig{Level: "info"},
|
||||
Worker: WorkerConfig{Concurrency: 1, BlockTimeoutS: 5, SweepIntervalS: 30},
|
||||
Search: SearchConfig{Engine: "meilisearch", Host: "not a url", TimeoutSeconds: 5},
|
||||
Server: ServerConfig{Host: "localhost", Port: 8080, Mode: "debug"},
|
||||
Database: DatabaseConfig{DSN: "postgres://user@localhost:5432/db?sslmode=disable"},
|
||||
Redis: RedisConfig{DSN: "redis://localhost:6379"},
|
||||
JWT: JWTConfig{Secret: "test-secret-key-min-32-chars!!"},
|
||||
Log: LogConfig{Level: "info"},
|
||||
Worker: WorkerConfig{Concurrency: 1, BlockTimeoutS: 5, SweepIntervalS: 30},
|
||||
Search: SearchConfig{Engine: "meilisearch", Host: "not a url", TimeoutSeconds: 5},
|
||||
}
|
||||
|
||||
err := Validate(cfg)
|
||||
@@ -173,13 +169,13 @@ func TestValidate_SearchMeilisearchRequiresValidHost(t *testing.T) {
|
||||
|
||||
func TestValidate_SearchDBFallbackAllowed(t *testing.T) {
|
||||
cfg := &Config{
|
||||
Server: ServerConfig{Host: "localhost", Port: 8080, Mode: "debug"},
|
||||
Database: DatabaseConfig{Host: "localhost", Name: "db", User: "user"},
|
||||
Redis: RedisConfig{URL: "redis://localhost:6379"},
|
||||
JWT: JWTConfig{Secret: "test-secret-key-min-32-chars!!"},
|
||||
Log: LogConfig{Level: "info"},
|
||||
Worker: WorkerConfig{Concurrency: 1, BlockTimeoutS: 5, SweepIntervalS: 30},
|
||||
Search: SearchConfig{Engine: "db"},
|
||||
Server: ServerConfig{Host: "localhost", Port: 8080, Mode: "debug"},
|
||||
Database: DatabaseConfig{DSN: "postgres://user@localhost:5432/db?sslmode=disable"},
|
||||
Redis: RedisConfig{DSN: "redis://localhost:6379"},
|
||||
JWT: JWTConfig{Secret: "test-secret-key-min-32-chars!!"},
|
||||
Log: LogConfig{Level: "info"},
|
||||
Worker: WorkerConfig{Concurrency: 1, BlockTimeoutS: 5, SweepIntervalS: 30},
|
||||
Search: SearchConfig{Engine: "db"},
|
||||
}
|
||||
|
||||
err := Validate(cfg)
|
||||
@@ -188,13 +184,13 @@ func TestValidate_SearchDBFallbackAllowed(t *testing.T) {
|
||||
|
||||
func TestValidate_SearchDBFallbackRejectedInRelease(t *testing.T) {
|
||||
cfg := &Config{
|
||||
Server: ServerConfig{Host: "localhost", Port: 8080, Mode: "release"},
|
||||
Database: DatabaseConfig{Host: "localhost", Name: "db", User: "user"},
|
||||
Redis: RedisConfig{URL: "redis://localhost:6379"},
|
||||
JWT: JWTConfig{Secret: "test-secret-key-min-32-chars!!"},
|
||||
Log: LogConfig{Level: "info"},
|
||||
Worker: WorkerConfig{Concurrency: 1, BlockTimeoutS: 5, SweepIntervalS: 30},
|
||||
Search: SearchConfig{Engine: "db"},
|
||||
Server: ServerConfig{Host: "localhost", Port: 8080, Mode: "release"},
|
||||
Database: DatabaseConfig{DSN: "postgres://user@localhost:5432/db?sslmode=disable"},
|
||||
Redis: RedisConfig{DSN: "redis://localhost:6379"},
|
||||
JWT: JWTConfig{Secret: "test-secret-key-min-32-chars!!"},
|
||||
Log: LogConfig{Level: "info"},
|
||||
Worker: WorkerConfig{Concurrency: 1, BlockTimeoutS: 5, SweepIntervalS: 30},
|
||||
Search: SearchConfig{Engine: "db"},
|
||||
}
|
||||
|
||||
err := Validate(cfg)
|
||||
@@ -202,22 +198,13 @@ func TestValidate_SearchDBFallbackRejectedInRelease(t *testing.T) {
|
||||
assert.Contains(t, err.Error(), "release mode requires meilisearch")
|
||||
}
|
||||
|
||||
func TestDatabaseConfig_DSN(t *testing.T) {
|
||||
func TestDatabaseConfig_MigrateDSN(t *testing.T) {
|
||||
cfg := DatabaseConfig{
|
||||
Host: "localhost",
|
||||
Port: 5432,
|
||||
User: "gochat",
|
||||
Password: "secret",
|
||||
DBName: "gochat_db",
|
||||
SSLMode: "disable",
|
||||
DSN: "postgres://gochat:secret@localhost:5432/gochat_db?sslmode=disable",
|
||||
}
|
||||
|
||||
dsn := cfg.DSN()
|
||||
assert.Contains(t, dsn, "host=localhost")
|
||||
assert.Contains(t, dsn, "port=5432")
|
||||
assert.Contains(t, dsn, "user=gochat")
|
||||
assert.Contains(t, dsn, "dbname=gochat_db")
|
||||
assert.Contains(t, dsn, "sslmode=disable")
|
||||
dsn := cfg.MigrateDSN()
|
||||
assert.Equal(t, "postgres://gochat:secret@localhost:5432/gochat_db?sslmode=disable", dsn)
|
||||
}
|
||||
|
||||
func TestJWTConfig_ExpiryDuration(t *testing.T) {
|
||||
|
||||
@@ -74,12 +74,12 @@ func TestConfigReloader_ApplyReloadableFields(t *testing.T) {
|
||||
oldCfg := validTestConfig()
|
||||
oldCfg.Log.Level = "debug"
|
||||
oldCfg.Worker.Concurrency = 4
|
||||
oldCfg.Database.Host = "original-host" // immutable field
|
||||
oldCfg.Database.DSN = "postgres://user@original-host:5432/db?sslmode=disable" // immutable field
|
||||
|
||||
newCfg := validTestConfig()
|
||||
newCfg.Log.Level = "info"
|
||||
newCfg.Worker.Concurrency = 8
|
||||
newCfg.Database.Host = "changed-host" // should NOT be applied
|
||||
newCfg.Database.DSN = "postgres://user@changed-host:5432/db?sslmode=disable" // should NOT be applied
|
||||
|
||||
r := &ConfigReloader{cfg: oldCfg}
|
||||
r.applyReloadableFields(newCfg)
|
||||
@@ -89,7 +89,7 @@ func TestConfigReloader_ApplyReloadableFields(t *testing.T) {
|
||||
assert.Equal(t, 8, r.cfg.Worker.Concurrency)
|
||||
|
||||
// Immutable fields should NOT be updated
|
||||
assert.Equal(t, "original-host", r.cfg.Database.Host)
|
||||
assert.Equal(t, "postgres://user@original-host:5432/db?sslmode=disable", r.cfg.Database.DSN)
|
||||
}
|
||||
|
||||
func TestConfigReloader_Stop(t *testing.T) {
|
||||
@@ -128,14 +128,9 @@ server:
|
||||
port: 3000
|
||||
mode: "debug"
|
||||
database:
|
||||
host: "localhost"
|
||||
port: 5432
|
||||
user: "gochat"
|
||||
password: "secret"
|
||||
dbname: "gochat_db"
|
||||
sslmode: "disable"
|
||||
dsn: "postgres://gochat:secret@localhost:5432/gochat_db?sslmode=disable"
|
||||
redis:
|
||||
url: "redis://localhost:6379"
|
||||
dsn: "redis://localhost:6379"
|
||||
jwt:
|
||||
secret: "test-secret-key-min-32-chars!!"
|
||||
expiry_hours: 72
|
||||
@@ -185,7 +180,7 @@ func TestLoadDotEnv_QuotedValues(t *testing.T) {
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "my-super-secret-key-with-quotes", value)
|
||||
|
||||
_, value, ok = parseDotEnvLine("GOCHAT_REDIS_URL='redis://special-host:6379'")
|
||||
_, value, ok = parseDotEnvLine("GOCHAT_REDIS_DSN='redis://special-host:6379'")
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "redis://special-host:6379", value)
|
||||
}
|
||||
@@ -199,11 +194,11 @@ func TestEnvBindingsComplete(t *testing.T) {
|
||||
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||
v.AutomaticEnv()
|
||||
|
||||
// GOCHAT_DATABASE_HOST → viper key "database.host" via env prefix + replacer
|
||||
// GOCHAT_DATABASE_DSN → viper key "database.dsn" via env prefix + replacer
|
||||
// The replacer replaces . with _ in viper keys when looking up env vars.
|
||||
// So viper key "database.host" → env key "DATABASE_HOST" → with prefix "GOCHAT_DATABASE_HOST"
|
||||
lowerKey := strings.ToLower(strings.ReplaceAll("database.host", ".", "_"))
|
||||
assert.Equal(t, "database_host", lowerKey)
|
||||
// So viper key "database.dsn" → env key "DATABASE_DSN" → with prefix "GOCHAT_DATABASE_DSN"
|
||||
lowerKey := strings.ToLower(strings.ReplaceAll("database.dsn", ".", "_"))
|
||||
assert.Equal(t, "database_dsn", lowerKey)
|
||||
}
|
||||
|
||||
// --- Defaults Tests ---
|
||||
@@ -215,11 +210,9 @@ func TestSetDefaults(t *testing.T) {
|
||||
assert.Equal(t, "0.0.0.0", v.GetString("server.host"))
|
||||
assert.Equal(t, 3000, v.GetInt("server.port"))
|
||||
assert.Equal(t, "debug", v.GetString("server.mode"))
|
||||
assert.Equal(t, "localhost", v.GetString("database.host"))
|
||||
assert.Equal(t, 5432, v.GetInt("database.port"))
|
||||
assert.Equal(t, "disable", v.GetString("database.sslmode"))
|
||||
assert.Equal(t, "localhost", v.GetString("redis.host"))
|
||||
assert.Equal(t, 6379, v.GetInt("redis.port"))
|
||||
assert.Equal(t, 10, v.GetInt("database.max_idle_conns"))
|
||||
assert.Equal(t, 100, v.GetInt("database.max_open_conns"))
|
||||
assert.Equal(t, 50, v.GetInt("redis.pool_size"))
|
||||
assert.Equal(t, "debug", v.GetString("log.level"))
|
||||
assert.Equal(t, "json", v.GetString("log.format"))
|
||||
assert.Equal(t, 4, v.GetInt("worker.concurrency"))
|
||||
@@ -244,8 +237,8 @@ func TestApplyZeroDefaults(t *testing.T) {
|
||||
func validTestConfig() *Config {
|
||||
return &Config{
|
||||
Server: ServerConfig{Host: "localhost", Port: 8080, Mode: "debug"},
|
||||
Database: DatabaseConfig{Host: "localhost", Port: 5432, User: "gochat", Password: "secret", Name: "gochat_db", DBName: "gochat_db"},
|
||||
Redis: RedisConfig{URL: "redis://localhost:6379"},
|
||||
Database: DatabaseConfig{DSN: "postgres://gochat:secret@localhost:5432/gochat_db?sslmode=disable"},
|
||||
Redis: RedisConfig{DSN: "redis://localhost:6379"},
|
||||
JWT: JWTConfig{Secret: "test-secret-key-min-32-chars!!", ExpiryHours: 24, RefreshExpiryHours: 168},
|
||||
Log: LogConfig{Level: "info", Format: "json"},
|
||||
Worker: WorkerConfig{Concurrency: 4},
|
||||
|
||||
@@ -20,29 +20,33 @@ func Validate(cfg *Config) error {
|
||||
}
|
||||
|
||||
// Database validation (ref: Chatwoot requires POSTGRES_* env vars)
|
||||
if cfg.Database.Host == "" {
|
||||
return fmt.Errorf("database host is required")
|
||||
if cfg.Database.DSN == "" {
|
||||
return fmt.Errorf("database DSN is required")
|
||||
}
|
||||
if cfg.Database.Name == "" {
|
||||
return fmt.Errorf("database name is required")
|
||||
dbURL, err := url.Parse(cfg.Database.DSN)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid database DSN: %w", err)
|
||||
}
|
||||
if cfg.Database.User == "" {
|
||||
return fmt.Errorf("database user is required")
|
||||
if dbURL.Scheme != "postgres" && dbURL.Scheme != "postgresql" {
|
||||
return fmt.Errorf("invalid database DSN: scheme must be postgres or postgresql, got %s", dbURL.Scheme)
|
||||
}
|
||||
if dbURL.Host == "" {
|
||||
return fmt.Errorf("invalid database DSN: host is required")
|
||||
}
|
||||
|
||||
// Redis validation (ref: Chatwoot config/cable.yml requires REDIS_URL)
|
||||
if cfg.Redis.URL == "" {
|
||||
return fmt.Errorf("redis url is required")
|
||||
// Redis validation (ref: Chatwoot config/cable.yml requires Redis connection)
|
||||
if cfg.Redis.DSN == "" {
|
||||
return fmt.Errorf("redis DSN is required")
|
||||
}
|
||||
redisURL, err := url.Parse(cfg.Redis.URL)
|
||||
redisURL, err := url.Parse(cfg.Redis.DSN)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid redis URL: %w", err)
|
||||
return fmt.Errorf("invalid redis DSN: %w", err)
|
||||
}
|
||||
if redisURL.Scheme != "redis" && redisURL.Scheme != "rediss" {
|
||||
return fmt.Errorf("invalid redis URL: scheme must be redis or rediss, got %s", redisURL.Scheme)
|
||||
return fmt.Errorf("invalid redis DSN: scheme must be redis or rediss, got %s", redisURL.Scheme)
|
||||
}
|
||||
if redisURL.Host == "" {
|
||||
return fmt.Errorf("invalid redis URL: host is required")
|
||||
return fmt.Errorf("invalid redis DSN: host is required")
|
||||
}
|
||||
|
||||
// JWT validation
|
||||
|
||||
@@ -8,12 +8,21 @@ set -euo pipefail
|
||||
ENV="${1:-production}"
|
||||
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||
BACKUP_DIR="${GOCHAT_BACKUP_DIR:-/var/backups/gochat}"
|
||||
DB_NAME="${POSTGRES_DB:-gochat_production}"
|
||||
DB_USER="${POSTGRES_USER:-gochat}"
|
||||
DB_HOST="${POSTGRES_HOST:-localhost}"
|
||||
DB_PORT="${POSTGRES_PORT:-5432}"
|
||||
DSN="${GOCHAT_DATABASE_DSN:-}"
|
||||
RETENTION_DAYS="${GOCHAT_BACKUP_RETENTION_DAYS:-30}"
|
||||
|
||||
if [[ -z "$DSN" ]]; then
|
||||
echo "[$(date)] ERROR: GOCHAT_DATABASE_DSN is not set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Parse DSN: postgres://user:password@host:port/dbname?sslmode=...
|
||||
DB_USER=$(echo "$DSN" | sed 's|.*://||; s|:.*||')
|
||||
DB_PASS=$(echo "$DSN" | sed 's|.*://[^:]*:||; s|@.*||')
|
||||
DB_HOST=$(echo "$DSN" | sed 's|.*@||; s|:.*||')
|
||||
DB_PORT=$(echo "$DSN" | sed 's|.*@.*:||; s|/.*||')
|
||||
DB_NAME=$(echo "$DSN" | sed 's|.*/||; s|\?.*||')
|
||||
|
||||
mkdir -p "${BACKUP_DIR}"
|
||||
|
||||
BACKUP_FILE="${BACKUP_DIR}/${DB_NAME}_${TIMESTAMP}.sql.gz"
|
||||
@@ -21,7 +30,14 @@ BACKUP_FILE="${BACKUP_DIR}/${DB_NAME}_${TIMESTAMP}.sql.gz"
|
||||
echo "[$(date)] Starting backup of ${DB_NAME} on ${DB_HOST}:${DB_PORT}"
|
||||
|
||||
# pg_dump with compression — mirrors Chatwoot backup approach
|
||||
PGPASSWORD="${POSTGRES_PASSWORD}" pg_dump -h "${DB_HOST}" -p "${DB_PORT}" -U "${DB_USER}" -d "${DB_NAME}" --format=custom --compress=9 | gzip > "${BACKUP_FILE}"
|
||||
PGPASSWORD="${DB_PASS}" pg_dump \
|
||||
-h "${DB_HOST}" \
|
||||
-p "${DB_PORT}" \
|
||||
-U "${DB_USER}" \
|
||||
-d "${DB_NAME}" \
|
||||
--format=custom \
|
||||
--compress=9 \
|
||||
| gzip > "${BACKUP_FILE}"
|
||||
|
||||
BACKUP_SIZE=$(du -h "${BACKUP_FILE}" | cut -f1)
|
||||
echo "[$(date)] Backup complete: ${BACKUP_FILE} (${BACKUP_SIZE})"
|
||||
|
||||
@@ -6,17 +6,16 @@
|
||||
set -e
|
||||
|
||||
MIGRATIONS_DIR="./migrations"
|
||||
DB_HOST="${POSTGRES_HOST:-localhost}"
|
||||
DB_PORT="${POSTGRES_PORT:-5432}"
|
||||
DB_NAME="${POSTGRES_DATABASE:-gochat_dev}"
|
||||
DB_USER="${POSTGRES_USERNAME:-postgres}"
|
||||
DB_PASS="${POSTGRES_PASSWORD:-}"
|
||||
|
||||
DSN="postgres://${DB_USER}:${DB_PASS}@${DB_HOST}:${DB_PORT}/${DB_NAME}?sslmode=disable"
|
||||
DSN="${GOCHAT_DATABASE_DSN:-postgres://postgres:postgres@localhost:5432/gochat_dev?sslmode=disable}"
|
||||
|
||||
case "$1" in
|
||||
create)
|
||||
echo "Creating database ${DB_NAME}..."
|
||||
echo "Creating database from DSN..."
|
||||
# Extract dbname from DSN for createdb
|
||||
DB_NAME=$(echo "$DSN" | sed 's|.*/||; s|\?.*||')
|
||||
DB_HOST=$(echo "$DSN" | sed 's|.*@||; s|:.*||')
|
||||
DB_PORT=$(echo "$DSN" | sed 's|.*@.*:||; s|/.*||')
|
||||
DB_USER=$(echo "$DSN" | sed 's|.*://||; s|:.*||')
|
||||
createdb -h "${DB_HOST}" -p "${DB_PORT}" -U "${DB_USER}" "${DB_NAME}" 2>/dev/null || echo "Database already exists"
|
||||
;;
|
||||
up)
|
||||
|
||||
@@ -43,8 +43,13 @@ if [[ "${1:-}" == "--clean" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
log_warn "Cleaning all data before seeding..."
|
||||
# Truncate tables in safe order (preserve migrations/schema)
|
||||
psql_cmd="psql -h ${POSTGRES_HOST:-localhost} -p ${POSTGRES_PORT:-5432} -U ${POSTGRES_USER:-gochat} -d ${POSTGRES_DB:-gochat_dev}"
|
||||
# Parse DSN for psql connection
|
||||
DB_HOST=$(echo "$GOCHAT_DATABASE_DSN" | sed 's|.*@||; s|:.*||')
|
||||
DB_PORT=$(echo "$GOCHAT_DATABASE_DSN" | sed 's|.*@.*:||; s|/.*||')
|
||||
DB_USER=$(echo "$GOCHAT_DATABASE_DSN" | sed 's|.*://||; s|:.*||')
|
||||
DB_PASS=$(echo "$GOCHAT_DATABASE_DSN" | sed 's|.*://[^:]*:||; s|@.*||')
|
||||
DB_NAME=$(echo "$GOCHAT_DATABASE_DSN" | sed 's|.*/||; s|\?.*||')
|
||||
psql_cmd="PGPASSWORD=${DB_PASS} psql -h ${DB_HOST} -p ${DB_PORT} -U ${DB_USER} -d ${DB_NAME}"
|
||||
$psql_cmd -c "
|
||||
TRUNCATE TABLE
|
||||
messages,
|
||||
|
||||
@@ -60,13 +60,8 @@ services:
|
||||
env_file: ../../.env
|
||||
environment:
|
||||
- GOCHAT_ENV=development
|
||||
- POSTGRES_HOST=postgres
|
||||
- POSTGRES_PORT=5432
|
||||
- POSTGRES_DATABASE=gochat_dev
|
||||
- POSTGRES_USER=postgres
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
- REDIS_HOST=redis
|
||||
- REDIS_PORT=6379
|
||||
- GOCHAT_DATABASE_DSN=postgres://postgres:postgres@postgres:5432/gochat_dev?sslmode=disable
|
||||
- GOCHAT_REDIS_DSN=redis://redis:6379
|
||||
- SMTP_HOST=mailhog
|
||||
- SMTP_PORT=1025
|
||||
|
||||
@@ -86,13 +81,8 @@ services:
|
||||
env_file: ../../.env
|
||||
environment:
|
||||
- GOCHAT_ENV=development
|
||||
- POSTGRES_HOST=postgres
|
||||
- POSTGRES_PORT=5432
|
||||
- POSTGRES_DATABASE=gochat_dev
|
||||
- POSTGRES_USER=postgres
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
- REDIS_HOST=redis
|
||||
- REDIS_PORT=6379
|
||||
- GOCHAT_DATABASE_DSN=postgres://postgres:postgres@postgres:5432/gochat_dev?sslmode=disable
|
||||
- GOCHAT_REDIS_DSN=redis://redis:6379
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
|
||||
@@ -60,10 +60,8 @@ services:
|
||||
env_file: ../../.env
|
||||
environment:
|
||||
- GOCHAT_ENV=production
|
||||
- POSTGRES_HOST=postgres
|
||||
- POSTGRES_PORT=5432
|
||||
- REDIS_HOST=redis
|
||||
- REDIS_PORT=6379
|
||||
- GOCHAT_DATABASE_DSN=postgres://${POSTGRES_USER:-gochat}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-gochat_production}?sslmode=disable
|
||||
- GOCHAT_REDIS_DSN=redis://:${REDIS_PASSWORD}@redis:6379
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
|
||||
interval: 30s
|
||||
@@ -92,10 +90,8 @@ services:
|
||||
env_file: ../../.env
|
||||
environment:
|
||||
- GOCHAT_ENV=production
|
||||
- POSTGRES_HOST=postgres
|
||||
- POSTGRES_PORT=5432
|
||||
- REDIS_HOST=redis
|
||||
- REDIS_PORT=6379
|
||||
- GOCHAT_DATABASE_DSN=postgres://${POSTGRES_USER:-gochat}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-gochat_production}?sslmode=disable
|
||||
- GOCHAT_REDIS_DSN=redis://:${REDIS_PASSWORD}@redis:6379
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
|
||||
@@ -40,12 +40,7 @@ services:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
- GOCHAT_ENV=test
|
||||
- POSTGRES_HOST=postgres
|
||||
- POSTGRES_PORT=5432
|
||||
- POSTGRES_USER=postgres
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
- POSTGRES_DATABASE=gochat_test
|
||||
- REDIS_HOST=redis
|
||||
- REDIS_PORT=6379
|
||||
- GOCHAT_DATABASE_DSN=postgres://postgres:postgres@postgres:5432/gochat_test?sslmode=disable
|
||||
- GOCHAT_REDIS_DSN=redis://redis:6379
|
||||
- GOCHAT_JWT_SECRET=test_secret
|
||||
command: ["test"]
|
||||
|
||||
@@ -58,12 +58,8 @@ services:
|
||||
env_file: ../../.env
|
||||
environment:
|
||||
- GOCHAT_ENV=development
|
||||
- POSTGRES_HOST=postgres
|
||||
- POSTGRES_PORT=5432
|
||||
- POSTGRES_DATABASE=gochat_dev
|
||||
- POSTGRES_USERNAME=postgres
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
- REDIS_URL=redis://redis:6379
|
||||
- GOCHAT_DATABASE_DSN=postgres://postgres:postgres@postgres:5432/gochat_dev?sslmode=disable
|
||||
- GOCHAT_REDIS_DSN=redis://redis:6379
|
||||
volumes:
|
||||
- ../../backend:/app:delegated
|
||||
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
apiVersion: v2
|
||||
name: gochat
|
||||
description: GoChat — Open-source customer engagement platform (Go port of Chatwoot)
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "1.0.0"
|
||||
home: https://github.com/gochat/gochat
|
||||
icon: https://gochat.io/logo.png
|
||||
maintainers:
|
||||
- name: gochat-team
|
||||
email: team@gochat.io
|
||||
sources:
|
||||
- https://github.com/gochat/gochat
|
||||
keywords:
|
||||
- chat
|
||||
- customer-engagement
|
||||
- live-chat
|
||||
- omnichannel
|
||||
- chatwoot
|
||||
@@ -1,49 +0,0 @@
|
||||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "gochat.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified app name.
|
||||
*/}}
|
||||
{{- define "gochat.fullname" -}}
|
||||
{{- if .Values.fullnameOverride }}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride }}
|
||||
{{- if contains $name .Release.Name }}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create chart name and version as used by the chart label.
|
||||
*/}}
|
||||
{{- define "gochat.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Common labels
|
||||
*/}}
|
||||
{{- define "gochat.labels" -}}
|
||||
helm.sh/chart: {{ include "gochat.chart" . }}
|
||||
{{ include "gochat.selectorLabels" . }}
|
||||
{{- if .Chart.AppVersion }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
{{- end }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Selector labels
|
||||
*/}}
|
||||
{{- define "gochat.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "gochat.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end }}
|
||||
@@ -1,15 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: {{ include "gochat.fullname" . }}-config
|
||||
labels:
|
||||
{{- include "gochat.labels" . | nindent 4 }}
|
||||
data:
|
||||
{{- range $key, $value := .Values.configMap.data }}
|
||||
{{ $key }}: {{ $value | quote }}
|
||||
{{- end }}
|
||||
POSTGRES_HOST: {{ if .Values.postgresql.enabled }}{{ include "gochat.fullname" . }}-postgresql{{ else }}{{ .Values.configMap.data.POSTGRES_HOST | default "localhost" }}{{ end }}
|
||||
POSTGRES_PORT: "5432"
|
||||
POSTGRES_DATABASE: {{ .Values.postgresql.auth.database | quote }}
|
||||
REDIS_HOST: {{ if .Values.redis.enabled }}{{ include "gochat.fullname" . }}-redis-master{{ else }}{{ .Values.configMap.data.REDIS_HOST | default "localhost" }}{{ end }}
|
||||
REDIS_PORT: "6379"
|
||||
@@ -1,82 +0,0 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "gochat.fullname" . }}
|
||||
labels:
|
||||
{{- include "gochat.labels" . | nindent 4 }}
|
||||
spec:
|
||||
replicas: {{ .Values.app.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "gochat.selectorLabels" . | nindent 6 }}
|
||||
strategy:
|
||||
{{- toYaml .Values.app.strategy | nindent 4 }}
|
||||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
{{- toYaml .Values.app.podAnnotations | nindent 8 }}
|
||||
labels:
|
||||
{{- include "gochat.selectorLabels" . | nindent 8 }}
|
||||
spec:
|
||||
terminationGracePeriodSeconds: {{ .Values.app.terminationGracePeriodSeconds }}
|
||||
containers:
|
||||
- name: gochat
|
||||
image: "{{ .Values.app.image.repository }}:{{ .Values.app.image.tag }}"
|
||||
imagePullPolicy: {{ .Values.app.image.pullPolicy }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 3000
|
||||
protocol: TCP
|
||||
{{- if .Values.metrics.enabled }}
|
||||
- name: metrics
|
||||
containerPort: {{ .Values.metrics.service.port }}
|
||||
protocol: TCP
|
||||
{{- end }}
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: {{ include "gochat.fullname" . }}-config
|
||||
- secretRef:
|
||||
name: {{ include "gochat.fullname" . }}-secret
|
||||
{{- if .Values.tracing.enabled }}
|
||||
- configMapRef:
|
||||
name: {{ include "gochat.fullname" . }}-otel-config
|
||||
{{- end }}
|
||||
{{- range $key, $value := .Values.app.extraEnv }}
|
||||
env:
|
||||
- name: {{ $key }}
|
||||
value: {{ $value | quote }}
|
||||
{{- end }}
|
||||
lifecycle:
|
||||
preStop:
|
||||
exec:
|
||||
command: ["sh", "-c", "sleep {{ .Values.app.preStopDelaySeconds }}"]
|
||||
livenessProbe:
|
||||
{{- toYaml .Values.app.livenessProbe | nindent 12 }}
|
||||
readinessProbe:
|
||||
{{- toYaml .Values.app.readinessProbe | nindent 12 }}
|
||||
resources:
|
||||
{{- toYaml .Values.app.resources | nindent 12 }}
|
||||
volumeMounts:
|
||||
- name: configs
|
||||
mountPath: /app/configs
|
||||
- name: migrations
|
||||
mountPath: /app/migrations
|
||||
volumes:
|
||||
- name: configs
|
||||
configMap:
|
||||
name: {{ include "gochat.fullname" . }}-configs
|
||||
- name: migrations
|
||||
configMap:
|
||||
name: {{ include "gochat.fullname" . }}-migrations
|
||||
{{- with .Values.app.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.app.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.app.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
@@ -1,27 +0,0 @@
|
||||
{{- if and .Values.sealedSecrets.enabled .Values.sealedSecrets.externalSecret.enabled }}
|
||||
apiVersion: external-secrets.io/v1beta1
|
||||
kind: ExternalSecret
|
||||
metadata:
|
||||
name: {{ include "gochat.fullname" . }}-external-secret
|
||||
labels:
|
||||
{{- include "gochat.labels" . | nindent 4 }}
|
||||
spec:
|
||||
refreshInterval: {{ .Values.sealedSecrets.externalSecret.refreshInterval }}
|
||||
secretStoreRef:
|
||||
name: {{ .Values.sealedSecrets.externalSecret.secretStoreRef.name }}
|
||||
kind: {{ .Values.sealedSecrets.externalSecret.secretStoreRef.kind }}
|
||||
target:
|
||||
name: {{ include "gochat.fullname" . }}-secret
|
||||
template:
|
||||
type: Opaque
|
||||
data:
|
||||
{{- range $key, $remoteKey := .Values.sealedSecrets.externalSecret.mapping }}
|
||||
{{ $key }}: "{{ `{{ .` }}{{ $remoteKey }}{{ ` }}` }}"
|
||||
{{- end }}
|
||||
data:
|
||||
{{- range $key, $remoteKey := .Values.sealedSecrets.externalSecret.mapping }}
|
||||
- secretKey: {{ $key }}
|
||||
remoteRef:
|
||||
key: {{ $remoteKey }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -1,49 +0,0 @@
|
||||
{{- if .Values.autoscaling.enabled }}
|
||||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: {{ include "gochat.fullname" . }}
|
||||
labels:
|
||||
{{- include "gochat.labels" . | nindent 4 }}
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: {{ include "gochat.fullname" . }}
|
||||
minReplicas: {{ .Values.autoscaling.minReplicas }}
|
||||
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
|
||||
metrics:
|
||||
{{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
|
||||
{{- end }}
|
||||
{{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
|
||||
- type: Resource
|
||||
resource:
|
||||
name: memory
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
|
||||
{{- end }}
|
||||
behavior:
|
||||
scaleDown:
|
||||
stabilizationWindowSeconds: {{ .Values.autoscaling.behavior.scaleDown.stabilizationWindowSeconds }}
|
||||
policies:
|
||||
- type: Percent
|
||||
value: {{ .Values.autoscaling.behavior.scaleDown.percent }}
|
||||
periodSeconds: {{ .Values.autoscaling.behavior.scaleDown.periodSeconds }}
|
||||
scaleUp:
|
||||
stabilizationWindowSeconds: {{ .Values.autoscaling.behavior.scaleUp.stabilizationWindowSeconds }}
|
||||
policies:
|
||||
- type: Percent
|
||||
value: {{ .Values.autoscaling.behavior.scaleUp.percent }}
|
||||
periodSeconds: {{ .Values.autoscaling.behavior.scaleUp.periodSeconds }}
|
||||
- type: Pods
|
||||
value: {{ .Values.autoscaling.behavior.scaleUp.pods }}
|
||||
periodSeconds: {{ .Values.autoscaling.behavior.scaleUp.podsPeriodSeconds }}
|
||||
selectPolicy: Max
|
||||
{{- end }}
|
||||
@@ -1,37 +0,0 @@
|
||||
{{- if .Values.ingress.enabled }}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ include "gochat.fullname" . }}
|
||||
labels:
|
||||
{{- include "gochat.labels" . | nindent 4 }}
|
||||
annotations:
|
||||
{{- toYaml .Values.ingress.annotations | nindent 4 }}
|
||||
spec:
|
||||
ingressClassName: {{ .Values.ingress.className }}
|
||||
{{- if .Values.ingress.tls }}
|
||||
tls:
|
||||
{{- range .Values.ingress.tls }}
|
||||
- hosts:
|
||||
{{- range .hosts }}
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
secretName: {{ .secretName }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
rules:
|
||||
{{- range .Values.ingress.hosts }}
|
||||
- host: {{ .host | quote }}
|
||||
http:
|
||||
paths:
|
||||
{{- range .paths }}
|
||||
- path: {{ .path }}
|
||||
pathType: {{ .pathType }}
|
||||
backend:
|
||||
service:
|
||||
name: {{ include "gochat.fullname" $ }}
|
||||
port:
|
||||
number: {{ $.Values.app.service.port }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -1,83 +0,0 @@
|
||||
{{- if .Values.tracing.enabled }}
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "gochat.fullname" . }}-jaeger
|
||||
labels:
|
||||
{{- include "gochat.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: jaeger
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "gochat.selectorLabels" . | nindent 6 }}
|
||||
app.kubernetes.io/component: jaeger
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "gochat.selectorLabels" . | nindent 8 }}
|
||||
app.kubernetes.io/component: jaeger
|
||||
spec:
|
||||
containers:
|
||||
- name: jaeger
|
||||
image: "{{ .Values.tracing.jaeger.image.repository }}:{{ .Values.tracing.jaeger.image.tag }}"
|
||||
imagePullPolicy: {{ .Values.tracing.jaeger.image.pullPolicy }}
|
||||
ports:
|
||||
- name: otlp-grpc
|
||||
containerPort: 4317
|
||||
protocol: TCP
|
||||
- name: otlp-http
|
||||
containerPort: 4318
|
||||
protocol: TCP
|
||||
- name: jaeger-query
|
||||
containerPort: 16686
|
||||
protocol: TCP
|
||||
- name: jaeger-admin
|
||||
containerPort: 14269
|
||||
protocol: TCP
|
||||
env:
|
||||
- name: COLLECTOR_OTLP_ENABLED
|
||||
value: "true"
|
||||
- name: LOG_LEVEL
|
||||
value: {{ .Values.tracing.jaeger.logLevel | quote }}
|
||||
resources:
|
||||
{{- toYaml .Values.tracing.jaeger.resources | nindent 12 }}
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 14269
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 15
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 14269
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 15
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "gochat.fullname" . }}-jaeger
|
||||
labels:
|
||||
{{- include "gochat.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: jaeger
|
||||
spec:
|
||||
type: ClusterIP
|
||||
ports:
|
||||
- name: otlp-grpc
|
||||
port: 4317
|
||||
targetPort: otlp-grpc
|
||||
protocol: TCP
|
||||
- name: otlp-http
|
||||
port: 4318
|
||||
targetPort: otlp-http
|
||||
protocol: TCP
|
||||
- name: query
|
||||
port: 16686
|
||||
targetPort: jaeger-query
|
||||
protocol: TCP
|
||||
selector:
|
||||
{{- include "gochat.selectorLabels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: jaeger
|
||||
{{- end }}
|
||||
@@ -1,17 +0,0 @@
|
||||
{{- if .Values.metrics.enabled }}
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "gochat.fullname" . }}-metrics
|
||||
labels:
|
||||
{{- include "gochat.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.metrics.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.metrics.service.port }}
|
||||
targetPort: metrics
|
||||
protocol: TCP
|
||||
name: metrics
|
||||
selector:
|
||||
{{- include "gochat.selectorLabels" . | nindent 4 }}
|
||||
{{- end }}
|
||||
@@ -1,97 +0,0 @@
|
||||
{{- if .Values.networkPolicy.enabled }}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: {{ include "gochat.fullname" . }}-allow-ingress
|
||||
labels:
|
||||
{{- include "gochat.labels" . | nindent 4 }}
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
{{- include "gochat.selectorLabels" . | nindent 6 }}
|
||||
policyTypes:
|
||||
- Ingress
|
||||
- Egress
|
||||
ingress:
|
||||
# Allow traffic from Ingress controller (nginx)
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
{{- toYaml .Values.networkPolicy.ingressNamespaceLabels | nindent 12 }}
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: {{ .Values.app.service.port }}
|
||||
# Allow Prometheus scraping for metrics
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
{{- toYaml .Values.networkPolicy.monitoringNamespaceLabels | nindent 12 }}
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: {{ .Values.metrics.service.port }}
|
||||
# Allow internal pod-to-pod communication (app ↔ worker)
|
||||
- from:
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
{{- include "gochat.selectorLabels" . | nindent 12 }}
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: {{ .Values.app.service.port }}
|
||||
---
|
||||
# Egress policy: allow DNS, PostgreSQL, Redis, and outbound HTTPS
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: {{ include "gochat.fullname" . }}-allow-egress
|
||||
labels:
|
||||
{{- include "gochat.labels" . | nindent 4 }}
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
{{- include "gochat.selectorLabels" . | nindent 6 }}
|
||||
policyTypes:
|
||||
- Egress
|
||||
egress:
|
||||
# Allow DNS resolution (kube-dns)
|
||||
- to:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: kube-system
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
k8s-app: kube-dns
|
||||
ports:
|
||||
- protocol: UDP
|
||||
port: 53
|
||||
- protocol: TCP
|
||||
port: 53
|
||||
# Allow PostgreSQL connection
|
||||
- to:
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: postgresql
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 5432
|
||||
# Allow Redis connection
|
||||
- to:
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: redis
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 6379
|
||||
# Allow outbound HTTPS (LLM APIs, webhook callbacks, SMTP)
|
||||
- to:
|
||||
- ipBlock:
|
||||
cidr: 0.0.0.0/0
|
||||
except:
|
||||
- 10.0.0.0/8
|
||||
- 172.16.0.0/12
|
||||
- 192.168.0.0/16
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 443
|
||||
- protocol: TCP
|
||||
port: 587
|
||||
{{- end }}
|
||||
@@ -1,21 +0,0 @@
|
||||
{{- if .Values.tracing.enabled }}
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: {{ include "gochat.fullname" . }}-otel-config
|
||||
labels:
|
||||
{{- include "gochat.labels" . | nindent 4 }}
|
||||
data:
|
||||
OTEL_SERVICE_NAME: "{{ include "gochat.fullname" . }}"
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: "{{ .Values.tracing.otlp.endpoint }}"
|
||||
OTEL_EXPORTER_OTLP_PROTOCOL: "{{ .Values.tracing.otlp.protocol }}"
|
||||
OTEL_TRACES_SAMPLER: "{{ .Values.tracing.sampler.type }}"
|
||||
OTEL_TRACES_SAMPLER_ARG: "{{ .Values.tracing.sampler.arg }}"
|
||||
OTEL_PROPAGATORS: "{{ .Values.tracing.propagators }}"
|
||||
OTEL_RESOURCE_ATTRIBUTES: "service.name={{ include "gochat.fullname" . }},service.version={{ .Values.app.image.tag }},deployment.environment={{ .Values.global.environment }}"
|
||||
OTEL_LOG_LEVEL: "{{ .Values.tracing.logLevel }}"
|
||||
OTEL_EXPORTER_OTLP_TIMEOUT: "{{ .Values.tracing.otlp.timeout }}"
|
||||
OTEL_BSP_SCHEDULE_DELAY: "5000"
|
||||
OTEL_BSP_MAX_QUEUE_SIZE: "2048"
|
||||
OTEL_BSP_MAX_EXPORT_BATCH_SIZE: "512"
|
||||
{{- end }}
|
||||
@@ -1,18 +0,0 @@
|
||||
{{- if .Values.podDisruptionBudget.enabled }}
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: {{ include "gochat.fullname" . }}
|
||||
labels:
|
||||
{{- include "gochat.labels" . | nindent 4 }}
|
||||
spec:
|
||||
{{- if .Values.podDisruptionBudget.minAvailable }}
|
||||
minAvailable: {{ .Values.podDisruptionBudget.minAvailable }}
|
||||
{{- end }}
|
||||
{{- if .Values.podDisruptionBudget.maxUnavailable }}
|
||||
maxUnavailable: {{ .Values.podDisruptionBudget.maxUnavailable }}
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "gochat.selectorLabels" . | nindent 6 }}
|
||||
{{- end }}
|
||||
@@ -1,23 +0,0 @@
|
||||
{{- if .Values.sealedSecrets.enabled }}
|
||||
apiVersion: bitnami.com/v1alpha1
|
||||
kind: SealedSecret
|
||||
metadata:
|
||||
name: {{ include "gochat.fullname" . }}-sealed-secret
|
||||
labels:
|
||||
{{- include "gochat.labels" . | nindent 4 }}
|
||||
annotations:
|
||||
# Sealed Secrets are encrypted with the cluster's public key
|
||||
# Use kubeseal to encrypt: kubeseal --format yaml < secret.yaml > sealed-secret.yaml
|
||||
sealedsecrets.bitnami.com/cluster-wide: "true"
|
||||
spec:
|
||||
encryptedData:
|
||||
{{- range $key, $value := .Values.sealedSecrets.encryptedData }}
|
||||
{{ $key }}: {{ $value }}
|
||||
{{- end }}
|
||||
template:
|
||||
metadata:
|
||||
name: {{ include "gochat.fullname" . }}-secret
|
||||
labels:
|
||||
{{- include "gochat.labels" . | nindent 8 }}
|
||||
type: Opaque
|
||||
{{- end }}
|
||||
@@ -1,11 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: {{ include "gochat.fullname" . }}-secret
|
||||
labels:
|
||||
{{- include "gochat.labels" . | nindent 4 }}
|
||||
type: Opaque
|
||||
data:
|
||||
{{- range $key, $value := .Values.secrets.data }}
|
||||
{{ $key }}: {{ $value | b64enc }}
|
||||
{{- end }}
|
||||
@@ -1,15 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "gochat.fullname" . }}
|
||||
labels:
|
||||
{{- include "gochat.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.app.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.app.service.port }}
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
{{- include "gochat.selectorLabels" . | nindent 4 }}
|
||||
@@ -1,16 +0,0 @@
|
||||
{{- if and .Values.metrics.enabled .Values.metrics.serviceMonitor.enabled }}
|
||||
apiVersion: monitoring.coreos.com/v1
|
||||
kind: ServiceMonitor
|
||||
metadata:
|
||||
name: {{ include "gochat.fullname" . }}-metrics
|
||||
labels:
|
||||
{{- include "gochat.labels" . | nindent 4 }}
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "gochat.selectorLabels" . | nindent 6 }}
|
||||
endpoints:
|
||||
- port: metrics
|
||||
interval: {{ .Values.metrics.serviceMonitor.interval }}
|
||||
path: {{ .Values.metrics.serviceMonitor.path }}
|
||||
{{- end }}
|
||||
@@ -1,35 +0,0 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "gochat.fullname" . }}-worker
|
||||
labels:
|
||||
{{- include "gochat.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: worker
|
||||
spec:
|
||||
replicas: {{ .Values.worker.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "gochat.selectorLabels" . | nindent 6 }}
|
||||
app.kubernetes.io/component: worker
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "gochat.selectorLabels" . | nindent 8 }}
|
||||
app.kubernetes.io/component: worker
|
||||
spec:
|
||||
containers:
|
||||
- name: worker
|
||||
image: "{{ .Values.worker.image.repository }}:{{ .Values.worker.image.tag }}"
|
||||
imagePullPolicy: {{ .Values.worker.image.pullPolicy }}
|
||||
command: {{- toYaml .Values.worker.command | nindent 12 }}
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: {{ include "gochat.fullname" . }}-config
|
||||
- secretRef:
|
||||
name: {{ include "gochat.fullname" . }}-secret
|
||||
resources:
|
||||
{{- toYaml .Values.worker.resources | nindent 12 }}
|
||||
{{- with .Values.app.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
@@ -1,89 +0,0 @@
|
||||
# GoChat Production Values Override
|
||||
global:
|
||||
environment: production
|
||||
|
||||
app:
|
||||
replicaCount: 3
|
||||
resources:
|
||||
limits:
|
||||
cpu: 2000m
|
||||
memory: 1Gi
|
||||
requests:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
|
||||
worker:
|
||||
replicaCount: 3
|
||||
resources:
|
||||
limits:
|
||||
cpu: 2000m
|
||||
memory: 1Gi
|
||||
|
||||
postgresql:
|
||||
primary:
|
||||
persistence:
|
||||
size: 50Gi
|
||||
resources:
|
||||
limits:
|
||||
cpu: 2000m
|
||||
memory: 2Gi
|
||||
|
||||
redis:
|
||||
master:
|
||||
persistence:
|
||||
size: 10Gi
|
||||
configuration: |
|
||||
maxmemory 512mb
|
||||
maxmemory-policy allkeys-lru
|
||||
appendonly yes
|
||||
appendfsync everysec
|
||||
replica:
|
||||
replicaCount: 2
|
||||
|
||||
ingress:
|
||||
hosts:
|
||||
- host: gochat.example.com
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
tls:
|
||||
- secretName: gochat-tls
|
||||
hosts:
|
||||
- gochat.example.com
|
||||
|
||||
# ---- Autoscaling (HPA) — enabled in production ----
|
||||
autoscaling:
|
||||
enabled: true
|
||||
minReplicas: 3
|
||||
maxReplicas: 15
|
||||
targetCPUUtilizationPercentage: 70
|
||||
targetMemoryUtilizationPercentage: 80
|
||||
|
||||
# ---- NetworkPolicy — enabled in production ----
|
||||
networkPolicy:
|
||||
enabled: true
|
||||
|
||||
# ---- PodDisruptionBudget — enabled in production ----
|
||||
podDisruptionBudget:
|
||||
enabled: true
|
||||
minAvailable: 1
|
||||
|
||||
# ---- Sealed Secrets — use in production ----
|
||||
sealedSecrets:
|
||||
enabled: true
|
||||
encryptedData: {}
|
||||
# Generate encrypted data with: kubeseal --format yaml < secret.yaml
|
||||
|
||||
# ---- Distributed Tracing — enabled in production ----
|
||||
tracing:
|
||||
enabled: true
|
||||
sampler:
|
||||
type: parentbased_traceidratio
|
||||
arg: "0.1" # 10% sampling in production
|
||||
|
||||
configMap:
|
||||
data:
|
||||
GOCHAT_ENV: "production"
|
||||
GOCHAT_SERVER_MODE: "release"
|
||||
GOCHAT_LOG_LEVEL: "info"
|
||||
GOCHAT_WORKER_CONCURRENCY: "10"
|
||||
@@ -1,54 +0,0 @@
|
||||
# GoChat Staging Values Override
|
||||
global:
|
||||
environment: staging
|
||||
|
||||
app:
|
||||
replicaCount: 1
|
||||
image:
|
||||
tag: "develop"
|
||||
resources:
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 256Mi
|
||||
requests:
|
||||
cpu: 250m
|
||||
memory: 128Mi
|
||||
|
||||
worker:
|
||||
replicaCount: 1
|
||||
resources:
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 256Mi
|
||||
|
||||
postgresql:
|
||||
primary:
|
||||
persistence:
|
||||
size: 5Gi
|
||||
|
||||
redis:
|
||||
master:
|
||||
persistence:
|
||||
size: 2Gi
|
||||
configuration: |
|
||||
maxmemory 128mb
|
||||
maxmemory-policy allkeys-lru
|
||||
appendonly yes
|
||||
|
||||
ingress:
|
||||
hosts:
|
||||
- host: gochat-staging.example.com
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
tls:
|
||||
- secretName: gochat-staging-tls
|
||||
hosts:
|
||||
- gochat-staging.example.com
|
||||
|
||||
configMap:
|
||||
data:
|
||||
GOCHAT_ENV: "staging"
|
||||
GOCHAT_SERVER_MODE: "debug"
|
||||
GOCHAT_LOG_LEVEL: "debug"
|
||||
FRONTEND_URL: "https://gochat-staging.example.com"
|
||||
@@ -1,278 +0,0 @@
|
||||
# GoChat Helm Chart Values
|
||||
# Reference: Chatwoot Helm chart pattern — app + worker + postgres + redis
|
||||
# Adjust values per environment (dev/staging/prod)
|
||||
|
||||
# ---- Global ----
|
||||
global:
|
||||
environment: production
|
||||
|
||||
# ---- Application ----
|
||||
app:
|
||||
replicaCount: 2
|
||||
image:
|
||||
repository: gochat/gochat
|
||||
tag: "1.0.0"
|
||||
pullPolicy: IfNotPresent
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 3000
|
||||
resources:
|
||||
limits:
|
||||
cpu: 1000m
|
||||
memory: 512Mi
|
||||
requests:
|
||||
cpu: 500m
|
||||
memory: 256Mi
|
||||
# Health probes — references the health endpoints we created
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /live
|
||||
port: 3000
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 30
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /ready
|
||||
port: 3000
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
# Rolling update strategy
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
maxSurge: 1
|
||||
maxUnavailable: 0
|
||||
# Graceful shutdown configuration
|
||||
terminationGracePeriodSeconds: 30
|
||||
preStopDelaySeconds: 5 # Delay before SIGTERM to allow load balancer deregistration
|
||||
# Environment variables from ConfigMap + Secrets
|
||||
envFrom:
|
||||
configMapRef: gochat-config
|
||||
secretRef: gochat-secret
|
||||
# Additional env vars
|
||||
extraEnv: {}
|
||||
# Pod annotations for monitoring
|
||||
podAnnotations:
|
||||
prometheus.io/scrape: "true"
|
||||
prometheus.io/port: "9090"
|
||||
prometheus.io/path: "/metrics"
|
||||
# Affinity for multi-AZ deployment
|
||||
affinity:
|
||||
podAntiAffinity:
|
||||
preferredDuringSchedulingIgnoredDuringExecution:
|
||||
- weight: 100
|
||||
podAffinityTerm:
|
||||
labelSelector:
|
||||
matchExpressions:
|
||||
- key: app.kubernetes.io/name
|
||||
operator: In
|
||||
values:
|
||||
- gochat
|
||||
topologyKey: kubernetes.io/hostname
|
||||
# Node selector
|
||||
nodeSelector: {}
|
||||
# Tolerations
|
||||
tolerations: []
|
||||
|
||||
# ---- Worker ----
|
||||
worker:
|
||||
replicaCount: 2
|
||||
image:
|
||||
repository: gochat/gochat
|
||||
tag: "1.0.0"
|
||||
pullPolicy: IfNotPresent
|
||||
command: ["serve", "--worker-only"]
|
||||
resources:
|
||||
limits:
|
||||
cpu: 1000m
|
||||
memory: 512Mi
|
||||
requests:
|
||||
cpu: 250m
|
||||
memory: 256Mi
|
||||
envFrom:
|
||||
configMapRef: gochat-config
|
||||
secretRef: gochat-secret
|
||||
|
||||
# ---- Metrics sidecar ----
|
||||
metrics:
|
||||
enabled: true
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 9090
|
||||
serviceMonitor:
|
||||
enabled: true
|
||||
interval: 15s
|
||||
path: /metrics
|
||||
|
||||
# ---- PostgreSQL ----
|
||||
postgresql:
|
||||
enabled: true # Set false to use external PostgreSQL
|
||||
image:
|
||||
repository: pgvector/pgvector
|
||||
tag: pg16
|
||||
auth:
|
||||
database: gochat_production
|
||||
username: gochat
|
||||
password: "" # Set via --set or secrets
|
||||
existingSecret: gochat-postgres-secret
|
||||
primary:
|
||||
persistence:
|
||||
enabled: true
|
||||
size: 10Gi
|
||||
storageClass: ""
|
||||
resources:
|
||||
limits:
|
||||
cpu: 1000m
|
||||
memory: 1Gi
|
||||
requests:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
service:
|
||||
port: 5432
|
||||
|
||||
# ---- Redis ----
|
||||
redis:
|
||||
enabled: true # Set false to use external Redis
|
||||
auth:
|
||||
password: "" # Set via --set or secrets
|
||||
existingSecret: gochat-redis-secret
|
||||
master:
|
||||
persistence:
|
||||
enabled: true
|
||||
size: 5Gi
|
||||
storageClass: ""
|
||||
resources:
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
requests:
|
||||
cpu: 250m
|
||||
memory: 256Mi
|
||||
configuration: |
|
||||
maxmemory 512mb
|
||||
maxmemory-policy allkeys-lru
|
||||
appendonly yes
|
||||
appendfsync everysec
|
||||
replica:
|
||||
replicaCount: 1
|
||||
persistence:
|
||||
enabled: true
|
||||
size: 5Gi
|
||||
|
||||
# ---- Ingress ----
|
||||
ingress:
|
||||
enabled: true
|
||||
className: "nginx"
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
nginx.ingress.kubernetes.io/proxy-body-size: "50m"
|
||||
nginx.ingress.kubernetes.io/ssl-redirect: "true"
|
||||
hosts:
|
||||
- host: gochat.example.com
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
tls:
|
||||
- secretName: gochat-tls
|
||||
hosts:
|
||||
- gochat.example.com
|
||||
|
||||
# ---- ConfigMap data ----
|
||||
configMap:
|
||||
data:
|
||||
GOCHAT_ENV: "production"
|
||||
GOCHAT_SERVER_HOST: "0.0.0.0"
|
||||
GOCHAT_SERVER_PORT: "3000"
|
||||
GOCHAT_SERVER_MODE: "release"
|
||||
GOCHAT_LOG_LEVEL: "info"
|
||||
GOCHAT_LOG_FORMAT: "json"
|
||||
GOCHAT_METRICS_ENABLED: "true"
|
||||
GOCHAT_METRICS_PORT: "9090"
|
||||
GOCHAT_WORKER_CONCURRENCY: "10"
|
||||
GOCHAT_FEATURE_CAPTAIN_AI: "false"
|
||||
GOCHAT_FEATURE_CSAT: "true"
|
||||
FRONTEND_URL: "https://gochat.example.com"
|
||||
|
||||
# ---- Autoscaling (HPA) ----
|
||||
autoscaling:
|
||||
enabled: false # Enable for production
|
||||
minReplicas: 2
|
||||
maxReplicas: 10
|
||||
targetCPUUtilizationPercentage: 70
|
||||
targetMemoryUtilizationPercentage: 80
|
||||
behavior:
|
||||
scaleDown:
|
||||
stabilizationWindowSeconds: 300
|
||||
percent: 10
|
||||
periodSeconds: 60
|
||||
scaleUp:
|
||||
stabilizationWindowSeconds: 60
|
||||
percent: 50
|
||||
periodSeconds: 60
|
||||
pods: 2
|
||||
podsPeriodSeconds: 60
|
||||
|
||||
# ---- NetworkPolicy ----
|
||||
networkPolicy:
|
||||
enabled: false # Enable for production
|
||||
ingressNamespaceLabels:
|
||||
kubernetes.io/metadata.name: ingress-nginx
|
||||
monitoringNamespaceLabels:
|
||||
kubernetes.io/metadata.name: monitoring
|
||||
|
||||
# ---- PodDisruptionBudget ----
|
||||
podDisruptionBudget:
|
||||
enabled: false # Enable for production (requires >= 2 replicas)
|
||||
minAvailable: 1 # Keep at least 1 pod available during disruptions
|
||||
# maxUnavailable: 1 # Alternative: allow max 1 pod unavailable
|
||||
|
||||
# ---- Secrets (placeholder — use --set or sealed-secrets) ----
|
||||
secrets:
|
||||
data: {}
|
||||
# POSTGRES_PASSWORD, REDIS_PASSWORD, JWT_SECRET, SMTP_PASSWORD, etc.
|
||||
# MUST be set via --set or external secret management
|
||||
|
||||
# ---- Sealed Secrets / External Secret Management ----
|
||||
sealedSecrets:
|
||||
enabled: false # Enable for production
|
||||
encryptedData: {}
|
||||
externalSecret:
|
||||
enabled: false # Enable for production with External Secrets Operator
|
||||
refreshInterval: 1h
|
||||
secretStoreRef:
|
||||
name: aws-secrets-manager
|
||||
kind: ClusterSecretStore
|
||||
mapping: {}
|
||||
# POSTGRES_PASSWORD: postgres-password
|
||||
# REDIS_PASSWORD: redis-password
|
||||
# JWT_SECRET: jwt-secret
|
||||
|
||||
# ---- Distributed Tracing (OpenTelemetry + Jaeger) ----
|
||||
tracing:
|
||||
enabled: false # Enable for production/staging
|
||||
sampler:
|
||||
type: parentbased_traceidratio # Sampling strategy: always_on, always_off, parentbased_traceidratio
|
||||
arg: "0.1" # Sample 10% of traces in production (adjust per environment)
|
||||
propagators: "tracecontext,baggage" # W3C Trace Context propagation
|
||||
logLevel: info
|
||||
otlp:
|
||||
endpoint: "gochat-jaeger:4317" # OTLP gRPC endpoint (in-cluster Jaeger)
|
||||
protocol: grpc
|
||||
timeout: "10s"
|
||||
jaeger:
|
||||
image:
|
||||
repository: jaegertracing/all-in-one
|
||||
tag: "1.55"
|
||||
pullPolicy: IfNotPresent
|
||||
logLevel: info
|
||||
resources:
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 128Mi
|
||||
@@ -80,17 +80,10 @@ services:
|
||||
GOCHAT_SERVER_HOST: 0.0.0.0
|
||||
GOCHAT_SERVER_PORT: 3000
|
||||
GOCHAT_SERVER_MODE: ${GOCHAT_SERVER_MODE:-debug}
|
||||
GOCHAT_DATABASE_HOST: postgres
|
||||
GOCHAT_DATABASE_PORT: 5432
|
||||
GOCHAT_DATABASE_USER: ${POSTGRES_USER:-gochat}
|
||||
GOCHAT_DATABASE_PASSWORD: ${POSTGRES_PASSWORD:-gochat_dev}
|
||||
GOCHAT_DATABASE_DBNAME: ${POSTGRES_DB:-gochat_dev}
|
||||
GOCHAT_DATABASE_SSLMODE: disable
|
||||
GOCHAT_DATABASE_DSN: postgres://${POSTGRES_USER:-gochat}:${POSTGRES_PASSWORD:-gochat_dev}@postgres:5432/${POSTGRES_DB:-gochat_dev}?sslmode=disable
|
||||
GOCHAT_DATABASE_RUN_MIGRATIONS: "true"
|
||||
GOCHAT_DATABASE_MIGRATIONS_PATH: /app/migrations
|
||||
GOCHAT_REDIS_HOST: redis
|
||||
GOCHAT_REDIS_PORT: 6379
|
||||
GOCHAT_REDIS_PASSWORD: ${REDIS_PASSWORD:-}
|
||||
GOCHAT_REDIS_DSN: ${REDIS_DSN:-redis://redis:6379}
|
||||
GOCHAT_REDIS_POOL_SIZE: 50
|
||||
GOCHAT_SEARCH_ENGINE: meilisearch
|
||||
GOCHAT_SEARCH_HOST: http://meilisearch:7700
|
||||
|
||||
@@ -39,33 +39,17 @@ curl -f http://gochat-green:3000/health
|
||||
docker compose -f docker-compose.prod.yml stop gochat
|
||||
```
|
||||
|
||||
### Step 4: K8s Rolling Update
|
||||
```bash
|
||||
# Kubernetes handles rolling updates automatically via Deployment strategy
|
||||
kubectl set image deployment/gochat gochat=gochat/gochat:${NEW_VERSION}
|
||||
kubectl rollout status deployment/gochat
|
||||
```
|
||||
|
||||
K8s config (in Helm chart):
|
||||
- maxSurge: 1 (one extra pod during rollout)
|
||||
- maxUnavailable: 0 (no pods allowed to be unavailable)
|
||||
- readinessProbe: /ready endpoint
|
||||
- livenessProbe: /live endpoint
|
||||
|
||||
### Step 5: Verification
|
||||
1. Smoke test: hit /health, /ready endpoints
|
||||
### Step 4: Verification
|
||||
1. Smoke test: hit /health endpoint
|
||||
2. Check logs for errors: `docker compose logs gochat --since 5m`
|
||||
3. Verify metrics: Prometheus dashboard should show normal traffic
|
||||
4. Monitor for 15 minutes before finalizing
|
||||
|
||||
### Step 6: Rollback (if needed)
|
||||
### Step 5: Rollback (if needed)
|
||||
```bash
|
||||
# Docker Compose rollback
|
||||
docker compose -f docker-compose.prod.yml exec gochat /app/gochat migrate down ${N}
|
||||
docker compose -f docker-compose.prod.yml up -d --no-deps gochat-${OLD_VERSION}
|
||||
|
||||
# K8s rollback
|
||||
kubectl rollout undo deployment/gochat
|
||||
```
|
||||
|
||||
## Migration Compatibility Rules
|
||||
|
||||
@@ -352,7 +352,7 @@ GoChat 是一个开源的企业级多渠道客服平台,旨在为企业提供
|
||||
| Prometheus 指标导出 | ❌ | 2天 |
|
||||
| Grafana Dashboard 模板 | ❌ | 1天 |
|
||||
| OpenTelemetry 分布式追踪 | ❌ | 3天 |
|
||||
| Kubernetes Helm Chart | ❌ | 3天 |
|
||||
| Kubernetes Helm Chart | ❌ | 已移除 |
|
||||
| JSON 结构化日志 | ⚠️ zap有 | 1天 |
|
||||
| 健康检查增强 | ⚠️ | 1天 |
|
||||
| SLA 管理 | ❌ stub | 3天 |
|
||||
@@ -427,7 +427,7 @@ GoChat 是一个开源的企业级多渠道客服平台,旨在为企业提供
|
||||
**验收标准**:
|
||||
1. Prometheus/Grafana 监控面板可用
|
||||
2. OpenTelemetry 追踪可用
|
||||
3. Kubernetes Helm Chart 可一键部署
|
||||
3. Docker Compose 一键部署可用
|
||||
4. SLA 管理/审计日志可用
|
||||
5. Slack/API Channel 渠道可用
|
||||
6. CI/CD 完整流水线运行
|
||||
|
||||
@@ -311,8 +311,9 @@ gochat/
|
||||
│ + 对应 down.sql 回滚文件
|
||||
│
|
||||
├── deploy/ # 部署配置
|
||||
│ ├── k8s/ # Kubernetes manifests
|
||||
│ ├── docker/ # Docker 相关
|
||||
│ ├── docker/ # Dockerfile + docker-compose
|
||||
│ ├── quickstart/ # 一键启动 Compose 栈
|
||||
│ ├── fluentd/ # 日志收集
|
||||
│
|
||||
├── tests/ # 测试目录
|
||||
│ ├── e2e/ # 端到端测试
|
||||
@@ -1162,13 +1163,7 @@ docker-compose.prod.yml → 生产 (健康检查 + 资源限制)
|
||||
docker-compose.test.yml → 测试 (独立PG + 覆盖率)
|
||||
```
|
||||
|
||||
### 12.2 Kubernetes 部署
|
||||
|
||||
```
|
||||
deploy/k8s/ → Deployment + Service + ConfigMap + Secret manifests
|
||||
```
|
||||
|
||||
### 12.3 环境配置优先级
|
||||
### 12.2 环境配置优先级
|
||||
|
||||
```
|
||||
.env (最高) → configs/production.yaml → configs/default.yaml → 程序默认值
|
||||
|
||||
@@ -473,7 +473,7 @@ gochat/
|
||||
│ ├── docker/
|
||||
│ │ ├── Dockerfile
|
||||
│ │ └── docker-compose.yaml
|
||||
│ └── k8s/ # Kubernetes配置(可选)
|
||||
│ └── quickstart/ # 一键启动 Compose 栈
|
||||
│
|
||||
├── go.mod # Go模块定义
|
||||
├── go.sum # Go依赖锁定
|
||||
|
||||
Reference in New Issue
Block a user