feat: publish standalone worker

Separate worker packaging and service lifecycle from the control plane.
Этот коммит содержится в:
Gleb Tv
2026-07-13 17:55:14 +03:00
Коммит 2c7a0236da
309 изменённых файлов: 44004 добавлений и 0 удалений

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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 поставляемый Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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)
}