chore: initial import from sless/shared-sqs (v0.1.14)
- Standalone SQS-service repository - Multi-tenant message queue service, AWS SQS compatible - Based on GoAws, with mutable tenants, auth, WebUI, Redis persistence - Ready for independent development and deployment - See doc/ and README.md for architecture and usage
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
// app/cmd/goaws.go
|
||||
// Entry point — shared-sqs server
|
||||
// Updated: 2026-04-10 — добавлена Redis persistence (write-through cache)
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"shared-sqs/app/conf"
|
||||
"shared-sqs/app/gosqs"
|
||||
"shared-sqs/app/models"
|
||||
"shared-sqs/app/persistence"
|
||||
"shared-sqs/app/router"
|
||||
"shared-sqs/app/tenant"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var configFile string
|
||||
var adminToken string
|
||||
var port string
|
||||
var debug bool
|
||||
var loglevel string
|
||||
|
||||
flag.StringVar(&configFile, "config", "", "config file location")
|
||||
flag.StringVar(&adminToken, "admin-token", "", "admin API bearer token")
|
||||
flag.StringVar(&port, "port", "4100", "listen port")
|
||||
flag.BoolVar(&debug, "debug", false, "set debug log level")
|
||||
flag.StringVar(&loglevel, "loglevel", "info", "log level (info, debug, warn, error)")
|
||||
flag.Parse()
|
||||
|
||||
log.SetFormatter(&log.JSONFormatter{})
|
||||
log.SetOutput(os.Stdout)
|
||||
|
||||
if debug {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
} else {
|
||||
level, err := log.ParseLevel(loglevel)
|
||||
if err != nil {
|
||||
log.SetLevel(log.InfoLevel)
|
||||
log.Warnf("Failed to parse loglevel %v, defaulting to info", loglevel)
|
||||
} else {
|
||||
log.SetLevel(level)
|
||||
}
|
||||
}
|
||||
|
||||
// Admin token: flag > env SHARED_SQS_ADMIN_TOKEN > fatal (Trap #13)
|
||||
if adminToken == "" {
|
||||
adminToken = os.Getenv("SHARED_SQS_ADMIN_TOKEN")
|
||||
}
|
||||
if adminToken == "" {
|
||||
log.Fatal("admin token required: use --admin-token flag or SHARED_SQS_ADMIN_TOKEN env var")
|
||||
}
|
||||
|
||||
// Загрузить конфиг (очереди, env — без SNS)
|
||||
env := "Local"
|
||||
if flag.NArg() > 0 {
|
||||
env = flag.Arg(0)
|
||||
}
|
||||
conf.LoadYamlConfig(configFile, env)
|
||||
|
||||
if models.CurrentEnvironment.LogToFile {
|
||||
filename := models.CurrentEnvironment.LogFile
|
||||
file, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
|
||||
if err == nil {
|
||||
log.SetOutput(file)
|
||||
} else {
|
||||
log.Infof("Failed to log to file: %s, using default stdout", filename)
|
||||
}
|
||||
}
|
||||
|
||||
// Инициализация in-memory TenantStore
|
||||
tenantStore := tenant.NewTenantStore()
|
||||
|
||||
// Подключение к Redis (если задан REDIS_ADDR)
|
||||
// При ошибке — предупреждение, но продолжаем в memory-only режиме
|
||||
redisAddr := os.Getenv("REDIS_ADDR")
|
||||
redisUser := os.Getenv("REDIS_USER")
|
||||
redisPass := os.Getenv("REDIS_PASSWORD")
|
||||
if redisAddr != "" {
|
||||
if err := persistence.Connect(redisAddr, redisUser, redisPass); err != nil {
|
||||
log.Warnf("Не удалось подключиться к Redis: %v — работаем в memory-only режиме", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Восстановление состояния из Redis (тенанты + очереди)
|
||||
if persistence.Client != nil {
|
||||
// Загружаем тенантов
|
||||
tenantsRaw, err := persistence.LoadAllTenantsRaw()
|
||||
if err != nil {
|
||||
log.Warnf("Ошибка загрузки тенантов из Redis: %v", err)
|
||||
} else {
|
||||
for _, jsonBytes := range tenantsRaw {
|
||||
var t tenant.Tenant
|
||||
if err := json.Unmarshal(jsonBytes, &t); err != nil {
|
||||
log.Errorf("Ошибка десериализации тенанта: %v", err)
|
||||
continue
|
||||
}
|
||||
tenantStore.LoadTenant(&t)
|
||||
}
|
||||
}
|
||||
// Загружаем очереди
|
||||
queues, err := persistence.LoadAllQueues()
|
||||
if err != nil {
|
||||
log.Warnf("Ошибка загрузки очередей из Redis: %v", err)
|
||||
} else {
|
||||
models.SyncQueues.Lock()
|
||||
for k, q := range queues {
|
||||
models.SyncQueues.Queues[k] = q
|
||||
}
|
||||
models.SyncQueues.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// Автосид демо-данных при SHARED_SQS_SEED_DEMO=true
|
||||
if os.Getenv("SHARED_SQS_SEED_DEMO") == "true" {
|
||||
seedDemoData(tenantStore)
|
||||
}
|
||||
// Роутер с tenant auth и admin API
|
||||
r := router.New(tenantStore, adminToken)
|
||||
|
||||
// PeriodicTasks — visibility timeout, DLQ, deduplication
|
||||
quit := make(chan bool)
|
||||
go gosqs.PeriodicTasks(1*time.Second, quit)
|
||||
|
||||
// HTTP сервер с таймаутами
|
||||
srv := &http.Server{
|
||||
Addr: "0.0.0.0:" + port,
|
||||
Handler: r,
|
||||
ReadTimeout: 30 * time.Second,
|
||||
WriteTimeout: 35 * time.Second, // чуть больше чем max WaitTimeSeconds (20s)
|
||||
IdleTimeout: 60 * time.Second,
|
||||
}
|
||||
|
||||
// Запуск в горутине для graceful shutdown
|
||||
serverErr := make(chan error, 1)
|
||||
go func() {
|
||||
log.Infof("shared-sqs listening on 0.0.0.0:%s", port)
|
||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
serverErr <- err
|
||||
}
|
||||
}()
|
||||
|
||||
// Graceful shutdown по SIGTERM/SIGINT (Trap #13)
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, syscall.SIGTERM, syscall.SIGINT)
|
||||
|
||||
select {
|
||||
case sig := <-sigCh:
|
||||
log.Infof("Received signal %s, shutting down", sig)
|
||||
case err := <-serverErr:
|
||||
log.Fatalf("Server error: %v", err)
|
||||
}
|
||||
|
||||
// Остановить PeriodicTasks
|
||||
close(quit)
|
||||
|
||||
// Дать 10 секунд на завершение текущих HTTP запросов
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
if err := srv.Shutdown(ctx); err != nil {
|
||||
log.Errorf("Server shutdown error: %v", err)
|
||||
}
|
||||
log.Info("shared-sqs stopped")
|
||||
}
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
// app/cmd/seed.go
|
||||
// Created: 2026-04-09
|
||||
// Updated: 2026-04-09 — фиксированные credentials для demo-tenant (BYOC)
|
||||
// Автосид демо-данных при старте через SHARED_SQS_SEED_DEMO=true.
|
||||
// Создаёт тенанта demo-service с 5 очередями и демо-сообщениями.
|
||||
//
|
||||
// DEMO CREDENTIALS — только для тестового стенда.
|
||||
// Тенант demo-service изолирован: видит только свои очереди, не имеет доступа к
|
||||
// admin API и к очередям других тенантов. Credentials открыты намеренно — стенд публичный.
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/md5" //nolint:gosec — MD5 используется для SQS-совместимости, не для безопасности
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"shared-sqs/app/models"
|
||||
"shared-sqs/app/tenant"
|
||||
)
|
||||
|
||||
// Фиксированные credentials демо-тенанта.
|
||||
// Открыты намеренно — тестовый стенд.
|
||||
// Тенант ограничен 10 очередями и не имеет прав admin API.
|
||||
const (
|
||||
demoTenantID = "t-demo-shared-sqs-ngcloud"
|
||||
demoAccessKey = "SSAK-demo-shared-sqs"
|
||||
demoSecretKey = "demo-secret-key-shared-sqs-ngcloud-2026"
|
||||
demoTenantName = "demo-service"
|
||||
demoMaxQueues = 10
|
||||
)
|
||||
|
||||
// seedDemoData создаёт тенанта demo-service с очередями и сообщениями.
|
||||
// Вызывается при SHARED_SQS_SEED_DEMO=true при старте сервера.
|
||||
func seedDemoData(store *tenant.TenantStore) {
|
||||
t, err := store.CreateFixed(demoTenantName, demoMaxQueues, demoTenantID, demoAccessKey, demoSecretKey)
|
||||
if err != nil {
|
||||
log.Warnf("seed: не удалось создать demo-tenant: %v", err)
|
||||
return
|
||||
}
|
||||
log.Infof("seed: создан тенант %s (AccessKey=%s)", t.ID, t.AccessKey)
|
||||
|
||||
// Демо-очереди с набором сообщений
|
||||
queues := []struct {
|
||||
name string
|
||||
msgs []string
|
||||
}{
|
||||
{
|
||||
"orders",
|
||||
[]string{
|
||||
`{"order_id":"1001","amount":99.99,"status":"pending"}`,
|
||||
`{"order_id":"1002","amount":14.50,"status":"completed"}`,
|
||||
`{"order_id":"1003","amount":299.00,"status":"processing"}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
"notifications",
|
||||
[]string{
|
||||
`{"to":"user@example.com","text":"Welcome to the service!"}`,
|
||||
`{"to":"admin@example.com","text":"New user signed up"}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
"emails",
|
||||
[]string{
|
||||
`{"subject":"Invoice #42","body":"See attachment","to":"billing@example.com"}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
"uploads",
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"user-events",
|
||||
[]string{
|
||||
`{"event":"login","user_id":"u-123","ts":1744000000}`,
|
||||
`{"event":"logout","user_id":"u-123","ts":1744003600}`,
|
||||
`{"event":"purchase","user_id":"u-456","item_id":"prod-7","ts":1744005000}`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, q := range queues {
|
||||
key := t.AccessKey + ":" + q.name
|
||||
|
||||
// URL и ARN формируем по тому же шаблону что gosqs/tenant_helpers.go:tenantQueueURL/tenantQueueARN
|
||||
// Иначе AWS CLI получает пустой QueueUrl в ListQueues и не может работать с очередью.
|
||||
env := models.CurrentEnvironment
|
||||
var queueURL string
|
||||
if env.Region != "" {
|
||||
queueURL = "http://" + env.Region + "." + env.Host + ":" + env.Port + "/" + t.ID + "/" + q.name
|
||||
} else {
|
||||
queueURL = "http://" + env.Host + ":" + env.Port + "/" + t.ID + "/" + q.name
|
||||
}
|
||||
queueARN := "arn:aws:sqs:" + env.Region + ":" + t.ID + ":" + q.name
|
||||
|
||||
msgs := make([]models.SqsMessage, 0, len(q.msgs))
|
||||
for _, body := range q.msgs {
|
||||
//nolint:gosec — MD5 здесь для совместимости с AWS SQS протоколом
|
||||
sum := md5.Sum([]byte(body)) //nolint:gosec
|
||||
msgs = append(msgs, models.SqsMessage{
|
||||
MessageBody: body,
|
||||
Uuid: uuid.NewString(),
|
||||
MD5OfMessageBody: fmt.Sprintf("%x", sum),
|
||||
SentTime: time.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
models.SyncQueues.Lock()
|
||||
models.SyncQueues.Queues[key] = &models.Queue{
|
||||
Name: q.name,
|
||||
URL: queueURL,
|
||||
Arn: queueARN,
|
||||
VisibilityTimeout: 30,
|
||||
MaximumMessageSize: 262144,
|
||||
MessageRetentionPeriod: 345600,
|
||||
Messages: msgs,
|
||||
Duplicates: make(map[string]time.Time),
|
||||
}
|
||||
models.SyncQueues.Unlock()
|
||||
|
||||
log.Infof("seed: очередь %s (%d сообщений)", q.name, len(q.msgs))
|
||||
}
|
||||
|
||||
log.Infof("seed: демо-данные готовы — тенант %s, 5 очередей", t.Name)
|
||||
}
|
||||
Reference in New Issue
Block a user