feat: publish standalone worker
Separate worker packaging and service lifecycle from the control plane.
Этот коммит содержится в:
21
config/application/init.go
Обычный файл
21
config/application/init.go
Обычный файл
@@ -0,0 +1,21 @@
|
||||
// Package application provides functionality.
|
||||
package application
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
_ "rsgit.ru/rsmon/rsmon/config/env" // Import to ensure .env is loaded before reading env vars
|
||||
)
|
||||
|
||||
// Env provides functionality.
|
||||
var Env string
|
||||
|
||||
func init() {
|
||||
Env = os.Getenv("RSMON_ENV")
|
||||
if Env == "" {
|
||||
Env = os.Getenv("GO_ENV")
|
||||
}
|
||||
if Env == "" {
|
||||
Env = "development"
|
||||
}
|
||||
}
|
||||
69
config/credis/redis.go
Обычный файл
69
config/credis/redis.go
Обычный файл
@@ -0,0 +1,69 @@
|
||||
// Package credis provides functionality.
|
||||
package credis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/rbcervilla/redisstore/v8"
|
||||
|
||||
_ "rsgit.ru/rsmon/rsmon/config/env" // Import to ensure .env is loaded before reading env vars
|
||||
)
|
||||
|
||||
// Redis provides functionality.
|
||||
var Redis *redis.Client
|
||||
|
||||
// Store provides functionality.
|
||||
var Store *redisstore.RedisStore
|
||||
|
||||
func envOrDefault(key, defaultVal string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return defaultVal
|
||||
}
|
||||
|
||||
// Init provides functionality.
|
||||
func Init() {
|
||||
host := envOrDefault("REDIS_HOST", "localhost")
|
||||
portStr := envOrDefault("REDIS_PORT", "6379")
|
||||
password := os.Getenv("REDIS_PASSWORD")
|
||||
dbStr := envOrDefault("REDIS_DATABASE", "0")
|
||||
|
||||
port, _ := strconv.Atoi(portStr)
|
||||
database, _ := strconv.Atoi(dbStr)
|
||||
|
||||
options := redis.Options{
|
||||
Addr: host + ":" + strconv.Itoa(port),
|
||||
DB: database,
|
||||
}
|
||||
if password != "" {
|
||||
options.Password = password
|
||||
}
|
||||
Redis = redis.NewClient(&options)
|
||||
|
||||
isTest := os.Getenv("RSMON_ENV") == "test" || os.Getenv("GO_ENV") == "test" || os.Getenv("CI") == "true"
|
||||
|
||||
var err error
|
||||
Store, err = redisstore.NewRedisStore(context.Background(), Redis)
|
||||
if err != nil {
|
||||
if isTest {
|
||||
log.Println("Warning: Redis not available in test mode, sessions will not work")
|
||||
Store = nil
|
||||
return
|
||||
}
|
||||
log.Fatal("failed to create redis store: ", err)
|
||||
}
|
||||
|
||||
Store.KeyPrefix("session_")
|
||||
Store.Options(sessions.Options{
|
||||
Path: "/",
|
||||
MaxAge: 86400 * 60,
|
||||
})
|
||||
|
||||
log.Printf("Redis connected: %s:%d db=%d", host, port, database)
|
||||
}
|
||||
80
config/database/database.go
Обычный файл
80
config/database/database.go
Обычный файл
@@ -0,0 +1,80 @@
|
||||
// Package database provides database initialization and access for RSMon.
|
||||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
|
||||
"rsgit.ru/rsmon/rsmon/app/models"
|
||||
_ "rsgit.ru/rsmon/rsmon/config/env" // Import to ensure .env is loaded before reading env vars
|
||||
)
|
||||
|
||||
// db Gorm DB
|
||||
var db *gorm.DB
|
||||
|
||||
// Logger provides functionality.
|
||||
var Logger logger.Interface
|
||||
|
||||
func envOrDefault(key, fallbackKey, defaultVal string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
if fallbackKey != "" {
|
||||
if v := os.Getenv(fallbackKey); v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return defaultVal
|
||||
}
|
||||
|
||||
// Init provides functionality.
|
||||
func Init() {
|
||||
host := envOrDefault("DATABASE_HOST", "POSTGRES_HOST", "localhost")
|
||||
portStr := envOrDefault("DATABASE_PORT", "POSTGRES_PORT", "5432")
|
||||
user := envOrDefault("DATABASE_USER", "POSTGRES_USER", "rsmon")
|
||||
pass := envOrDefault("DATABASE_PASSWORD", "POSTGRES_PASSWORD", "rsmon")
|
||||
dbname := envOrDefault("DATABASE_NAME", "POSTGRES_DB", "rsmon_development")
|
||||
dbLog := envOrDefault("DATABASE_LOG", "", "false")
|
||||
|
||||
port := 5432
|
||||
_, _ = fmt.Sscanf(portStr, "%d", &port)
|
||||
|
||||
connstr := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable",
|
||||
host, port, user, pass, dbname)
|
||||
|
||||
logMode := logger.Silent
|
||||
if dbLog == "true" || dbLog == "1" {
|
||||
logMode = logger.Info
|
||||
}
|
||||
|
||||
cfg := &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logMode),
|
||||
}
|
||||
|
||||
pg := postgres.Open(connstr)
|
||||
var err error
|
||||
db, err = gorm.Open(pg, cfg)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
sqlDB.SetMaxIdleConns(10)
|
||||
sqlDB.SetMaxOpenConns(10)
|
||||
sqlDB.SetConnMaxLifetime(time.Hour)
|
||||
|
||||
models.RegisterCallbacks(db)
|
||||
db = db.Set("gorm:association_autoupdate", false)
|
||||
models.SetDB(db)
|
||||
|
||||
log.Printf("Database connected: %s@%s:%d/%s (log=%s)", user, host, port, dbname, dbLog)
|
||||
}
|
||||
95
config/env/env.go
поставляемый
Обычный файл
95
config/env/env.go
поставляемый
Обычный файл
@@ -0,0 +1,95 @@
|
||||
// Package env provides functionality.
|
||||
package env
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
func init() {
|
||||
LoadEnvFiles()
|
||||
}
|
||||
|
||||
// LoadEnvFiles provides functionality.
|
||||
func LoadEnvFiles() {
|
||||
env := getEnvironment()
|
||||
envFile := ".env." + env
|
||||
|
||||
if tryLoadEnv(envFile, env) {
|
||||
return
|
||||
}
|
||||
|
||||
if tryLoadEnv(".env", "") {
|
||||
return
|
||||
}
|
||||
|
||||
if env != "test" {
|
||||
log.Printf("Warning: No .env file found")
|
||||
} else {
|
||||
log.Printf("Note: No .env file loaded in test environment (this is normal)")
|
||||
}
|
||||
}
|
||||
|
||||
func tryLoadEnv(filename, envName string) bool {
|
||||
if godotenv.Load(filename) == nil {
|
||||
if envName != "" {
|
||||
log.Printf(".env.%s file loaded successfully", envName)
|
||||
} else {
|
||||
log.Println(".env file loaded successfully")
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
if cwd := os.Getenv("CWD"); cwd != "" {
|
||||
path := filepath.Join(cwd, filename)
|
||||
if godotenv.Load(path) == nil {
|
||||
if envName != "" {
|
||||
log.Printf(".env.%s file loaded from %s", envName, cwd)
|
||||
} else {
|
||||
log.Printf(".env file loaded from %s", cwd)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
dir, _ := os.Getwd()
|
||||
for {
|
||||
path := filepath.Join(dir, filename)
|
||||
if godotenv.Load(path) == nil {
|
||||
if envName != "" {
|
||||
log.Printf(".env.%s file loaded from %s", envName, dir)
|
||||
} else {
|
||||
log.Printf(".env file loaded from %s", dir)
|
||||
}
|
||||
return true
|
||||
}
|
||||
parent := filepath.Dir(dir)
|
||||
if parent == dir {
|
||||
break
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
|
||||
break
|
||||
}
|
||||
dir = parent
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// getEnvironment determines the current environment from various environment variables
|
||||
func getEnvironment() string {
|
||||
// Check RSMON_ENV first (rsmon-specific)
|
||||
if env := os.Getenv("RSMON_ENV"); env != "" {
|
||||
return env
|
||||
}
|
||||
// Then check GO_ENV (standard)
|
||||
if env := os.Getenv("GO_ENV"); env != "" {
|
||||
return env
|
||||
}
|
||||
|
||||
// Default to development
|
||||
return "development"
|
||||
}
|
||||
172
config/secrets/secrets.go
Обычный файл
172
config/secrets/secrets.go
Обычный файл
@@ -0,0 +1,172 @@
|
||||
// Package secrets provides functionality.
|
||||
package secrets
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/go-yaml/yaml"
|
||||
|
||||
"rsgit.ru/rsmon/rsmon/config/application"
|
||||
_ "rsgit.ru/rsmon/rsmon/config/env" // Import to ensure .env is loaded before reading env vars
|
||||
)
|
||||
|
||||
const envTest = "test"
|
||||
|
||||
// SecretsBase provides functionality.
|
||||
type SecretsBase struct {
|
||||
URL string `yaml:"url"`
|
||||
InfluxToken string `yaml:"influx_token"`
|
||||
WorkerAdminSecret string `yaml:"worker_admin_secret"`
|
||||
}
|
||||
|
||||
// SecretsCrypto provides functionality.
|
||||
type SecretsCrypto struct {
|
||||
SessionKey string `yaml:"session_key"`
|
||||
CsrfKey string `yaml:"csrf_key"`
|
||||
Pepper string `yaml:"pepper"`
|
||||
}
|
||||
|
||||
// SecretsCrm provides functionality.
|
||||
type SecretsCrm struct {
|
||||
URL string `yaml:"url"`
|
||||
IngressToken string `yaml:"ingress_token"`
|
||||
}
|
||||
|
||||
// SecretsData provides functionality.
|
||||
type SecretsData struct {
|
||||
Base *SecretsBase `yaml:"base"`
|
||||
Crypto *SecretsCrypto `yaml:"crypto"`
|
||||
Crm *SecretsCrm `yaml:"crm"`
|
||||
}
|
||||
|
||||
var (
|
||||
// Data provides functionality.
|
||||
Data SecretsData
|
||||
// Base provides functionality.
|
||||
Base *SecretsBase
|
||||
// Crypto provides cryptography configuration.
|
||||
Crypto *SecretsCrypto
|
||||
// Crm provides CRM configuration.
|
||||
Crm *SecretsCrm
|
||||
)
|
||||
|
||||
func envFirst(keys ...string) string {
|
||||
for _, key := range keys {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func buildSecretsFromEnv() SecretsData {
|
||||
baseURL := envFirst("BASE_URL", "APP_BASE_URL")
|
||||
if baseURL == "" {
|
||||
baseURL = "http://localhost:7401"
|
||||
}
|
||||
|
||||
sessionKey := envFirst("SESSION_KEY")
|
||||
pepper := envFirst("PEPPER")
|
||||
csrfKey := envFirst("CSRF_KEY")
|
||||
|
||||
return SecretsData{
|
||||
Base: &SecretsBase{
|
||||
URL: baseURL,
|
||||
InfluxToken: envFirst("INFLUX_TOKEN"),
|
||||
WorkerAdminSecret: envFirst("WORKER_ADMIN_SECRET", "RSMON_ADMIN_SECRET"),
|
||||
},
|
||||
Crypto: &SecretsCrypto{
|
||||
SessionKey: sessionKey,
|
||||
CsrfKey: csrfKey,
|
||||
Pepper: pepper,
|
||||
},
|
||||
Crm: &SecretsCrm{
|
||||
URL: envFirst("CRM_URL", "SENSEI_CRM_URL"),
|
||||
IngressToken: envFirst("CRM_INGRESS_TOKEN", "SENSEI_CRM_INGRESS_TOKEN"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func assign(data SecretsData) {
|
||||
Data = data
|
||||
Base = Data.Base
|
||||
Crypto = Data.Crypto
|
||||
Crm = Data.Crm
|
||||
}
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
|
||||
// Try multiple paths for secrets.yml
|
||||
cwd := os.Getenv("CWD")
|
||||
if cwd == "" {
|
||||
cwd = "."
|
||||
}
|
||||
|
||||
// Possible paths to check - try relative to CWD and from module root
|
||||
paths := []string{
|
||||
filepath.Join(cwd, "config", "secrets.yml"),
|
||||
"config/secrets.yml",
|
||||
"../../config/secrets.yml",
|
||||
"../../../config/secrets.yml",
|
||||
"../../../../config/secrets.yml",
|
||||
}
|
||||
|
||||
var dat []byte
|
||||
for _, path := range paths {
|
||||
dat, err = os.ReadFile(path)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// If we're in test mode and no secrets file found, use defaults
|
||||
// Check multiple test indicators since init() order is unpredictable
|
||||
isTest := os.Getenv("RSMON_ENV") == envTest ||
|
||||
os.Getenv("GO_ENV") == envTest ||
|
||||
os.Getenv("CI") == "true" ||
|
||||
application.Env == envTest
|
||||
|
||||
if err != nil && isTest {
|
||||
log.Println("No secrets.yml found in test environment, using defaults")
|
||||
// Set default values for test environment
|
||||
Base = &SecretsBase{
|
||||
URL: "http://localhost:7401",
|
||||
InfluxToken: "",
|
||||
WorkerAdminSecret: "test-secret",
|
||||
}
|
||||
Crypto = &SecretsCrypto{
|
||||
SessionKey: "abc",
|
||||
CsrfKey: "bGludXhoaW50LmNvbQo=",
|
||||
Pepper: "abc",
|
||||
}
|
||||
Crm = &SecretsCrm{URL: "", IngressToken: ""}
|
||||
assign(SecretsData{
|
||||
Base: Base,
|
||||
Crypto: Crypto,
|
||||
Crm: Crm,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Println("No secrets.yml found, using environment variables")
|
||||
assign(buildSecretsFromEnv())
|
||||
return
|
||||
}
|
||||
|
||||
var AllSecrets map[string]SecretsData
|
||||
|
||||
err = yaml.Unmarshal(dat, &AllSecrets)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
env := application.Env
|
||||
if env == "" {
|
||||
env = "development"
|
||||
}
|
||||
assign(AllSecrets[env])
|
||||
}
|
||||
47
config/translator/translator.go
Обычный файл
47
config/translator/translator.go
Обычный файл
@@ -0,0 +1,47 @@
|
||||
// Package translator provides i18n translation utilities for RSMon.
|
||||
package translator
|
||||
|
||||
import (
|
||||
"github.com/go-playground/locales"
|
||||
ruData "github.com/go-playground/locales/ru"
|
||||
ut "github.com/go-playground/universal-translator"
|
||||
)
|
||||
|
||||
var (
|
||||
uni *ut.UniversalTranslator
|
||||
// Translator provides functionality.
|
||||
Translator ut.Translator
|
||||
|
||||
// Translator provides functionality.
|
||||
)
|
||||
|
||||
func init() {
|
||||
ru := ruData.New()
|
||||
uni = ut.New(ru, ru, ru)
|
||||
|
||||
Translator, _ = uni.GetTranslator("ru")
|
||||
|
||||
addData()
|
||||
}
|
||||
|
||||
func addData() {
|
||||
_ = Translator.AddCardinal("monitor", "доступен {0} монитор", locales.PluralRuleOne, false)
|
||||
_ = Translator.AddCardinal("monitor", "доступно {0} монитора", locales.PluralRuleFew, false)
|
||||
_ = Translator.AddCardinal("monitor", "доступно {0} мониторов", locales.PluralRuleMany, false)
|
||||
_ = Translator.AddCardinal("monitor", "доступно {0} мониторов", locales.PluralRuleOther, false)
|
||||
|
||||
_ = Translator.AddCardinal("expires", "истекает {0} монитор", locales.PluralRuleOne, false)
|
||||
_ = Translator.AddCardinal("expires", "истекает {0} монитора", locales.PluralRuleFew, false)
|
||||
_ = Translator.AddCardinal("expires", "истекает {0} мониторов", locales.PluralRuleMany, false)
|
||||
_ = Translator.AddCardinal("expires", "истекают {0} мониторов", locales.PluralRuleOther, false)
|
||||
|
||||
_ = Translator.AddCardinal("hours", "{0} час", locales.PluralRuleOne, false)
|
||||
_ = Translator.AddCardinal("hours", "{0} часа", locales.PluralRuleFew, false)
|
||||
_ = Translator.AddCardinal("hours", "{0} часов", locales.PluralRuleMany, false)
|
||||
_ = Translator.AddCardinal("hours", "{0} часов", locales.PluralRuleOther, false)
|
||||
|
||||
_ = Translator.AddCardinal("minutes", "{0} минуту", locales.PluralRuleOne, false)
|
||||
_ = Translator.AddCardinal("minutes", "{0} минуты", locales.PluralRuleFew, false)
|
||||
_ = Translator.AddCardinal("minutes", "{0} минут", locales.PluralRuleMany, false)
|
||||
_ = Translator.AddCardinal("minutes", "{0} минут", locales.PluralRuleOther, false)
|
||||
}
|
||||
Ссылка в новой задаче
Block a user