From 4dd95eee808e14db6a96cbb2e8562ec5289f1226 Mon Sep 17 00:00:00 2001 From: MD Towhidul Islam Date: Tue, 12 Aug 2025 23:20:55 +0600 Subject: [PATCH 01/21] GE4-19 - Add Transaction # Refactor context handling in services and repositories; implement transaction middleware for improved database operations --- cmd/serve.go | 2 - domain/repository.go | 2 + domain/service.go | 2 +- domain/user.go | 3 +- middleware/auth.go | 2 +- middleware/transaction.go | 47 +++++++++++++++++++ repositories/sql_common.go | 80 ++++++++++++++++++++++----------- repositories/user.go | 12 +---- server/server.go | 8 ++-- services/curl.go | 11 +++-- services/deepseek.go | 11 ++--- services/gemini.go | 16 +++---- services/notifier/dispatcher.go | 3 -- services/service.go | 21 +++++---- services/user.go | 7 +-- utils/errutil/error_handler.go | 4 +- 16 files changed, 142 insertions(+), 89 deletions(-) create mode 100644 middleware/transaction.go diff --git a/cmd/serve.go b/cmd/serve.go index 8892624..ad49b64 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -2,7 +2,6 @@ package cmd import ( "NotificationManagement/config" - "NotificationManagement/conn" "NotificationManagement/logger" "NotificationManagement/server" "context" @@ -23,7 +22,6 @@ var serveCmd = &cobra.Command{ Long: `Start the notification management server with the specified configuration`, Run: func(cmd *cobra.Command, args []string) { app := fx.New( - fx.Provide(conn.NewDB), server.Module, fx.Invoke(func(lc fx.Lifecycle, e *echo.Echo) { e.GET("/health", func(c echo.Context) error { diff --git a/domain/repository.go b/domain/repository.go index 7f851fd..9f6faef 100644 --- a/domain/repository.go +++ b/domain/repository.go @@ -2,11 +2,13 @@ package domain import ( "context" + "gorm.io/gorm" ) type Repository[T any, ID comparable] interface { GetDB(ctx context.Context) *gorm.DB + WithTx(tx *gorm.DB) Repository[T, ID] Create(ctx context.Context, entity *T) error GetByID(ctx context.Context, id ID, preloads *[]string) (*T, error) GetByIDs(ctx context.Context, ids []uint, preloads *[]string) ([]T, error) diff --git a/domain/service.go b/domain/service.go index 9f98661..0cf7dc1 100644 --- a/domain/service.go +++ b/domain/service.go @@ -11,5 +11,5 @@ type CommonService[T any] interface { UpdateModel(c context.Context, id uint, model *T) (*T, error) DeleteModel(c context.Context, id uint) error GetInstance() CommonService[T] - GetContext() context.Context + ProcessContext(context.Context) context.Context } diff --git a/domain/user.go b/domain/user.go index d0e99a3..feda944 100644 --- a/domain/user.go +++ b/domain/user.go @@ -8,12 +8,11 @@ import ( type UserRepository interface { Repository[models.User, uint] FindByKeycloakID(keycloakID string, ctx context.Context) (*models.User, error) - FindByEmail(email string) (*models.User, error) } type UserService interface { CommonService[models.User] - RegisterOrUpdateUser(user *models.User) (*models.User, error) + RegisterOrUpdateUser(ctx context.Context, user *models.User) (*models.User, error) } type UserController interface { diff --git a/middleware/auth.go b/middleware/auth.go index 3060d28..28554a3 100644 --- a/middleware/auth.go +++ b/middleware/auth.go @@ -102,7 +102,7 @@ func KeycloakMiddleware(userService domain.UserService) echo.MiddlewareFunc { Roles: strings.Join(roles, ","), } - user, err := userService.RegisterOrUpdateUser(user) + user, err := userService.RegisterOrUpdateUser(c.Request().Context(), user) if err != nil { return errutil.NewAppError(errutil.ErrUserRegistrationFailed, err) } diff --git a/middleware/transaction.go b/middleware/transaction.go new file mode 100644 index 0000000..7339f3d --- /dev/null +++ b/middleware/transaction.go @@ -0,0 +1,47 @@ +package middleware + +import ( + "NotificationManagement/repositories" + "NotificationManagement/utils/errutil" + "context" + + "github.com/labstack/echo/v4" + "gorm.io/gorm" +) + +func TransactionMiddleware(db *gorm.DB) echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + tx := db.Begin() + if tx.Error != nil { + return tx.Error + } + + // Store the transaction in the context + ctx := GetContext(c, tx) + c.SetRequest(c.Request().WithContext(ctx)) + + // Call the next handler + err := next(c) + + if err != nil { + tx.Rollback() + return errutil.HandleError(c, err) + } + + tx.Commit() + return nil + } + } +} + +func GetContext(c echo.Context, tx *gorm.DB) context.Context { + var ctx context.Context + if c != nil { + ctx = c.Request().Context() + } else { + ctx = context.Background() + } + var filters []*repositories.Filter + return context.WithValue(ctx, repositories.TXContextKey, &repositories.TxContextKey{DB: tx, Filter: filters}) +} diff --git a/repositories/sql_common.go b/repositories/sql_common.go index 286eacd..33e9740 100644 --- a/repositories/sql_common.go +++ b/repositories/sql_common.go @@ -6,6 +6,7 @@ import ( "context" "errors" "fmt" + "github.com/jackc/pgx/v5/pgconn" "gorm.io/gorm" ) @@ -13,26 +14,64 @@ import ( type SQLRepository[T any] struct { db *gorm.DB } +type txContextKey string + +const TXContextKey txContextKey = "txContext" + +type TxContextKey struct { + DB *gorm.DB + Filter []*Filter +} type Filter struct { - Field string - Op string // e.g., "=", "LIKE", "IN" - Value interface{} + Field string + Op string + Value interface{} + Applied bool } -type ContextStruct struct { - Filter *[]Filter +func NewFilter(field string, op string, value interface{}) *Filter { + return &Filter{Value: value, Op: op, Field: field, Applied: false} } func NewSQLRepository[T any](db *gorm.DB) domain.Repository[T, uint] { return &SQLRepository[T]{db: db} } + func (r *SQLRepository[T]) GetDB(ctx context.Context) *gorm.DB { - return r.db + if tx, ok := GetTxContext(ctx); ok { + return tx.DB.WithContext(ctx) + } + return r.db.WithContext(ctx) +} + +func GetTxContext(ctx context.Context) (*TxContextKey, bool) { + if contextKey, ok := ctx.Value(TXContextKey).(*TxContextKey); ok { + return contextKey, true + } + return nil, false +} + +func ApplyFilter(ctx context.Context, query *gorm.DB) *gorm.DB { + if contextKey, ok := GetTxContext(ctx); ok { + for i, f := range contextKey.Filter { + if f.Applied { + continue + } + clause := fmt.Sprintf("%s %s ?", f.Field, f.Op) + query = query.Where(clause, f.Value) + (contextKey.Filter)[i].Applied = true + } + } + return query +} + +func (r *SQLRepository[T]) WithTx(tx *gorm.DB) domain.Repository[T, uint] { + return &SQLRepository[T]{db: tx} } func (r *SQLRepository[T]) Create(ctx context.Context, entity *T) error { - withContext := r.db.WithContext(ctx) + withContext := r.GetDB(ctx) withContext = ApplyFilter(ctx, withContext) err := withContext.Create(entity).Error if err != nil { @@ -60,7 +99,7 @@ func handleDbError(err error) error { func (r *SQLRepository[T]) GetByID(ctx context.Context, id uint, preloads *[]string) (*T, error) { var entity T - dbContext := r.db.WithContext(ctx) + dbContext := r.GetDB(ctx) if preloads != nil { for _, it := range *preloads { dbContext = dbContext.Preload(it) @@ -76,7 +115,7 @@ func (r *SQLRepository[T]) GetByID(ctx context.Context, id uint, preloads *[]str func (r *SQLRepository[T]) GetAll(ctx context.Context, limit, offset int) ([]T, error) { var entities []T - withContext := r.db.WithContext(ctx) + withContext := r.GetDB(ctx) withContext = ApplyFilter(ctx, withContext) err := withContext.Limit(limit).Offset(offset).Find(&entities).Error if err != nil { @@ -85,21 +124,8 @@ func (r *SQLRepository[T]) GetAll(ctx context.Context, limit, offset int) ([]T, return entities, nil } -func ApplyFilter(ctx context.Context, query *gorm.DB) *gorm.DB { - key := ContextStruct{} - - type ExtraFilters *[]Filter - if contextKey, ok := ctx.Value(key).(*ContextStruct); ok { - for _, f := range *contextKey.Filter { - clause := fmt.Sprintf("%s %s ?", f.Field, f.Op) - query = query.Where(clause, f.Value) - } - } - return query -} - func (r *SQLRepository[T]) Update(ctx context.Context, entity *T) error { - err := r.db.WithContext(ctx).Save(entity).Error + err := r.GetDB(ctx).Save(entity).Error if err != nil { return handleDbError(err) } @@ -108,7 +134,7 @@ func (r *SQLRepository[T]) Update(ctx context.Context, entity *T) error { func (r *SQLRepository[T]) Delete(ctx context.Context, id uint) error { var entity T - res := r.db.WithContext(ctx).Delete(&entity, id) + res := r.GetDB(ctx).Delete(&entity, id) err := res.Error if err != nil { return errutil.NewAppError(errutil.ErrDatabaseQuery, err) @@ -122,7 +148,7 @@ func (r *SQLRepository[T]) Delete(ctx context.Context, id uint) error { func (r *SQLRepository[T]) Count(ctx context.Context) (int64, error) { var count int64 - err := r.db.Model(new(T)).Count(&count).Error + err := r.GetDB(ctx).Model(new(T)).Count(&count).Error if err != nil { return 0, handleDbError(err) } @@ -131,13 +157,13 @@ func (r *SQLRepository[T]) Count(ctx context.Context) (int64, error) { func (r *SQLRepository[T]) GetByIDs(ctx context.Context, ids []uint, preloads *[]string) ([]T, error) { var entities []T - dbContext := r.db.WithContext(ctx) + dbContext := r.GetDB(ctx) if preloads != nil { for _, it := range *preloads { dbContext = dbContext.Preload(it) } } - dbContext = ApplyFilter(ctx, r.db) + dbContext = ApplyFilter(ctx, dbContext) err := dbContext.Where("id IN (?)", ids).Find(&entities).Error if err != nil { return nil, handleDbError(err) diff --git a/repositories/user.go b/repositories/user.go index 7efa830..5a49f49 100644 --- a/repositories/user.go +++ b/repositories/user.go @@ -10,28 +10,18 @@ import ( type UserRepositoryImpl struct { domain.Repository[models.User, uint] - db *gorm.DB } func NewUserRepository(db *gorm.DB) domain.UserRepository { return &UserRepositoryImpl{ Repository: NewSQLRepository[models.User](db), - db: db, } } func (r *UserRepositoryImpl) FindByKeycloakID(keycloakID string, ctx context.Context) (*models.User, error) { var user models.User - err := r.db.Where("keycloak_id = ?", keycloakID).First(&user).Error - if err != nil { - return nil, handleDbError(err) - } - return &user, nil -} -func (r *UserRepositoryImpl) FindByEmail(email string) (*models.User, error) { - var user models.User - err := r.db.Where("email = ?", email).First(&user).Error + err := r.GetDB(ctx).Where("keycloak_id = ?", keycloakID).First(&user).Error if err != nil { return nil, handleDbError(err) } diff --git a/server/server.go b/server/server.go index e8f771f..9b7b73a 100644 --- a/server/server.go +++ b/server/server.go @@ -10,15 +10,16 @@ import ( "NotificationManagement/routes" "NotificationManagement/services" "NotificationManagement/services/notifier" - "NotificationManagement/utils/errutil" "github.com/labstack/echo/v4" "go.uber.org/fx" + "gorm.io/gorm" // Import gorm ) -func NewEcho() *echo.Echo { +func NewEcho(db *gorm.DB) *echo.Echo { e := echo.New() e.Use(interceptLogger) - e.Use(errutil.ErrorHandler()) + //e.Use(errutil.ErrorHandler()) + e.Use(middleware.TransactionMiddleware(db)) return e } @@ -50,6 +51,7 @@ func interceptLogger(next echo.HandlerFunc) echo.HandlerFunc { var Module = fx.Options( fx.Provide( NewEcho, + conn.NewDB, conn.NewAsynq, conn.NewAsynqInspector, notifier.NewEmailNotifier, diff --git a/services/curl.go b/services/curl.go index e81c24f..2f2e7e4 100644 --- a/services/curl.go +++ b/services/curl.go @@ -30,7 +30,7 @@ func (s *CurlServiceImpl) GetModelById(c context.Context, id uint, preloads *[]s if preloads == nil { preloads = &[]string{"AdditionalFields"} } - return s.CurlRepo.GetByID(s.GetInstance().GetContext(), id, preloads) + return s.CurlRepo.GetByID(s.GetInstance().ProcessContext(c), id, preloads) } func NewCurlService(repo domain.CurlRequestRepository, fieldsRepository domain.AdditionalFieldsRepository) domain.CurlService { @@ -218,12 +218,11 @@ func (s *CurlServiceImpl) UpdateModel(c context.Context, id uint, model *models. existingIDsMap := make(map[uint]bool) if len(idsToCheck) > 0 { - background := context.Background() - f := []repositories.Filter{ - {Field: "request_id", Op: "=", Value: id}, + if txContext, ok := repositories.GetTxContext(c); ok { + filters := append(txContext.Filter, repositories.NewFilter("request_id", "=", id)) + txContext.Filter = filters } - background = context.WithValue(background, repositories.ContextStruct{}, &repositories.ContextStruct{Filter: &f}) - existingAdditionalFields, err := s.AdditionalFieldRepo.GetByIDs(background, idsToCheck, nil) + existingAdditionalFields, err := s.AdditionalFieldRepo.GetByIDs(c, idsToCheck, nil) if err != nil { return nil, err } diff --git a/services/deepseek.go b/services/deepseek.go index d9baf60..2ba7c47 100644 --- a/services/deepseek.go +++ b/services/deepseek.go @@ -30,12 +30,13 @@ func NewDeepseekModelService(repo domain.DeepseekModelRepository, curl domain.Cu return service } -func (s *DeepseekServiceImpl) GetContext() context.Context { - background := context.Background() - f := []repositories.Filter{ - {Field: "type", Op: "=", Value: "deepseek"}, +func (s *DeepseekServiceImpl) ProcessContext(ctx context.Context) context.Context { + if txContext, ok := repositories.GetTxContext(ctx); ok { + filters := append(txContext.Filter, repositories.NewFilter("type", "=", s.GetModelType())) + txContext.Filter = filters } - return context.WithValue(background, repositories.ContextStruct{}, &repositories.ContextStruct{Filter: &f}) + + return ctx } func (s *DeepseekServiceImpl) MakeAIRequest(c context.Context, m *models.AIModel, requestId uint) (interface{}, error) { diff --git a/services/gemini.go b/services/gemini.go index 48db8a3..bc31774 100644 --- a/services/gemini.go +++ b/services/gemini.go @@ -27,12 +27,12 @@ func NewGeminiService(repo domain.GeminiModelRepository, curlService domain.Curl return service } -func (s *GeminiServiceImpl) GetContext() context.Context { - background := context.Background() - f := []repositories.Filter{ - {Field: "type", Op: "=", Value: "gemini"}, +func (s *GeminiServiceImpl) ProcessContext(ctx context.Context) context.Context { + if txContext, ok := repositories.GetTxContext(ctx); ok { + filters := append(txContext.Filter, repositories.NewFilter("type", "=", s.GetModelType())) + txContext.Filter = filters } - return context.WithValue(background, repositories.ContextStruct{}, &repositories.ContextStruct{Filter: &f}) + return ctx } func (s *GeminiServiceImpl) MakeAIRequest(c context.Context, m *models.AIModel, requestId uint) (interface{}, error) { @@ -49,7 +49,7 @@ func (s *GeminiServiceImpl) MakeAIRequest(c context.Context, m *models.AIModel, if err != nil { return nil, err } - respBody, err := geminiCall(model, curlResponse, curl) + respBody, err := geminiCall(c, model, curlResponse, curl) if err != nil { return nil, errutil.NewAppError(errutil.ErrExternalServiceError, err) } @@ -73,13 +73,11 @@ func (s *GeminiServiceImpl) GetModelType() string { return "gemini" } -func geminiCall(model *models.GeminiModel, response *types.CurlResponse, req *models.CurlRequest) (*genai.GenerateContentResponse, error) { +func geminiCall(ctx context.Context, model *models.GeminiModel, response *types.CurlResponse, req *models.CurlRequest) (*genai.GenerateContentResponse, error) { assistantContent, err := response.GetAssistantContent(req.ResponseType) if err != nil { return nil, err } - ctx := context.Background() - // The client gets the API key from the environment variable `GEMINI_API_KEY`. client, err := genai.NewClient(ctx, &genai.ClientConfig{ APIKey: model.GetAPIKey(), }) diff --git a/services/notifier/dispatcher.go b/services/notifier/dispatcher.go index 1fdb4c9..3124e94 100644 --- a/services/notifier/dispatcher.go +++ b/services/notifier/dispatcher.go @@ -19,9 +19,6 @@ func NewNotificationDispatcher(email *EmailNotifier, sms *SMSNotifier, telegram } func (d *Dispatcher) Notify(ctx context.Context, notification *types.Notification) error { - if ctx != nil { - ctx = context.Background() - } if notification.User == nil { user, err := d.UserService.GetModelById(ctx, notification.UserId, &[]string{"Telegram"}) if err != nil { diff --git a/services/service.go b/services/service.go index 9f80673..d2f7298 100644 --- a/services/service.go +++ b/services/service.go @@ -8,27 +8,27 @@ import ( ) type CommonServiceImpl[T any] struct { - Repo domain.Repository[T, uint] + domain.Repository[T, uint] Instance domain.CommonService[T] } func NewCommonService[T any](repo domain.Repository[T, uint], instance domain.CommonService[T]) domain.CommonService[T] { - return &CommonServiceImpl[T]{Repo: repo, Instance: instance} + return &CommonServiceImpl[T]{Repository: repo, Instance: instance} } -func (s *CommonServiceImpl[T]) GetContext() context.Context { - return context.Background() +func (s *CommonServiceImpl[T]) ProcessContext(c context.Context) context.Context { + return c } func (s *CommonServiceImpl[T]) GetInstance() domain.CommonService[T] { return s.Instance } func (s *CommonServiceImpl[T]) CreateModel(c context.Context, entity *T) error { - return s.Repo.Create(s.Instance.GetContext(), entity) + return s.Create(s.Instance.ProcessContext(c), entity) } func (s *CommonServiceImpl[T]) GetModelById(c context.Context, id uint, preloads *[]string) (*T, error) { - model, err := s.Repo.GetByID(s.Instance.GetContext(), id, preloads) + model, err := s.GetByID(s.Instance.ProcessContext(c), id, preloads) if err != nil { return nil, err } @@ -36,7 +36,7 @@ func (s *CommonServiceImpl[T]) GetModelById(c context.Context, id uint, preloads } func (s *CommonServiceImpl[T]) GetAllModels(c context.Context, limit, offset int) ([]T, error) { - m, err := s.Repo.GetAll(s.Instance.GetContext(), limit, offset) + m, err := s.GetAll(s.Instance.ProcessContext(c), limit, offset) if err != nil { return nil, err } @@ -44,13 +44,12 @@ func (s *CommonServiceImpl[T]) GetAllModels(c context.Context, limit, offset int } func (s *CommonServiceImpl[T]) UpdateModel(c context.Context, id uint, model *T) (*T, error) { - // Check if the model implements ModelInterface modelUpdater, ok := any(model).(models.ModelInterface) if !ok { return nil, errutil.NewAppError(errutil.ErrFeatureNotAvailable, errutil.ErrInvalidFeature) } - existing, err := s.Repo.GetByID(s.Instance.GetContext(), id, nil) + existing, err := s.GetByID(s.Instance.ProcessContext(c), id, nil) if err != nil { return nil, err } @@ -58,10 +57,10 @@ func (s *CommonServiceImpl[T]) UpdateModel(c context.Context, id uint, model *T) existingUpdater.UpdateFromModel(modelUpdater) } - err = s.Repo.Update(s.Instance.GetContext(), existing) + err = s.Update(s.Instance.ProcessContext(c), existing) return existing, err } func (s *CommonServiceImpl[T]) DeleteModel(c context.Context, id uint) error { - return s.Repo.Delete(s.Instance.GetContext(), id) + return s.Delete(s.Instance.ProcessContext(c), id) } diff --git a/services/user.go b/services/user.go index b623331..34e5686 100644 --- a/services/user.go +++ b/services/user.go @@ -21,8 +21,7 @@ func NewUserService(repo domain.UserRepository) domain.UserService { return service } -func (s *UserServiceImpl) RegisterOrUpdateUser(user *models.User) (*models.User, error) { - ctx := context.Background() +func (s *UserServiceImpl) RegisterOrUpdateUser(ctx context.Context, user *models.User) (*models.User, error) { existingUser, err := s.UserRepo.FindByKeycloakID(user.KeycloakID, ctx) if err != nil { var appErr *errutil.AppError @@ -47,7 +46,3 @@ func (s *UserServiceImpl) RegisterOrUpdateUser(user *models.User) (*models.User, } return existingUser, nil } - -func (s *UserServiceImpl) GetContext() context.Context { - return context.Background() -} diff --git a/utils/errutil/error_handler.go b/utils/errutil/error_handler.go index 2eb29a1..e1d7a66 100644 --- a/utils/errutil/error_handler.go +++ b/utils/errutil/error_handler.go @@ -15,12 +15,12 @@ func ErrorHandler() echo.MiddlewareFunc { if err == nil { return nil } - return handleError(c, err) + return HandleError(c, err) } } } -func handleError(c echo.Context, err error) error { +func HandleError(c echo.Context, err error) error { var appError *AppError if errors.As(err, &appError) { From 907d84ef61421eb8b503d5e250a347c5bbc3c1bb Mon Sep 17 00:00:00 2001 From: MD Towhidul Islam Date: Thu, 14 Aug 2025 21:22:18 +0600 Subject: [PATCH 02/21] GE4-21 - Circle CI + Github actions # Basic build --- .github/workflows/go-build.yml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 .github/workflows/go-build.yml diff --git a/.github/workflows/go-build.yml b/.github/workflows/go-build.yml new file mode 100644 index 0000000..a06aded --- /dev/null +++ b/.github/workflows/go-build.yml @@ -0,0 +1,29 @@ +name: Go Build + +on: + push: + branches: + - main + - develop + - feature/** + pull_request: + branches: + - main + - develop + - feature/** + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.22' # Use the Go version appropriate for your project + + - name: Build + run: go build -v ./... From 5febf2371e07a1653cb6bf89d2f9a5652a65be4e Mon Sep 17 00:00:00 2001 From: MD Towhidul Islam Date: Thu, 14 Aug 2025 21:53:09 +0600 Subject: [PATCH 03/21] GE4-21 - Circle CI + Github actions # Add k8 files --- k8/config-server.yaml | 54 +++++++++++++++++++++++++++++++++ k8/grafana.yaml | 61 ++++++++++++++++++++++++++++++++++++++ k8/keycloak.yaml | 68 ++++++++++++++++++++++++++++++++++++++++++ k8/mailcatcher.yaml | 45 ++++++++++++++++++++++++++++ k8/ollama.yaml | 40 +++++++++++++++++++++++++ k8/postgres.yaml | 69 +++++++++++++++++++++++++++++++++++++++++++ k8/prometheus.yaml | 49 ++++++++++++++++++++++++++++++ k8/redis.yaml | 59 ++++++++++++++++++++++++++++++++++++ 8 files changed, 445 insertions(+) create mode 100644 k8/config-server.yaml create mode 100644 k8/grafana.yaml create mode 100644 k8/keycloak.yaml create mode 100644 k8/mailcatcher.yaml create mode 100644 k8/ollama.yaml create mode 100644 k8/postgres.yaml create mode 100644 k8/prometheus.yaml create mode 100644 k8/redis.yaml diff --git a/k8/config-server.yaml b/k8/config-server.yaml new file mode 100644 index 0000000..6c7702b --- /dev/null +++ b/k8/config-server.yaml @@ -0,0 +1,54 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: config-server + labels: + app: config-server +spec: + replicas: 1 + selector: + matchLabels: + app: config-server + template: + metadata: + labels: + app: config-server + spec: + containers: + - name: config-server + image: localstack/localstack:3.2.0 + ports: + - containerPort: 4566 + env: + - name: SERVICES + value: "ssm,config" + - name: DEBUG + value: "1" + - name: PERSISTENCE + value: "1" + - name: DATA_DIR + value: "/tmp/localstack/data" + - name: LAMBDA_EXECUTOR + value: "docker" + volumeMounts: + - name: docker-sock + mountPath: /var/run/docker.sock + volumes: + - name: docker-sock + hostPath: + path: /var/run/docker.sock +--- +apiVersion: v1 +kind: Service +metadata: + name: config-server + labels: + app: config-server +spec: + selector: + app: config-server + ports: + - protocol: TCP + port: 4566 + targetPort: 4566 + type: ClusterIP diff --git a/k8/grafana.yaml b/k8/grafana.yaml new file mode 100644 index 0000000..eaf4029 --- /dev/null +++ b/k8/grafana.yaml @@ -0,0 +1,61 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: grafana + labels: + app: grafana +spec: + replicas: 1 + selector: + matchLabels: + app: grafana + template: + metadata: + labels: + app: grafana + spec: + containers: + - name: grafana + image: grafana/grafana:12.0.2 + ports: + - containerPort: 3000 + env: + - name: GF_SECURITY_ADMIN_USER + value: "admin" + - name: GF_SECURITY_ADMIN_PASSWORD + value: "admin" + volumeMounts: + - name: grafana-data + mountPath: /var/lib/grafana + volumes: + - name: grafana-data + persistentVolumeClaim: + claimName: grafana-pv-claim +--- +apiVersion: v1 +kind: Service +metadata: + name: grafana + labels: + app: grafana +spec: + selector: + app: grafana + ports: + - protocol: TCP + port: 3000 + targetPort: 3000 + type: ClusterIP +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: grafana-pv-claim + labels: + app: grafana +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi diff --git a/k8/keycloak.yaml b/k8/keycloak.yaml new file mode 100644 index 0000000..16dfbf4 --- /dev/null +++ b/k8/keycloak.yaml @@ -0,0 +1,68 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: keycloak + labels: + app: keycloak +spec: + replicas: 1 + selector: + matchLabels: + app: keycloak + template: + metadata: + labels: + app: keycloak + spec: + containers: + - name: keycloak + image: keycloak/keycloak:25.0.6 + command: + - start-dev + - --import-realm + env: + - name: KC_HOSTNAME + value: "localhost" + - name: KEYCLOAK_USER + value: "admin" + - name: KEYCLOAK_PASSWORD + value: "secret" + - name: KEYCLOAK_ADMIN + value: "admin" + - name: KEYCLOAK_ADMIN_PASSWORD + value: "secret" + - name: KC_HEALTH_ENABLED + value: "true" + - name: KC_FEATURES + value: "account-api,account3,authorization,client-policies,impersonation,docker,scripts,admin-fine-grained-authz" + ports: + - containerPort: 8080 + - containerPort: 9000 + volumeMounts: + - name: keycloak-import + mountPath: /opt/keycloak/data/import/ + volumes: + - name: keycloak-import + hostPath: + path: /home/towhidul/GoProjects/NotificationManagement/keycloak/import + type: DirectoryOrCreate +--- +apiVersion: v1 +kind: Service +metadata: + name: keycloak + labels: + app: keycloak +spec: + selector: + app: keycloak + ports: + - protocol: TCP + port: 8081 + targetPort: 8080 + name: http + - protocol: TCP + port: 9000 + targetPort: 9000 + name: management + type: ClusterIP diff --git a/k8/mailcatcher.yaml b/k8/mailcatcher.yaml new file mode 100644 index 0000000..869da2a --- /dev/null +++ b/k8/mailcatcher.yaml @@ -0,0 +1,45 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: mailcatcher + labels: + app: mailcatcher +spec: + replicas: 1 + selector: + matchLabels: + app: mailcatcher + template: + metadata: + labels: + app: mailcatcher + spec: + containers: + - name: mailcatcher + image: schickling/mailcatcher + ports: + - containerPort: 1080 + - containerPort: 1025 + env: + - name: HTTPPATH + value: "/mails" +--- +apiVersion: v1 +kind: Service +metadata: + name: mailcatcher + labels: + app: mailcatcher +spec: + selector: + app: mailcatcher + ports: + - protocol: TCP + port: 1080 + targetPort: 1080 + name: http + - protocol: TCP + port: 1025 + targetPort: 1025 + name: smtp + type: ClusterIP diff --git a/k8/ollama.yaml b/k8/ollama.yaml new file mode 100644 index 0000000..a4b040d --- /dev/null +++ b/k8/ollama.yaml @@ -0,0 +1,40 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: ollama + labels: + app: ollama +spec: + replicas: 1 + selector: + matchLabels: + app: ollama + template: + metadata: + labels: + app: ollama + spec: + containers: + - name: ollama + image: ollama/ollama:latest + ports: + - containerPort: 11434 + # resources: + # limits: + # memory: "2Gi" + # cpu: "500m" +--- +apiVersion: v1 +kind: Service +metadata: + name: ollama + labels: + app: ollama +spec: + selector: + app: ollama + ports: + - protocol: TCP + port: 11434 + targetPort: 11434 + type: ClusterIP diff --git a/k8/postgres.yaml b/k8/postgres.yaml new file mode 100644 index 0000000..81062a6 --- /dev/null +++ b/k8/postgres.yaml @@ -0,0 +1,69 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: postgres + labels: + app: postgres +spec: + replicas: 1 + selector: + matchLabels: + app: postgres + template: + metadata: + labels: + app: postgres + spec: + containers: + - name: postgres + image: postgres:15 + ports: + - containerPort: 5432 + env: + - name: POSTGRES_USER + value: "user" + - name: POSTGRES_PASSWORD + value: "password" + - name: POSTGRES_DB + value: "notification_management" + volumeMounts: + - name: postgres-data + mountPath: /var/lib/postgresql/data + - name: postgres-initdb + mountPath: /docker-entrypoint-initdb.d/ + volumes: + - name: postgres-data + persistentVolumeClaim: + claimName: postgres-pv-claim + - name: postgres-initdb + hostPath: + path: /home/towhidul/GoProjects/NotificationManagement/scripts/db + type: DirectoryOrCreate +--- +apiVersion: v1 +kind: Service +metadata: + name: postgres + labels: + app: postgres +spec: + selector: + app: postgres + ports: + - protocol: TCP + port: 5432 + targetPort: 5432 + type: ClusterIP +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: postgres-pv-claim + labels: + app: postgres +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi diff --git a/k8/prometheus.yaml b/k8/prometheus.yaml new file mode 100644 index 0000000..bd4379d --- /dev/null +++ b/k8/prometheus.yaml @@ -0,0 +1,49 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: prometheus + labels: + app: prometheus +spec: + replicas: 1 + selector: + matchLabels: + app: prometheus + template: + metadata: + labels: + app: prometheus + spec: + containers: + - name: prometheus + image: prom/prometheus:v2.53.5 + ports: + - containerPort: 9090 + volumeMounts: + - name: prometheus-config + mountPath: /etc/prometheus + command: + - '--config.file=/etc/prometheus/prometheus.yml' + - '--storage.tsdb.path=/prometheus' + - '--web.console.libraries=/usr/share/prometheus/console_libraries' + - '--web.console.templates=/usr/share/prometheus/consoles' + volumes: + - name: prometheus-config + hostPath: + path: /home/towhidul/GoProjects/NotificationManagement/prometheus + type: DirectoryOrCreate +--- +apiVersion: v1 +kind: Service +metadata: + name: prometheus + labels: + app: prometheus +spec: + selector: + app: prometheus + ports: + - protocol: TCP + port: 9090 + targetPort: 9090 + type: ClusterIP diff --git a/k8/redis.yaml b/k8/redis.yaml new file mode 100644 index 0000000..067a4f5 --- /dev/null +++ b/k8/redis.yaml @@ -0,0 +1,59 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: redis + labels: + app: redis +spec: + replicas: 1 + selector: + matchLabels: + app: redis + template: + metadata: + labels: + app: redis + spec: + containers: + - name: redis + image: bitnami/redis:6.0.9 + ports: + - containerPort: 6379 + env: + - name: ALLOW_EMPTY_PASSWORD + value: "yes" + volumeMounts: + - name: redis-data + mountPath: /bitnami/redis/data + volumes: + - name: redis-data + persistentVolumeClaim: + claimName: redis-pv-claim +--- +apiVersion: v1 +kind: Service +metadata: + name: redis + labels: + app: redis +spec: + selector: + app: redis + ports: + - protocol: TCP + port: 6379 + targetPort: 6379 + type: ClusterIP +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: redis-pv-claim + labels: + app: redis +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi From f2e93e99c0c363b941750d37707b4a47d6cedc5f Mon Sep 17 00:00:00 2001 From: MD Towhidul Islam Date: Thu, 14 Aug 2025 22:26:30 +0600 Subject: [PATCH 04/21] GE4-21 - Circle CI + Github actions # update docker compose --- Dockerfile | 32 ++++++++++++++++++++++++++++++++ docker-compose.yml | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5eb20fb --- /dev/null +++ b/Dockerfile @@ -0,0 +1,32 @@ +# Use the official Golang image as a base for building +FROM golang:1.22-alpine AS builder + +# Set the current working directory inside the container +WORKDIR /app + +# Copy go.mod and go.sum files and download dependencies +COPY go.mod go.sum ./ +RUN go mod download + +# Copy the rest of the application source code +COPY . . + +# Build the Go application +# CGO_ENABLED=0 is important for static binaries, useful for scratch images +# -o specifies the output file name +RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o notification-management ./main.go + +# Use a minimal image for the final stage +FROM alpine:latest + +# Set the current working directory inside the container +WORKDIR /root/ + +# Copy the compiled binary from the builder stage +COPY --from=builder /app/notification-management . + +# Expose the port the application listens on (assuming 8080) +EXPOSE 8080 + +# Command to run the executable +CMD ["./notification-management"] diff --git a/docker-compose.yml b/docker-compose.yml index c25de3d..d5e7b6f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -136,6 +136,47 @@ services: depends_on: - prometheus + app: + build: + context: . + dockerfile: Dockerfile + container_name: go-nms + command: + - serve + restart: unless-stopped + ports: + - "8080:8080" + environment: + - LOG_LEVEL + - TELEGRAM_TOKEN + - GEMINI_KEY + volumes: + - ./app.log:/app.log + depends_on: + redis: + condition: service_started + networks: + - nms-network + + worker: + build: + context: . + dockerfile: Dockerfile + container_name: go-ems-worker + command: + - worker + restart: unless-stopped + environment: + - LOG_LEVEL + - TELEGRAM_TOKEN + - GEMINI_KEY + volumes: + - ./app.log:/app.log + depends_on: + redis: + condition: service_started + networks: + - nms-network networks: nms-network: driver: bridge From 2f7a381bf3cb34a560a862f000f3014db04a920d Mon Sep 17 00:00:00 2001 From: MD Towhidul Islam Date: Fri, 15 Aug 2025 21:16:15 +0600 Subject: [PATCH 05/21] GE4-21 - Circle CI + Github actions # update some codes --- .gitignore | 1 + Dockerfile | 46 +++++++++++++++----------------------------- cmd/worker.go | 2 +- config/aws_client.go | 8 +++++--- config/config.go | 4 ++-- conn/asynq.go | 4 ++-- docker-compose.yml | 30 +++++++++++++++++++++-------- keycloak/Dockerfile | 12 ------------ 8 files changed, 49 insertions(+), 58 deletions(-) delete mode 100644 keycloak/Dockerfile diff --git a/.gitignore b/.gitignore index c67b8b3..2460426 100644 --- a/.gitignore +++ b/.gitignore @@ -53,6 +53,7 @@ localstack-data/ tmp/ temp/ localstack-data/ +vendor/ aws/ sample/ diff --git a/Dockerfile b/Dockerfile index 5eb20fb..80ed33e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,32 +1,18 @@ -# Use the official Golang image as a base for building -FROM golang:1.22-alpine AS builder +ARG GO_VERSION=1.24.2 +FROM golang:${GO_VERSION}-alpine AS builder +RUN mkdir /user && \ + echo 'nobody:x:65534:65534:nobody:/:' > /user/passwd && \ + echo 'nobody:x:65534:' > /user/group +RUN apk add --no-cache ca-certificates +WORKDIR /src +COPY ./ ./ +RUN CGO_ENABLED=0 GOFLAGS=-mod=vendor GOOS=linux go build -a -o /app . + +FROM alpine:latest AS final +COPY --from=builder /user/group /user/passwd /etc/ +COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ +COPY --from=builder /app /app +USER nobody:nobody -# Set the current working directory inside the container -WORKDIR /app - -# Copy go.mod and go.sum files and download dependencies -COPY go.mod go.sum ./ -RUN go mod download - -# Copy the rest of the application source code -COPY . . - -# Build the Go application -# CGO_ENABLED=0 is important for static binaries, useful for scratch images -# -o specifies the output file name -RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o notification-management ./main.go - -# Use a minimal image for the final stage -FROM alpine:latest - -# Set the current working directory inside the container -WORKDIR /root/ - -# Copy the compiled binary from the builder stage -COPY --from=builder /app/notification-management . - -# Expose the port the application listens on (assuming 8080) EXPOSE 8080 - -# Command to run the executable -CMD ["./notification-management"] +ENTRYPOINT ["/app"] \ No newline at end of file diff --git a/cmd/worker.go b/cmd/worker.go index 2bfb969..ffce359 100644 --- a/cmd/worker.go +++ b/cmd/worker.go @@ -83,7 +83,7 @@ func registerHooks(telegramAPI domain.TelegramAPI) { func NewAsynqServer() *asynq.Server { return asynq.NewServer( asynq.RedisClientOpt{ - Addr: config.Asynq().RedisAddr, + Addr: config.GetRedisAddr(), DB: *config.Asynq().DB, Password: config.Asynq().Pass, }, diff --git a/config/aws_client.go b/config/aws_client.go index ba5b8cb..dd52ec5 100644 --- a/config/aws_client.go +++ b/config/aws_client.go @@ -20,6 +20,11 @@ type AWSClient struct { } func NewAWSClient(cnf *AWSConfig) (*AWSClient, error) { + + if os.Getenv(EnvAWSConfigServiceEnabled) == "false" { + return nil, fmt.Errorf("aws service not available") + } + var creds aws.CredentialsProvider if cnf.AccessKeyID != "" && cnf.SecretAccessKey != "" { creds = credentials.NewStaticCredentialsProvider( @@ -94,9 +99,6 @@ func (c *AWSClient) GetConfigurationRecorderStatus(ctx context.Context) (*config } func (c *AWSClient) loadFromSsm() { - if os.Getenv(EnvConfigFromSSM) == "false" { - return - } resp, err := c.ssm.GetParameter(context.TODO(), &ssm.GetParameterInput{ Name: &c.awsConfig.ConfigService.SSM, WithDecryption: aws.Bool(true), diff --git a/config/config.go b/config/config.go index 90991ca..8c93055 100644 --- a/config/config.go +++ b/config/config.go @@ -42,7 +42,7 @@ type DatabaseConfig struct { } type AsynqConfig struct { - RedisAddr string `mapstructure:"redisaddr"` + //RedisAddr string `mapstructure:"redisaddr"` DB *int `mapstructure:"db"` Pass string `mapstructure:"pass"` Concurrency *int `mapstructure:"concurrency"` @@ -173,7 +173,7 @@ func loadDefaults() *Config { SSLMode: "disable", }, Asynq: AsynqConfig{ - RedisAddr: "127.0.0.1:6379", + //RedisAddr: "127.0.0.1:6379", DB: helper.ToInt("15"), Pass: "*****", Concurrency: helper.ToInt("10"), diff --git a/conn/asynq.go b/conn/asynq.go index aee442f..a68a8a6 100644 --- a/conn/asynq.go +++ b/conn/asynq.go @@ -8,7 +8,7 @@ import ( func NewAsynq() *asynq.Client { asynqConfig := config.Asynq() return asynq.NewClient(asynq.RedisClientOpt{ - Addr: asynqConfig.RedisAddr, + Addr: config.GetRedisAddr(), DB: *asynqConfig.DB, Password: asynqConfig.Pass, }) @@ -17,7 +17,7 @@ func NewAsynq() *asynq.Client { func NewAsynqInspector() *asynq.Inspector { asynqConfig := config.Asynq() return asynq.NewInspector(asynq.RedisClientOpt{ - Addr: asynqConfig.RedisAddr, + Addr: config.GetRedisAddr(), DB: *asynqConfig.DB, Password: asynqConfig.Pass, }) diff --git a/docker-compose.yml b/docker-compose.yml index d5e7b6f..4dea9d3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -48,14 +48,14 @@ services: networks: - nms-network - keycloak: - container_name: keycloak + keycloak_svc: + container_name: gocloak image: keycloak/keycloak:25.0.6 command: - start-dev - --import-realm environment: - KC_HOSTNAME: localhost + KC_HOSTNAME: keycloak KEYCLOAK_USER: admin KEYCLOAK_PASSWORD: secret KEYCLOAK_ADMIN: admin @@ -143,26 +143,36 @@ services: container_name: go-nms command: - serve - restart: unless-stopped +# restart: unless-stopped ports: - "8080:8080" environment: - LOG_LEVEL - TELEGRAM_TOKEN - GEMINI_KEY + - AWS_ENDPOINT + - AWS_CONFIG_SERVICE_ENABLED + - REDIS_HOST + - DB_HOST + - DB_PORT + - KEYCLOAK_SERVER_URL volumes: - ./app.log:/app.log depends_on: + config-server: + condition: service_healthy redis: condition: service_started networks: - nms-network + extra_hosts: + - "keycloak:host-gateway" worker: build: context: . dockerfile: Dockerfile - container_name: go-ems-worker + container_name: go-nms-worker command: - worker restart: unless-stopped @@ -170,13 +180,17 @@ services: - LOG_LEVEL - TELEGRAM_TOKEN - GEMINI_KEY + - AWS_ENDPOINT + - AWS_CONFIG_SERVICE_ENABLED + - REDIS_HOST + - DB_HOST + - DB_PORT + - KEYCLOAK_SERVER_URL volumes: - ./app.log:/app.log depends_on: redis: - condition: service_started - networks: - - nms-network + condition: service_healthy networks: nms-network: driver: bridge diff --git a/keycloak/Dockerfile b/keycloak/Dockerfile deleted file mode 100644 index 71c95d3..0000000 --- a/keycloak/Dockerfile +++ /dev/null @@ -1,12 +0,0 @@ -FROM quay.io/keycloak/keycloak:25.0.1 -COPY . data/import -WORKDIR /opt/keycloak -ENV KC_HOSTNAME=localhost -ENV KEYCLOAK_USER=admin -ENV KEYCLOAK_PASSWORD=secret -ENV KEYCLOAK_ADMIN=admin -ENV KEYCLOAK_ADMIN_PASSWORD=secret -ENV KC_HEALTH_ENABLED=true -ENV KC_FEATURES=account-api,account3,authorization,client-policies,impersonation,docker,scripts,admin-fine-grained-authz -RUN /opt/keycloak/bin/kc.sh import --file /data/import/gocloak-realm.json -ENTRYPOINT ["/opt/keycloak/bin/kc.sh"] \ No newline at end of file From cfeebf7a4b6ae96f2bf4609507d5000a518e6c37 Mon Sep 17 00:00:00 2001 From: MD Towhidul Islam Date: Fri, 15 Aug 2025 21:53:51 +0600 Subject: [PATCH 06/21] GE4-21 - Circle CI + Github actions #Add ConfigMap and Secret for environment configuration; refactor deployment files to use them --- docker-compose.yml | 3 +-- k8/config-maps.yaml | 23 +++++++++++++++++++++++ k8/config-server.yaml | 14 +++----------- k8/grafana.yaml | 10 +++++++--- k8/keycloak.yaml | 23 +++++++++++------------ k8/mailcatcher.yaml | 6 +++--- k8/ollama.yaml | 40 ---------------------------------------- k8/postgres.yaml | 12 +++++++----- k8/redis.yaml | 6 +++--- k8/secrets.yaml | 10 ++++++++++ 10 files changed, 68 insertions(+), 79 deletions(-) create mode 100644 k8/config-maps.yaml delete mode 100644 k8/ollama.yaml create mode 100644 k8/secrets.yaml diff --git a/docker-compose.yml b/docker-compose.yml index 4dea9d3..7656860 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -155,7 +155,7 @@ services: - REDIS_HOST - DB_HOST - DB_PORT - - KEYCLOAK_SERVER_URL + - KEYCLOAK_SERVER_URL=http://${HOSTNAME}:8080 volumes: - ./app.log:/app.log depends_on: @@ -185,7 +185,6 @@ services: - REDIS_HOST - DB_HOST - DB_PORT - - KEYCLOAK_SERVER_URL volumes: - ./app.log:/app.log depends_on: diff --git a/k8/config-maps.yaml b/k8/config-maps.yaml new file mode 100644 index 0000000..b67ef44 --- /dev/null +++ b/k8/config-maps.yaml @@ -0,0 +1,23 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: nms-config +data: + SERVICES: "ssm,config" + POSTGRES_USER: "user" + POSTGRES_DB: "notification_management" + KC_HOSTNAME: "keycloak" + KEYCLOAK_USER: "admin" + KEYCLOAK_ADMIN: "admin" + KC_HEALTH_ENABLED: "true" + KC_FEATURES: "account-api,account3,authorization,client-policies,impersonation,docker,scripts,admin-fine-grained-authz" + ALLOW_EMPTY_PASSWORD: "yes" + HTTPPATH: "/mails" + GF_SECURITY_ADMIN_USER: "admin" + LOG_LEVEL: "debug" + AWS_ENDPOINT: "http://config-server:4566" + AWS_CONFIG_SERVICE_ENABLED: "false" + REDIS_HOST: "redis" + DB_HOST: "postgres" + DB_PORT: "5432" + KEYCLOAK_SERVER_URL: "http://keycloak:8080" diff --git a/k8/config-server.yaml b/k8/config-server.yaml index 6c7702b..574840b 100644 --- a/k8/config-server.yaml +++ b/k8/config-server.yaml @@ -19,17 +19,9 @@ spec: image: localstack/localstack:3.2.0 ports: - containerPort: 4566 - env: - - name: SERVICES - value: "ssm,config" - - name: DEBUG - value: "1" - - name: PERSISTENCE - value: "1" - - name: DATA_DIR - value: "/tmp/localstack/data" - - name: LAMBDA_EXECUTOR - value: "docker" + envFrom: + - configMapRef: + name: nms-config volumeMounts: - name: docker-sock mountPath: /var/run/docker.sock diff --git a/k8/grafana.yaml b/k8/grafana.yaml index eaf4029..b350ec9 100644 --- a/k8/grafana.yaml +++ b/k8/grafana.yaml @@ -20,10 +20,14 @@ spec: ports: - containerPort: 3000 env: - - name: GF_SECURITY_ADMIN_USER - value: "admin" - name: GF_SECURITY_ADMIN_PASSWORD - value: "admin" + valueFrom: + secretKeyRef: + name: nms-secrets + key: GF_SECURITY_ADMIN_PASSWORD + envFrom: + - configMapRef: + name: nms-config volumeMounts: - name: grafana-data mountPath: /var/lib/grafana diff --git a/k8/keycloak.yaml b/k8/keycloak.yaml index 16dfbf4..870f4cb 100644 --- a/k8/keycloak.yaml +++ b/k8/keycloak.yaml @@ -21,20 +21,19 @@ spec: - start-dev - --import-realm env: - - name: KC_HOSTNAME - value: "localhost" - - name: KEYCLOAK_USER - value: "admin" - name: KEYCLOAK_PASSWORD - value: "secret" - - name: KEYCLOAK_ADMIN - value: "admin" + valueFrom: + secretKeyRef: + name: nms-secrets + key: KEYCLOAK_PASSWORD - name: KEYCLOAK_ADMIN_PASSWORD - value: "secret" - - name: KC_HEALTH_ENABLED - value: "true" - - name: KC_FEATURES - value: "account-api,account3,authorization,client-policies,impersonation,docker,scripts,admin-fine-grained-authz" + valueFrom: + secretKeyRef: + name: nms-secrets + key: KEYCLOAK_ADMIN_PASSWORD + envFrom: + - configMapRef: + name: nms-config ports: - containerPort: 8080 - containerPort: 9000 diff --git a/k8/mailcatcher.yaml b/k8/mailcatcher.yaml index 869da2a..f76fa50 100644 --- a/k8/mailcatcher.yaml +++ b/k8/mailcatcher.yaml @@ -20,9 +20,9 @@ spec: ports: - containerPort: 1080 - containerPort: 1025 - env: - - name: HTTPPATH - value: "/mails" + envFrom: + - configMapRef: + name: nms-config --- apiVersion: v1 kind: Service diff --git a/k8/ollama.yaml b/k8/ollama.yaml deleted file mode 100644 index a4b040d..0000000 --- a/k8/ollama.yaml +++ /dev/null @@ -1,40 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: ollama - labels: - app: ollama -spec: - replicas: 1 - selector: - matchLabels: - app: ollama - template: - metadata: - labels: - app: ollama - spec: - containers: - - name: ollama - image: ollama/ollama:latest - ports: - - containerPort: 11434 - # resources: - # limits: - # memory: "2Gi" - # cpu: "500m" ---- -apiVersion: v1 -kind: Service -metadata: - name: ollama - labels: - app: ollama -spec: - selector: - app: ollama - ports: - - protocol: TCP - port: 11434 - targetPort: 11434 - type: ClusterIP diff --git a/k8/postgres.yaml b/k8/postgres.yaml index 81062a6..d7ab41f 100644 --- a/k8/postgres.yaml +++ b/k8/postgres.yaml @@ -20,12 +20,14 @@ spec: ports: - containerPort: 5432 env: - - name: POSTGRES_USER - value: "user" - name: POSTGRES_PASSWORD - value: "password" - - name: POSTGRES_DB - value: "notification_management" + valueFrom: + secretKeyRef: + name: nms-secrets + key: POSTGRES_PASSWORD + envFrom: + - configMapRef: + name: nms-config volumeMounts: - name: postgres-data mountPath: /var/lib/postgresql/data diff --git a/k8/redis.yaml b/k8/redis.yaml index 067a4f5..6fb871f 100644 --- a/k8/redis.yaml +++ b/k8/redis.yaml @@ -19,9 +19,9 @@ spec: image: bitnami/redis:6.0.9 ports: - containerPort: 6379 - env: - - name: ALLOW_EMPTY_PASSWORD - value: "yes" + envFrom: + - configMapRef: + name: nms-config volumeMounts: - name: redis-data mountPath: /bitnami/redis/data diff --git a/k8/secrets.yaml b/k8/secrets.yaml new file mode 100644 index 0000000..40b2dde --- /dev/null +++ b/k8/secrets.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Secret +metadata: + name: nms-secrets +type: Opaque +data: + POSTGRES_PASSWORD: "cGFzc3dvcmQ=" + KEYCLOAK_PASSWORD: "c2VjcmV0" + KEYCLOAK_ADMIN_PASSWORD: "c2VjcmV0" + GF_SECURITY_ADMIN_PASSWORD: "YWRtaW4=" From a8bf812c6d4ac733d28553cf7cb479a7f3ad51df Mon Sep 17 00:00:00 2001 From: MD Towhidul Islam Date: Sat, 16 Aug 2025 10:21:31 +0600 Subject: [PATCH 07/21] GE4-21 # Add deploy script and update Kubernetes manifests for dynamic path replacement --- deploy-keycloak.sh | 15 +++++++++++++++ k8/keycloak.yaml | 2 +- k8/postgres.yaml | 2 +- k8/prometheus.yaml | 2 +- 4 files changed, 18 insertions(+), 3 deletions(-) create mode 100644 deploy-keycloak.sh diff --git a/deploy-keycloak.sh b/deploy-keycloak.sh new file mode 100644 index 0000000..5215736 --- /dev/null +++ b/deploy-keycloak.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +PROJECT_BASE_PATH=$(pwd) + +for file in k8/*.yaml; do + if grep -q "__PROJECT_BASE_PATH__" "$file"; then + echo "Processing $file..." + sed "s|__PROJECT_BASE_PATH__|$PROJECT_BASE_PATH|g" "$file" | kubectl apply -f - + else + echo "Applying $file without path replacement..." + kubectl apply -f "$file" + fi +done + +echo "All Kubernetes manifests in 'k8' directory processed with dynamic path: $PROJECT_BASE_PATH" diff --git a/k8/keycloak.yaml b/k8/keycloak.yaml index 870f4cb..b3b629b 100644 --- a/k8/keycloak.yaml +++ b/k8/keycloak.yaml @@ -43,7 +43,7 @@ spec: volumes: - name: keycloak-import hostPath: - path: /home/towhidul/GoProjects/NotificationManagement/keycloak/import + path: __PROJECT_BASE_PATH__/keycloak/import type: DirectoryOrCreate --- apiVersion: v1 diff --git a/k8/postgres.yaml b/k8/postgres.yaml index d7ab41f..1fae833 100644 --- a/k8/postgres.yaml +++ b/k8/postgres.yaml @@ -39,7 +39,7 @@ spec: claimName: postgres-pv-claim - name: postgres-initdb hostPath: - path: /home/towhidul/GoProjects/NotificationManagement/scripts/db + path: __PROJECT_BASE_PATH__/scripts/db type: DirectoryOrCreate --- apiVersion: v1 diff --git a/k8/prometheus.yaml b/k8/prometheus.yaml index bd4379d..1d206c5 100644 --- a/k8/prometheus.yaml +++ b/k8/prometheus.yaml @@ -30,7 +30,7 @@ spec: volumes: - name: prometheus-config hostPath: - path: /home/towhidul/GoProjects/NotificationManagement/prometheus + path: __PROJECT_BASE_PATH__/prometheus type: DirectoryOrCreate --- apiVersion: v1 From 640bf2dc718c19553a1f395678a86b446dba2f69 Mon Sep 17 00:00:00 2001 From: MD Towhidul Islam Date: Sat, 16 Aug 2025 06:55:18 +0000 Subject: [PATCH 08/21] Fix: Keycloak start-dev command path in k8/keycloak.yaml --- Dockerfile | 1 + deploy-keycloak.sh => deploy-kubernates.sh | 4 ++-- k8/keycloak.yaml | 6 +++--- k8/prometheus.yaml | 18 +++++++++--------- prometheus/prometheus.yml | 0 5 files changed, 15 insertions(+), 14 deletions(-) rename deploy-keycloak.sh => deploy-kubernates.sh (67%) mode change 100644 => 100755 prometheus/prometheus.yml diff --git a/Dockerfile b/Dockerfile index 80ed33e..12f4f75 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,6 +6,7 @@ RUN mkdir /user && \ RUN apk add --no-cache ca-certificates WORKDIR /src COPY ./ ./ +RUN go mod vendor RUN CGO_ENABLED=0 GOFLAGS=-mod=vendor GOOS=linux go build -a -o /app . FROM alpine:latest AS final diff --git a/deploy-keycloak.sh b/deploy-kubernates.sh similarity index 67% rename from deploy-keycloak.sh rename to deploy-kubernates.sh index 5215736..6ed7d04 100644 --- a/deploy-keycloak.sh +++ b/deploy-kubernates.sh @@ -1,7 +1,8 @@ #!/bin/bash PROJECT_BASE_PATH=$(pwd) - +kubectl create configmap prometheus-config --from-file=prometheus.yml=prometheus/prometheus.yml +kubectl create configmap keycloak-import --from-file=gocloak-realm.json=keycloak/import/gocloak-realm.json for file in k8/*.yaml; do if grep -q "__PROJECT_BASE_PATH__" "$file"; then echo "Processing $file..." @@ -11,5 +12,4 @@ for file in k8/*.yaml; do kubectl apply -f "$file" fi done - echo "All Kubernetes manifests in 'k8' directory processed with dynamic path: $PROJECT_BASE_PATH" diff --git a/k8/keycloak.yaml b/k8/keycloak.yaml index b3b629b..9c2a458 100644 --- a/k8/keycloak.yaml +++ b/k8/keycloak.yaml @@ -18,6 +18,7 @@ spec: - name: keycloak image: keycloak/keycloak:25.0.6 command: + - /opt/keycloak/bin/kc.sh - start-dev - --import-realm env: @@ -42,9 +43,8 @@ spec: mountPath: /opt/keycloak/data/import/ volumes: - name: keycloak-import - hostPath: - path: __PROJECT_BASE_PATH__/keycloak/import - type: DirectoryOrCreate + configMap: + name: keycloak-import --- apiVersion: v1 kind: Service diff --git a/k8/prometheus.yaml b/k8/prometheus.yaml index 1d206c5..219ad2d 100644 --- a/k8/prometheus.yaml +++ b/k8/prometheus.yaml @@ -21,17 +21,17 @@ spec: - containerPort: 9090 volumeMounts: - name: prometheus-config - mountPath: /etc/prometheus - command: - - '--config.file=/etc/prometheus/prometheus.yml' - - '--storage.tsdb.path=/prometheus' - - '--web.console.libraries=/usr/share/prometheus/console_libraries' - - '--web.console.templates=/usr/share/prometheus/consoles' + mountPath: /etc/prometheus/ + command: ["prometheus"] + args: + - --config.file=/etc/prometheus/prometheus.yml + - --storage.tsdb.path=/prometheus + - --web.console.libraries=/usr/share/prometheus/console_libraries + - --web.console.templates=/usr/share/prometheus/consoles volumes: - name: prometheus-config - hostPath: - path: __PROJECT_BASE_PATH__/prometheus - type: DirectoryOrCreate + configMap: + name: prometheus-config --- apiVersion: v1 kind: Service diff --git a/prometheus/prometheus.yml b/prometheus/prometheus.yml old mode 100644 new mode 100755 From 9daeecaf7e8d909a39425a0dd9ac5a25b6871818 Mon Sep 17 00:00:00 2001 From: MD Towhidul Islam Date: Sat, 16 Aug 2025 07:50:33 +0000 Subject: [PATCH 09/21] feat: Add Kubernetes deployment for app and worker services Introduce Kubernetes deployment configurations for the main application (`app`) and background worker (`worker`). - Create `k8/app.yaml` and `k8/worker.yaml` to define Deployments and Services for the respective components, utilizing pre-built Docker images. - Update `k8/secrets.yaml` to include `TELEGRAM_TOKEN` and `GEMINI_KEY` for use in Kubernetes deployments. - Modify `docker-compose.yml` to specify explicit image names for `go-nms` and `go-nms-worker`, aligning with the images used in Kubernetes, and make minor adjustments to local development settings. This change enables deploying the application and worker components to a Kubernetes cluster. --- docker-compose.yml | 6 +++-- k8/app.yaml | 59 ++++++++++++++++++++++++++++++++++++++++++++++ k8/secrets.yaml | 2 ++ k8/worker.yaml | 57 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 122 insertions(+), 2 deletions(-) create mode 100644 k8/app.yaml create mode 100644 k8/worker.yaml diff --git a/docker-compose.yml b/docker-compose.yml index 7656860..8d4645b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -81,7 +81,7 @@ services: ports: - "6379:6379" environment: -# - REDIS_PASSWORD= + #- REDIS_PASSWORD= - ALLOW_EMPTY_PASSWORD=yes volumes: - nms_redis_local_db:/bitnami/redis/data @@ -141,9 +141,10 @@ services: context: . dockerfile: Dockerfile container_name: go-nms + image: tuhin47/go-nms:lastest command: - serve -# restart: unless-stopped + #restart: unless-stopped ports: - "8080:8080" environment: @@ -173,6 +174,7 @@ services: context: . dockerfile: Dockerfile container_name: go-nms-worker + image: tuhin47/go-worker:lastest command: - worker restart: unless-stopped diff --git a/k8/app.yaml b/k8/app.yaml new file mode 100644 index 0000000..2864ef6 --- /dev/null +++ b/k8/app.yaml @@ -0,0 +1,59 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: app + labels: + app: app +spec: + replicas: 1 + selector: + matchLabels: + app: app + template: + metadata: + labels: + app: app + spec: + containers: + - name: app + image: tuhin47/go-nms:lastest + imagePullPolicy: IfNotPresent + command: ["/app"] + args: ["serve"] + ports: + - containerPort: 8080 + env: + - name: TELEGRAM_TOKEN + valueFrom: + secretKeyRef: + name: nms-secrets + key: TELEGRAM_TOKEN + - name: GEMINI_KEY + valueFrom: + secretKeyRef: + name: nms-secrets + key: GEMINI_KEY + envFrom: + - configMapRef: + name: nms-config + volumeMounts: + - name: app-log + mountPath: /app.log + volumes: + - name: app-log + emptyDir: {} # Or a persistent volume if logs need to persist +--- +apiVersion: v1 +kind: Service +metadata: + name: app + labels: + app: app +spec: + selector: + app: app + ports: + - protocol: TCP + port: 8080 + targetPort: 8080 + type: ClusterIP diff --git a/k8/secrets.yaml b/k8/secrets.yaml index 40b2dde..1b36bfa 100644 --- a/k8/secrets.yaml +++ b/k8/secrets.yaml @@ -8,3 +8,5 @@ data: KEYCLOAK_PASSWORD: "c2VjcmV0" KEYCLOAK_ADMIN_PASSWORD: "c2VjcmV0" GF_SECURITY_ADMIN_PASSWORD: "YWRtaW4=" + TELEGRAM_TOKEN: "" + GEMINI_KEY: "" diff --git a/k8/worker.yaml b/k8/worker.yaml new file mode 100644 index 0000000..a29720a --- /dev/null +++ b/k8/worker.yaml @@ -0,0 +1,57 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: worker + labels: + app: worker +spec: + replicas: 1 + selector: + matchLabels: + app: worker + template: + metadata: + labels: + app: worker + spec: + containers: + - name: worker + image: tuhin47/go-worker:lastest + imagePullPolicy: IfNotPresent + command: ["/app"] + args: ["worker"] + env: + - name: TELEGRAM_TOKEN + valueFrom: + secretKeyRef: + name: nms-secrets + key: TELEGRAM_TOKEN + - name: GEMINI_KEY + valueFrom: + secretKeyRef: + name: nms-secrets + key: GEMINI_KEY + envFrom: + - configMapRef: + name: nms-config + volumeMounts: + - name: app-log + mountPath: /app.log + volumes: + - name: app-log + emptyDir: {} # Or a persistent volume if logs need to persist +--- +apiVersion: v1 +kind: Service +metadata: + name: worker + labels: + app: worker +spec: + selector: + app: worker + ports: + - protocol: TCP + port: 8080 # Placeholder port, as worker doesn't expose one in docker-compose + targetPort: 8080 # Placeholder targetPort + type: ClusterIP From fa7ca73c94ba6ede34cb4fa45aba62b0f3753d76 Mon Sep 17 00:00:00 2001 From: MD Towhidul Islam Date: Sat, 16 Aug 2025 17:54:08 +0000 Subject: [PATCH 10/21] Apply changes --- deploy-kubernates.sh | 29 +++++++++++++++++++++++++++++ docker-compose.yml | 11 +++++++---- port_forward.sh | 20 ++++++++++++++++++++ scripts/setup-dev.sh | 2 +- 4 files changed, 57 insertions(+), 5 deletions(-) create mode 100644 port_forward.sh diff --git a/deploy-kubernates.sh b/deploy-kubernates.sh index 6ed7d04..f6939d3 100644 --- a/deploy-kubernates.sh +++ b/deploy-kubernates.sh @@ -1,9 +1,38 @@ #!/bin/bash PROJECT_BASE_PATH=$(pwd) + +SECRETS_SED_COMMANDS="" + +while IFS='=' read -r key value; do + if [[ -z "$key" || -z "$value" ]]; then + continue + fi + + if grep -q " $key:" k8/secrets.yaml; then + ENCODED_VALUE=$(echo -n "$value" | base64) + SECRETS_SED_COMMANDS+=" -e \"s| $key: \".*\"| $key: \\\"$ENCODED_VALUE\\\"|g\"" + SECRETS_SED_COMMANDS+=" -e \"s| $key: .*| $key: \\\"$ENCODED_VALUE\\\"|g\"" + fi +done < .env + +if [[ -n "$SECRETS_SED_COMMANDS" ]]; then + echo "Updating k8/secrets.yaml with values from .env..." + eval "cat k8/secrets.yaml | sed $SECRETS_SED_COMMANDS" | kubectl apply -f - +else + echo "No matching secrets found in .env to update k8/secrets.yaml." + kubectl apply -f k8/secrets.yaml +fi + kubectl create configmap prometheus-config --from-file=prometheus.yml=prometheus/prometheus.yml kubectl create configmap keycloak-import --from-file=gocloak-realm.json=keycloak/import/gocloak-realm.json + for file in k8/*.yaml; do + if [ "$file" == "k8/secrets.yaml" ]; then + echo "Skipping k8/secrets.yaml as it's already processed." + continue + fi + if grep -q "__PROJECT_BASE_PATH__" "$file"; then echo "Processing $file..." sed "s|__PROJECT_BASE_PATH__|$PROJECT_BASE_PATH|g" "$file" | kubectl apply -f - diff --git a/docker-compose.yml b/docker-compose.yml index 8d4645b..f44efd6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -55,7 +55,8 @@ services: - start-dev - --import-realm environment: - KC_HOSTNAME: keycloak + KC_HOSTNAME: $KEYCLOAK_SERVER_URL + KC_HOSTNAME_STRICT: true KEYCLOAK_USER: admin KEYCLOAK_PASSWORD: secret KEYCLOAK_ADMIN: admin @@ -76,7 +77,7 @@ services: redis: image: "bitnami/redis:6.0.9" platform: linux/amd64 # Force x86 architecture for compatibility - container_name: nms-redis + container_name: redis restart: unless-stopped ports: - "6379:6379" @@ -156,7 +157,7 @@ services: - REDIS_HOST - DB_HOST - DB_PORT - - KEYCLOAK_SERVER_URL=http://${HOSTNAME}:8080 + - KEYCLOAK_SERVER_URL volumes: - ./app.log:/app.log depends_on: @@ -189,9 +190,11 @@ services: - DB_PORT volumes: - ./app.log:/app.log + networks: + - nms-network depends_on: redis: - condition: service_healthy + condition: service_started networks: nms-network: driver: bridge diff --git a/port_forward.sh b/port_forward.sh new file mode 100644 index 0000000..2ff66ab --- /dev/null +++ b/port_forward.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +REMOTE_IP="113.11.63.102" +SSH_PORT="42022" +SSH_USER="towhidul" + +LOCAL_PORT1="8080" +LOCAL_PORT2="8081" + +kubectl port-forward service/app :8080 & +kubectl port-forward service/keycloak :8081 & + +ssh -p $SSH_PORT \ + -R $LOCAL_PORT1:localhost:$LOCAL_PORT1 \ + -R $LOCAL_PORT2:localhost:$LOCAL_PORT2 \ + -o "ServerAliveInterval 30" \ + -o "ServerAliveCountMax 3" \ + $SSH_USER@$REMOTE_IP + +kill $(jobs -p) \ No newline at end of file diff --git a/scripts/setup-dev.sh b/scripts/setup-dev.sh index 0bf5a9e..6263f28 100755 --- a/scripts/setup-dev.sh +++ b/scripts/setup-dev.sh @@ -14,7 +14,7 @@ if ! docker info > /dev/null 2>&1; then fi echo "📦 Starting LocalStack..." -docker compose up -d config-server postgres keycloak mailcatcher redis +docker compose up -d config-server postgres keycloak_svc mailcatcher redis echo "⏳ Waiting for config-server to be ready..." sleep 10 From 3553ebf8d2e9f589023b649a0bd70fa41dee7d37 Mon Sep 17 00:00:00 2001 From: MD Towhidul Islam Date: Sun, 17 Aug 2025 03:53:43 +0000 Subject: [PATCH 11/21] Enhance Kubernetes deployment and update local configs * Add support for populating Kubernetes ConfigMaps from `.env` in `deploy-kubernates.sh`. * Update `docker-compose.yml` to use `go-nms` image for the worker and add `app` service dependency. * Switch to `app-config-prod.json` for SSM configuration upload. * Add `TELEGRAM_ENABLED` environment variable to worker Kubernetes deployment. * Improve `port_forward.sh` by explicitly mapping ports. * Update `.gitignore` to include new config files. --- .gitignore | 4 +++- Dockerfile | 2 +- deploy-kubernates.sh | 18 ++++++++++++++++-- docker-compose.yml | 7 +++---- k8/worker.yaml | 2 ++ port_forward.sh | 4 ++-- 6 files changed, 27 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index 2460426..d305eb7 100644 --- a/.gitignore +++ b/.gitignore @@ -58,4 +58,6 @@ aws/ sample/ keycloak/managed_context -keycloak/test_suite_analysis \ No newline at end of file +keycloak/test_suite_analysis +.prompts/ +app-config-prod.json diff --git a/Dockerfile b/Dockerfile index 12f4f75..713cbee 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,4 +16,4 @@ COPY --from=builder /app /app USER nobody:nobody EXPOSE 8080 -ENTRYPOINT ["/app"] \ No newline at end of file +ENTRYPOINT ["/app"] diff --git a/deploy-kubernates.sh b/deploy-kubernates.sh index f6939d3..97a6261 100644 --- a/deploy-kubernates.sh +++ b/deploy-kubernates.sh @@ -3,6 +3,7 @@ PROJECT_BASE_PATH=$(pwd) SECRETS_SED_COMMANDS="" +CONFIGMAP_SED_COMMANDS="" while IFS='=' read -r key value; do if [[ -z "$key" || -z "$value" ]]; then @@ -14,6 +15,11 @@ while IFS='=' read -r key value; do SECRETS_SED_COMMANDS+=" -e \"s| $key: \".*\"| $key: \\\"$ENCODED_VALUE\\\"|g\"" SECRETS_SED_COMMANDS+=" -e \"s| $key: .*| $key: \\\"$ENCODED_VALUE\\\"|g\"" fi + + if grep -q " $key:" k8/config-maps.yaml; then + CONFIGMAP_SED_COMMANDS+=" -e \"s| $key: \".*\"| $key: \\\"$value\\\"|g\"" + CONFIGMAP_SED_COMMANDS+=" -e \"s| $key: .*| $key: \\\"$value\\\"|g\"" + fi done < .env if [[ -n "$SECRETS_SED_COMMANDS" ]]; then @@ -24,12 +30,20 @@ else kubectl apply -f k8/secrets.yaml fi +if [[ -n "$CONFIGMAP_SED_COMMANDS" ]]; then + echo "Updating k8/config-maps.yaml with values from .env..." + eval "cat k8/config-maps.yaml | sed $CONFIGMAP_SED_COMMANDS" | kubectl apply -f - +else + echo "No matching config map values found in .env to update k8/config-maps.yaml." + kubectl apply -f k8/config-maps.yaml +fi + kubectl create configmap prometheus-config --from-file=prometheus.yml=prometheus/prometheus.yml kubectl create configmap keycloak-import --from-file=gocloak-realm.json=keycloak/import/gocloak-realm.json for file in k8/*.yaml; do - if [ "$file" == "k8/secrets.yaml" ]; then - echo "Skipping k8/secrets.yaml as it's already processed." + if [ "$file" == "k8/secrets.yaml" ] || [ "$file" == "k8/config-maps.yaml" ]; then + echo "Skipping $file as it's already processed." continue fi diff --git a/docker-compose.yml b/docker-compose.yml index f44efd6..aca3a88 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -171,11 +171,8 @@ services: - "keycloak:host-gateway" worker: - build: - context: . - dockerfile: Dockerfile container_name: go-nms-worker - image: tuhin47/go-worker:lastest + image: tuhin47/go-nms:lastest command: - worker restart: unless-stopped @@ -193,6 +190,8 @@ services: networks: - nms-network depends_on: + app: + condition: service_started redis: condition: service_started networks: diff --git a/k8/worker.yaml b/k8/worker.yaml index a29720a..45164c5 100644 --- a/k8/worker.yaml +++ b/k8/worker.yaml @@ -21,6 +21,8 @@ spec: command: ["/app"] args: ["worker"] env: + - name: TELEGRAM_ENABLED + value: "true" - name: TELEGRAM_TOKEN valueFrom: secretKeyRef: diff --git a/port_forward.sh b/port_forward.sh index 2ff66ab..fc382f4 100644 --- a/port_forward.sh +++ b/port_forward.sh @@ -7,8 +7,8 @@ SSH_USER="towhidul" LOCAL_PORT1="8080" LOCAL_PORT2="8081" -kubectl port-forward service/app :8080 & -kubectl port-forward service/keycloak :8081 & +kubectl port-forward service/app 8080:8080 & +kubectl port-forward service/keycloak 8081:8081 & ssh -p $SSH_PORT \ -R $LOCAL_PORT1:localhost:$LOCAL_PORT1 \ From f0720f43e728def62a63ec1e6538ebf9b1328443 Mon Sep 17 00:00:00 2001 From: MD Towhidul Islam Date: Sun, 17 Aug 2025 05:55:30 +0000 Subject: [PATCH 12/21] feat: enhance Kubernetes deployment with config management and environment setup - Add environment variable override for GEMINI_KEY and TELEGRAM_TOKEN in deploy script - Implement config-server deployment waiting and configuration copying - Create AWS LocalStack setup script for SSM parameter management - Simplify port forwarding by removing SSH tunneling - Optimize Docker build by moving go mod operations before source copy - Replace bash with sh in curl command execution for better compatibility --- Dockerfile | 4 +++- deploy-kubernates.sh | 29 ++++++++++++++++++++++++++++- env/aws_export.sh | 20 ++++++++++++++++++++ port_forward.sh | 20 ++------------------ services/curl.go | 2 +- 5 files changed, 54 insertions(+), 21 deletions(-) create mode 100644 env/aws_export.sh diff --git a/Dockerfile b/Dockerfile index 713cbee..833e9f9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,11 +5,13 @@ RUN mkdir /user && \ echo 'nobody:x:65534:' > /user/group RUN apk add --no-cache ca-certificates WORKDIR /src -COPY ./ ./ +COPY go.mod go.sum ./ RUN go mod vendor +COPY ./ ./ RUN CGO_ENABLED=0 GOFLAGS=-mod=vendor GOOS=linux go build -a -o /app . FROM alpine:latest AS final +RUN apk add --no-cache ca-certificates curl COPY --from=builder /user/group /user/passwd /etc/ COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ COPY --from=builder /app /app diff --git a/deploy-kubernates.sh b/deploy-kubernates.sh index 97a6261..b4d13e9 100644 --- a/deploy-kubernates.sh +++ b/deploy-kubernates.sh @@ -6,7 +6,20 @@ SECRETS_SED_COMMANDS="" CONFIGMAP_SED_COMMANDS="" while IFS='=' read -r key value; do - if [[ -z "$key" || -z "$value" ]]; then + if [[ -z "$key" ]]; then + continue + fi + + if [[ "$key" == "GEMINI_KEY" || "$key" == "TELEGRAM_TOKEN" ]]; then + if [[ -n "${!key}" ]]; then + value="${!key}" + echo "Using environment variable for $key" + else + echo "Warning: $key not found in environment variables. Using value from .env file." + fi + fi + + if [[ -z "$value" ]]; then continue fi @@ -56,3 +69,17 @@ for file in k8/*.yaml; do fi done echo "All Kubernetes manifests in 'k8' directory processed with dynamic path: $PROJECT_BASE_PATH" + +echo "Waiting for config-server deployment to be ready..." +kubectl wait --for=condition=available deployment/config-server --timeout=300s + +CONFIG_SERVER_POD=$(kubectl get pods -l app=config-server -o jsonpath='{.items[0].metadata.name}') + +if [ -z "$CONFIG_SERVER_POD" ]; then + echo "Error: config-server pod not found." + exit 1 +fi + +kubectl cp env/app-config-prod.json "$CONFIG_SERVER_POD":/tmp/app-config.json +kubectl cp env/aws_export.sh "$CONFIG_SERVER_POD":/tmp/aws_export.sh +kubectl exec "$CONFIG_SERVER_POD" -- bash /tmp/aws_export.sh; diff --git a/env/aws_export.sh b/env/aws_export.sh new file mode 100644 index 0000000..482c779 --- /dev/null +++ b/env/aws_export.sh @@ -0,0 +1,20 @@ +echo "🔧 Setting up environment variables..." +export AWS_ENDPOINT=http://localhost:4566 +export AWS_REGION=us-east-1 +export AWS_ACCESS_KEY_ID=test +export AWS_SECRET_ACCESS_KEY=test + +echo "📋 Environment variables set:" +echo " AWS_ENDPOINT=$AWS_ENDPOINT" +echo " AWS_REGION=$AWS_REGION" + +export CONFIG_SSM_PARAM="/myapp/config" +export CONFIG_FROM_SSM=true + +aws --endpoint-url=http://localhost:4566 ssm put-parameter \ + --name "$CONFIG_SSM_PARAM" \ + --region "$AWS_REGION" \ + --type "String" \ + --value "$(cat /tmp/app-config.json)" \ + --overwrite + diff --git a/port_forward.sh b/port_forward.sh index fc382f4..5a8573e 100644 --- a/port_forward.sh +++ b/port_forward.sh @@ -1,20 +1,4 @@ #!/bin/bash - -REMOTE_IP="113.11.63.102" -SSH_PORT="42022" -SSH_USER="towhidul" - -LOCAL_PORT1="8080" -LOCAL_PORT2="8081" - -kubectl port-forward service/app 8080:8080 & +kubectl port-forward service/app 8080:8080& kubectl port-forward service/keycloak 8081:8081 & - -ssh -p $SSH_PORT \ - -R $LOCAL_PORT1:localhost:$LOCAL_PORT1 \ - -R $LOCAL_PORT2:localhost:$LOCAL_PORT2 \ - -o "ServerAliveInterval 30" \ - -o "ServerAliveCountMax 3" \ - $SSH_USER@$REMOTE_IP - -kill $(jobs -p) \ No newline at end of file +kubectl port-forward service/mailcatcher 1080:1080 \ No newline at end of file diff --git a/services/curl.go b/services/curl.go index 2f2e7e4..f7ac1e6 100644 --- a/services/curl.go +++ b/services/curl.go @@ -100,7 +100,7 @@ func parseBasicCurl(raw string) (method, url string, headers map[string]string, } func executeCurlCommand(command string) (string, error) { - cmd := exec.Command("bash", "-c", command) + cmd := exec.Command("sh", "-c", command) output, err := cmd.CombinedOutput() if err != nil { return "", errutil.NewAppErrorWithMessage(errutil.ErrCurlCommandExecutionFailed, err, fmt.Sprintf("Output: %s", output)) From b45fd0afa89a744837daa20fa95cf9c42c1e582d Mon Sep 17 00:00:00 2001 From: MD Towhidul Islam Date: Sun, 17 Aug 2025 14:14:15 +0000 Subject: [PATCH 13/21] feat: Implement NGINX Ingress for Kubernetes deployments - Deploy the NGINX Ingress Controller and define initial ingress rules. - Configure Keycloak to use a relative path (`/keycloak`) for ingress routing. - Adjust Prometheus and Grafana configurations for compatibility with ingress. - Add a `--del` option to `deploy-kubernates.sh` for comprehensive resource deletion. - Update existing Kubernetes manifests to align with ingress-based external access. --- deploy-kubernates.sh | 11 +- docker-compose.yml | 4 + ingress-nginx-deploy.yaml | 663 +++++++++++++++++++++++++++++++ k8/config-maps.yaml | 3 + k8/grafana.yaml | 28 +- k8/ingress-nginx-controller.yaml | 266 +++++++++++++ k8/keycloak.yaml | 3 +- k8/notification-ingress.yaml | 55 +++ k8/postgres.yaml | 14 +- k8/prometheus.yaml | 8 + k8/redis.yaml | 28 +- 11 files changed, 1040 insertions(+), 43 deletions(-) create mode 100644 ingress-nginx-deploy.yaml create mode 100644 k8/ingress-nginx-controller.yaml create mode 100644 k8/notification-ingress.yaml diff --git a/deploy-kubernates.sh b/deploy-kubernates.sh index b4d13e9..8bb208f 100644 --- a/deploy-kubernates.sh +++ b/deploy-kubernates.sh @@ -2,6 +2,14 @@ PROJECT_BASE_PATH=$(pwd) +# Check for --del argument +if [[ "$1" == "--del" ]]; then + echo "Deleting all Kubernetes resources..." + kubectl delete deployment,service,configmap,secret,ingress,statefulset --all -n default + echo "Kubernetes resources deleted." + exit 0 +fi + SECRETS_SED_COMMANDS="" CONFIGMAP_SED_COMMANDS="" @@ -30,8 +38,7 @@ while IFS='=' read -r key value; do fi if grep -q " $key:" k8/config-maps.yaml; then - CONFIGMAP_SED_COMMANDS+=" -e \"s| $key: \".*\"| $key: \\\"$value\\\"|g\"" - CONFIGMAP_SED_COMMANDS+=" -e \"s| $key: .*| $key: \\\"$value\\\"|g\"" + CONFIGMAP_SED_COMMANDS+=" -e \"s|^[[:space:]]*$key:.*$| $key: \\\"$value\\\"|g\"" fi done < .env diff --git a/docker-compose.yml b/docker-compose.yml index aca3a88..3823eb2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -63,6 +63,7 @@ services: KEYCLOAK_ADMIN_PASSWORD: secret KC_HEALTH_ENABLED: "true" KC_FEATURES: account-api,account3,authorization,client-policies,impersonation,docker,scripts,admin-fine-grained-authz + KC_HTTP_RELATIVE_PATH: /keycloak ports: - "8081:8080" - "9000:9000" @@ -117,6 +118,7 @@ services: - '--storage.tsdb.path=/prometheus' - '--web.console.libraries=/usr/share/prometheus/console_libraries' - '--web.console.templates=/usr/share/prometheus/consoles' + # - '--web.external-url=/prometheus' extra_hosts: - "host.docker.internal:host-gateway" # Required for Linux to resolve host.docker.internal networks: @@ -130,6 +132,8 @@ services: environment: - GF_SECURITY_ADMIN_USER=admin - GF_SECURITY_ADMIN_PASSWORD=admin + # - GF_SERVER_ROOT_URL=http://localhost:3000/grafana + # - GF_SERVER_SERVE_FROM_SUB_PATH=true volumes: - grafana-data:/var/lib/grafana networks: diff --git a/ingress-nginx-deploy.yaml b/ingress-nginx-deploy.yaml new file mode 100644 index 0000000..13e1163 --- /dev/null +++ b/ingress-nginx-deploy.yaml @@ -0,0 +1,663 @@ +apiVersion: v1 +kind: Namespace +metadata: + labels: + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/name: ingress-nginx + name: ingress-nginx +--- +apiVersion: v1 +automountServiceAccountToken: true +kind: ServiceAccount +metadata: + labels: + app.kubernetes.io/component: controller + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/part-of: ingress-nginx + app.kubernetes.io/version: 1.11.1 + name: ingress-nginx + namespace: ingress-nginx +--- +apiVersion: v1 +automountServiceAccountToken: true +kind: ServiceAccount +metadata: + labels: + app.kubernetes.io/component: admission-webhook + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/part-of: ingress-nginx + app.kubernetes.io/version: 1.11.1 + name: ingress-nginx-admission + namespace: ingress-nginx +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + app.kubernetes.io/component: controller + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/part-of: ingress-nginx + app.kubernetes.io/version: 1.11.1 + name: ingress-nginx + namespace: ingress-nginx +rules: +- apiGroups: + - "" + resources: + - namespaces + verbs: + - get +- apiGroups: + - "" + resources: + - configmaps + - pods + - secrets + - endpoints + verbs: + - get + - list + - watch +- apiGroups: + - "" + resources: + - services + verbs: + - get + - list + - watch +- apiGroups: + - networking.k8s.io + resources: + - ingresses + verbs: + - get + - list + - watch +- apiGroups: + - networking.k8s.io + resources: + - ingresses/status + verbs: + - update +- apiGroups: + - networking.k8s.io + resources: + - ingressclasses + verbs: + - get + - list + - watch +- apiGroups: + - coordination.k8s.io + resourceNames: + - ingress-nginx-leader + resources: + - leases + verbs: + - get + - update +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - create +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +- apiGroups: + - discovery.k8s.io + resources: + - endpointslices + verbs: + - list + - watch + - get +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + app.kubernetes.io/component: admission-webhook + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/part-of: ingress-nginx + app.kubernetes.io/version: 1.11.1 + name: ingress-nginx-admission + namespace: ingress-nginx +rules: +- apiGroups: + - "" + resources: + - secrets + verbs: + - get + - create +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/part-of: ingress-nginx + app.kubernetes.io/version: 1.11.1 + name: ingress-nginx +rules: +- apiGroups: + - "" + resources: + - configmaps + - endpoints + - nodes + - pods + - secrets + - namespaces + verbs: + - list + - watch +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - list + - watch +- apiGroups: + - "" + resources: + - nodes + verbs: + - get +- apiGroups: + - "" + resources: + - services + verbs: + - get + - list + - watch +- apiGroups: + - networking.k8s.io + resources: + - ingresses + verbs: + - get + - list + - watch +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +- apiGroups: + - networking.k8s.io + resources: + - ingresses/status + verbs: + - update +- apiGroups: + - networking.k8s.io + resources: + - ingressclasses + verbs: + - get + - list + - watch +- apiGroups: + - discovery.k8s.io + resources: + - endpointslices + verbs: + - list + - watch + - get +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/component: admission-webhook + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/part-of: ingress-nginx + app.kubernetes.io/version: 1.11.1 + name: ingress-nginx-admission +rules: +- apiGroups: + - admissionregistration.k8s.io + resources: + - validatingwebhookconfigurations + verbs: + - get + - update +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/component: controller + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/part-of: ingress-nginx + app.kubernetes.io/version: 1.11.1 + name: ingress-nginx + namespace: ingress-nginx +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: ingress-nginx +subjects: +- kind: ServiceAccount + name: ingress-nginx + namespace: ingress-nginx +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/component: admission-webhook + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/part-of: ingress-nginx + app.kubernetes.io/version: 1.11.1 + name: ingress-nginx-admission + namespace: ingress-nginx +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: ingress-nginx-admission +subjects: +- kind: ServiceAccount + name: ingress-nginx-admission + namespace: ingress-nginx +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/part-of: ingress-nginx + app.kubernetes.io/version: 1.11.1 + name: ingress-nginx +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: ingress-nginx +subjects: +- kind: ServiceAccount + name: ingress-nginx + namespace: ingress-nginx +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + app.kubernetes.io/component: admission-webhook + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/part-of: ingress-nginx + app.kubernetes.io/version: 1.11.1 + name: ingress-nginx-admission +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: ingress-nginx-admission +subjects: +- kind: ServiceAccount + name: ingress-nginx-admission + namespace: ingress-nginx +--- +apiVersion: v1 +data: + allow-snippet-annotations: "false" +kind: ConfigMap +metadata: + labels: + app.kubernetes.io/component: controller + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/part-of: ingress-nginx + app.kubernetes.io/version: 1.11.1 + name: ingress-nginx-controller + namespace: ingress-nginx +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/component: controller + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/part-of: ingress-nginx + app.kubernetes.io/version: 1.11.1 + name: ingress-nginx-controller + namespace: ingress-nginx +spec: + externalTrafficPolicy: Local + ipFamilies: + - IPv4 + ipFamilyPolicy: SingleStack + ports: + - appProtocol: http + name: http + port: 80 + protocol: TCP + targetPort: http + - appProtocol: https + name: https + port: 443 + protocol: TCP + targetPort: https + selector: + app.kubernetes.io/component: controller + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/name: ingress-nginx + type: LoadBalancer +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/component: controller + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/part-of: ingress-nginx + app.kubernetes.io/version: 1.11.1 + name: ingress-nginx-controller-admission + namespace: ingress-nginx +spec: + ports: + - appProtocol: https + name: https-webhook + port: 443 + targetPort: webhook + selector: + app.kubernetes.io/component: controller + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/name: ingress-nginx + type: ClusterIP +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app.kubernetes.io/component: controller + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/part-of: ingress-nginx + app.kubernetes.io/version: 1.11.1 + name: ingress-nginx-controller + namespace: ingress-nginx +spec: + minReadySeconds: 0 + revisionHistoryLimit: 10 + selector: + matchLabels: + app.kubernetes.io/component: controller + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/name: ingress-nginx + strategy: + rollingUpdate: + maxUnavailable: 1 + type: RollingUpdate + template: + metadata: + labels: + app.kubernetes.io/component: controller + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/part-of: ingress-nginx + app.kubernetes.io/version: 1.11.1 + spec: + containers: + - args: + - /nginx-ingress-controller + - --publish-service=$(POD_NAMESPACE)/ingress-nginx-controller + - --election-id=ingress-nginx-leader + - --controller-class=k8s.io/ingress-nginx + - --ingress-class=nginx + - --configmap=$(POD_NAMESPACE)/ingress-nginx-controller + - --validating-webhook=:8443 + - --validating-webhook-certificate=/usr/local/certificates/cert + - --validating-webhook-key=/usr/local/certificates/key + - --enable-metrics=false + env: + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: LD_PRELOAD + value: /usr/local/lib/libmimalloc.so + image: registry.k8s.io/ingress-nginx/controller:v1.11.1@sha256:e6439a12b52076965928e83b7b56aae6731231677b01e81818bce7fa5c60161a + imagePullPolicy: IfNotPresent + lifecycle: + preStop: + exec: + command: + - /wait-shutdown + livenessProbe: + failureThreshold: 5 + httpGet: + path: /healthz + port: 10254 + scheme: HTTP + initialDelaySeconds: 10 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + name: controller + ports: + - containerPort: 80 + name: http + protocol: TCP + - containerPort: 443 + name: https + protocol: TCP + - containerPort: 8443 + name: webhook + protocol: TCP + readinessProbe: + failureThreshold: 3 + httpGet: + path: /healthz + port: 10254 + scheme: HTTP + initialDelaySeconds: 10 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + resources: + requests: + cpu: 100m + memory: 90Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + add: + - NET_BIND_SERVICE + drop: + - ALL + readOnlyRootFilesystem: false + runAsNonRoot: true + runAsUser: 101 + seccompProfile: + type: RuntimeDefault + volumeMounts: + - mountPath: /usr/local/certificates/ + name: webhook-cert + readOnly: true + dnsPolicy: ClusterFirst + nodeSelector: + kubernetes.io/os: linux + serviceAccountName: ingress-nginx + terminationGracePeriodSeconds: 300 + volumes: + - name: webhook-cert + secret: + secretName: ingress-nginx-admission +--- +apiVersion: batch/v1 +kind: Job +metadata: + labels: + app.kubernetes.io/component: admission-webhook + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/part-of: ingress-nginx + app.kubernetes.io/version: 1.11.1 + name: ingress-nginx-admission-create + namespace: ingress-nginx +spec: + template: + metadata: + labels: + app.kubernetes.io/component: admission-webhook + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/part-of: ingress-nginx + app.kubernetes.io/version: 1.11.1 + name: ingress-nginx-admission-create + spec: + containers: + - args: + - create + - --host=ingress-nginx-controller-admission,ingress-nginx-controller-admission.$(POD_NAMESPACE).svc + - --namespace=$(POD_NAMESPACE) + - --secret-name=ingress-nginx-admission + env: + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + image: registry.k8s.io/ingress-nginx/kube-webhook-certgen:v1.4.1@sha256:36d05b4077fb8e3d13663702fa337f124675ba8667cbd949c03a8e8ea6fa4366 + imagePullPolicy: IfNotPresent + name: create + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 65532 + seccompProfile: + type: RuntimeDefault + nodeSelector: + kubernetes.io/os: linux + restartPolicy: OnFailure + serviceAccountName: ingress-nginx-admission +--- +apiVersion: batch/v1 +kind: Job +metadata: + labels: + app.kubernetes.io/component: admission-webhook + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/part-of: ingress-nginx + app.kubernetes.io/version: 1.11.1 + name: ingress-nginx-admission-patch + namespace: ingress-nginx +spec: + template: + metadata: + labels: + app.kubernetes.io/component: admission-webhook + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/part-of: ingress-nginx + app.kubernetes.io/version: 1.11.1 + name: ingress-nginx-admission-patch + spec: + containers: + - args: + - patch + - --webhook-name=ingress-nginx-admission + - --namespace=$(POD_NAMESPACE) + - --patch-mutating=false + - --secret-name=ingress-nginx-admission + - --patch-failure-policy=Fail + env: + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + image: registry.k8s.io/ingress-nginx/kube-webhook-certgen:v1.4.1@sha256:36d05b4077fb8e3d13663702fa337f124675ba8667cbd949c03a8e8ea6fa4366 + imagePullPolicy: IfNotPresent + name: patch + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 65532 + seccompProfile: + type: RuntimeDefault + nodeSelector: + kubernetes.io/os: linux + restartPolicy: OnFailure + serviceAccountName: ingress-nginx-admission +--- +apiVersion: networking.k8s.io/v1 +kind: IngressClass +metadata: + labels: + app.kubernetes.io/component: controller + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/part-of: ingress-nginx + app.kubernetes.io/version: 1.11.1 + name: nginx +spec: + controller: k8s.io/ingress-nginx +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + labels: + app.kubernetes.io/component: admission-webhook + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/part-of: ingress-nginx + app.kubernetes.io/version: 1.11.1 + name: ingress-nginx-admission +webhooks: +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: ingress-nginx-controller-admission + namespace: ingress-nginx + path: /networking/v1/ingresses + failurePolicy: Fail + matchPolicy: Equivalent + name: validate.nginx.ingress.kubernetes.io + rules: + - apiGroups: + - networking.k8s.io + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - ingresses + sideEffects: None diff --git a/k8/config-maps.yaml b/k8/config-maps.yaml index b67ef44..7fcb5d2 100644 --- a/k8/config-maps.yaml +++ b/k8/config-maps.yaml @@ -21,3 +21,6 @@ data: DB_HOST: "postgres" DB_PORT: "5432" KEYCLOAK_SERVER_URL: "http://keycloak:8080" + GF_SERVER_ROOT_URL: "http://localhost:3000/grafana" + GF_SERVER_SERVE_FROM_SUB_PATH: "true" + KC_HTTP_RELATIVE_PATH: "/keycloak" diff --git a/k8/grafana.yaml b/k8/grafana.yaml index b350ec9..02aa382 100644 --- a/k8/grafana.yaml +++ b/k8/grafana.yaml @@ -1,10 +1,11 @@ apiVersion: apps/v1 -kind: Deployment +kind: StatefulSet metadata: name: grafana labels: app: grafana spec: + serviceName: "grafana" replicas: 1 selector: matchLabels: @@ -31,10 +32,14 @@ spec: volumeMounts: - name: grafana-data mountPath: /var/lib/grafana - volumes: - - name: grafana-data - persistentVolumeClaim: - claimName: grafana-pv-claim + volumeClaimTemplates: + - metadata: + name: grafana-data + spec: + accessModes: [ "ReadWriteOnce" ] + resources: + requests: + storage: 1Gi --- apiVersion: v1 kind: Service @@ -50,16 +55,3 @@ spec: port: 3000 targetPort: 3000 type: ClusterIP ---- -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: grafana-pv-claim - labels: - app: grafana -spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 1Gi diff --git a/k8/ingress-nginx-controller.yaml b/k8/ingress-nginx-controller.yaml new file mode 100644 index 0000000..7c4ae48 --- /dev/null +++ b/k8/ingress-nginx-controller.yaml @@ -0,0 +1,266 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: ingress-nginx + +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: nginx-ingress-serviceaccount + namespace: ingress-nginx + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: nginx-ingress-clusterrole +rules: + - apiGroups: + - "" + resources: + - configmaps + - endpoints + - nodes + - pods + - secrets + verbs: + - list + - watch + - apiGroups: + - "" + resources: + - nodes + verbs: + - get + - apiGroups: + - "" + resources: + - services + verbs: + - get + - list + - watch + - apiGroups: + - extensions + - networking.k8s.io + resources: + - ingresses + verbs: + - get + - list + - watch + - apiGroups: + - "" + resources: + - events + verbs: + - create + - patch + - apiGroups: + - extensions + - networking.k8s.io + resources: + - ingresses/status + verbs: + - update + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: nginx-ingress-role + namespace: ingress-nginx +rules: + - apiGroups: + - "" + resources: + - configmaps + - pods + - secrets + - namespaces + verbs: + - get + - apiGroups: + - "" + resources: + - configmaps + resourceNames: + - "ingress-controller-leader-nginx" + verbs: + - get + - update + - apiGroups: + - "" + resources: + - configmaps + verbs: + - create + - apiGroups: + - "" + resources: + - endpoints + verbs: + - get + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: nginx-ingress-role-nisa-binding + namespace: ingress-nginx +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: nginx-ingress-role +subjects: + - kind: ServiceAccount + name: nginx-ingress-serviceaccount + namespace: ingress-nginx + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: nginx-ingress-clusterrole-nisa-binding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: nginx-ingress-clusterrole +subjects: + - kind: ServiceAccount + name: nginx-ingress-serviceaccount + namespace: ingress-nginx + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-ingress-controller + namespace: ingress-nginx + labels: + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/part-of: ingress-nginx +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/part-of: ingress-nginx + template: + metadata: + labels: + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/part-of: ingress-nginx + annotations: + prometheus.io/port: "10254" + prometheus.io/scrape: "true" + spec: + serviceAccountName: nginx-ingress-serviceaccount + containers: + - name: nginx-ingress-controller + image: k8s.gcr.io/ingress-nginx/controller:v1.1.3 + args: + - /nginx-ingress-controller + - --configmap=$(POD_NAMESPACE)/nginx-configuration + - --tcp-services-configmap=$(POD_NAMESPACE)/tcp-services + - --udp-services-configmap=$(POD_NAMESPACE)/udp-services + - --publish-service=$(POD_NAMESPACE)/ingress-nginx + - --annotations-prefix=nginx.ingress.kubernetes.io + securityContext: + allowPrivilegeEscalation: true + capabilities: + drop: + - ALL + add: + - NET_BIND_SERVICE + runAsUser: 101 + env: + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + ports: + - name: http + containerPort: 80 + - name: https + containerPort: 443 + livenessProbe: + failureThreshold: 3 + httpGet: + path: /healthz + port: 10254 + scheme: HTTP + initialDelaySeconds: 10 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 10 + readinessProbe: + failureThreshold: 3 + httpGet: + path: /healthz + port: 10254 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 10 + +--- +apiVersion: v1 +kind: Service +metadata: + name: ingress-nginx + namespace: ingress-nginx + labels: + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/part-of: ingress-nginx +spec: + type: NodePort + ports: + - name: http + port: 80 + targetPort: 80 + protocol: TCP + nodePort: 30080 + - name: https + port: 443 + targetPort: 443 + protocol: TCP + nodePort: 30443 + selector: + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/part-of: ingress-nginx + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: nginx-configuration + namespace: ingress-nginx + labels: + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/part-of: ingress-nginx + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: tcp-services + namespace: ingress-nginx + labels: + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/part-of: ingress-nginx + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: udp-services + namespace: ingress-nginx + labels: + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/part-of: ingress-nginx \ No newline at end of file diff --git a/k8/keycloak.yaml b/k8/keycloak.yaml index 9c2a458..68fc056 100644 --- a/k8/keycloak.yaml +++ b/k8/keycloak.yaml @@ -1,10 +1,11 @@ apiVersion: apps/v1 -kind: Deployment +kind: StatefulSet metadata: name: keycloak labels: app: keycloak spec: + serviceName: "keycloak" replicas: 1 selector: matchLabels: diff --git a/k8/notification-ingress.yaml b/k8/notification-ingress.yaml new file mode 100644 index 0000000..db1bb23 --- /dev/null +++ b/k8/notification-ingress.yaml @@ -0,0 +1,55 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: notification-management-ingress + namespace: default + annotations: + kubernetes.io/ingress.class: "nginx" + # nginx.ingress.kubernetes.io/rewrite-target: /$2 + nginx.ingress.kubernetes.io/use-regex: "true" +spec: + rules: + - http: + paths: + - path: /api(/|$)(.*) + pathType: ImplementationSpecific + backend: + service: + name: app + port: + number: 8080 + # - path: /config(/|$)(.*) + # pathType: ImplementationSpecific + # backend: + # service: + # name: config-server + # port: + # number: 4566 + - path: /grafana(/|$)(.*) + pathType: ImplementationSpecific + backend: + service: + name: grafana + port: + number: 3000 + - path: /keycloak(/|$)(.*) + pathType: ImplementationSpecific + backend: + service: + name: keycloak + port: + number: 8081 # Using the http port for Keycloak + - path: /mails(/|$)(.*) + pathType: ImplementationSpecific + backend: + service: + name: mailcatcher + port: + number: 1080 + - path: /prometheus(/|$)(.*) + pathType: ImplementationSpecific + backend: + service: + name: prometheus + port: + number: 9090 diff --git a/k8/postgres.yaml b/k8/postgres.yaml index 1fae833..f24a837 100644 --- a/k8/postgres.yaml +++ b/k8/postgres.yaml @@ -1,10 +1,11 @@ apiVersion: apps/v1 -kind: Deployment +kind: StatefulSet metadata: name: postgres labels: app: postgres spec: + serviceName: "postgres" replicas: 1 selector: matchLabels: @@ -34,13 +35,18 @@ spec: - name: postgres-initdb mountPath: /docker-entrypoint-initdb.d/ volumes: - - name: postgres-data - persistentVolumeClaim: - claimName: postgres-pv-claim - name: postgres-initdb hostPath: path: __PROJECT_BASE_PATH__/scripts/db type: DirectoryOrCreate + volumeClaimTemplates: + - metadata: + name: postgres-data + spec: + accessModes: [ "ReadWriteOnce" ] + resources: + requests: + storage: 1Gi --- apiVersion: v1 kind: Service diff --git a/k8/prometheus.yaml b/k8/prometheus.yaml index 219ad2d..7cd369c 100644 --- a/k8/prometheus.yaml +++ b/k8/prometheus.yaml @@ -22,12 +22,20 @@ spec: volumeMounts: - name: prometheus-config mountPath: /etc/prometheus/ + resources: + requests: + memory: "256Mi" + cpu: "100m" + limits: + memory: "512Mi" + cpu: "200m" command: ["prometheus"] args: - --config.file=/etc/prometheus/prometheus.yml - --storage.tsdb.path=/prometheus - --web.console.libraries=/usr/share/prometheus/console_libraries - --web.console.templates=/usr/share/prometheus/consoles + - '--web.external-url=/prometheus' volumes: - name: prometheus-config configMap: diff --git a/k8/redis.yaml b/k8/redis.yaml index 6fb871f..ef71ea6 100644 --- a/k8/redis.yaml +++ b/k8/redis.yaml @@ -1,10 +1,11 @@ apiVersion: apps/v1 -kind: Deployment +kind: StatefulSet metadata: name: redis labels: app: redis spec: + serviceName: "redis" replicas: 1 selector: matchLabels: @@ -25,10 +26,14 @@ spec: volumeMounts: - name: redis-data mountPath: /bitnami/redis/data - volumes: - - name: redis-data - persistentVolumeClaim: - claimName: redis-pv-claim + volumeClaimTemplates: + - metadata: + name: redis-data + spec: + accessModes: [ "ReadWriteOnce" ] + resources: + requests: + storage: 1Gi --- apiVersion: v1 kind: Service @@ -44,16 +49,3 @@ spec: port: 6379 targetPort: 6379 type: ClusterIP ---- -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: redis-pv-claim - labels: - app: redis -spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 1Gi From 75cfe72f9730dbfe189b299f7c59d70b075e03ee Mon Sep 17 00:00:00 2001 From: MD Towhidul Islam Date: Sun, 17 Aug 2025 21:46:44 +0600 Subject: [PATCH 14/21] feat: Update port forwarding and API endpoints for health and metrics --- cmd/serve.go | 4 ++-- models/telegram.go | 2 +- port_forward.sh | 4 +--- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/cmd/serve.go b/cmd/serve.go index ad49b64..f7aa059 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -24,13 +24,13 @@ var serveCmd = &cobra.Command{ app := fx.New( server.Module, fx.Invoke(func(lc fx.Lifecycle, e *echo.Echo) { - e.GET("/health", func(c echo.Context) error { + e.GET("/api/health", func(c echo.Context) error { return c.JSON(http.StatusOK, map[string]string{"status": "ok"}) }) // Add Prometheus middleware e.Use(echoprometheus.NewMiddleware("notification_management")) - e.GET("/metrics", echoprometheus.NewHandler()) + e.GET("/api/metrics", echoprometheus.NewHandler()) port := *config.App().Port addr := fmt.Sprintf(":%d", port) diff --git a/models/telegram.go b/models/telegram.go index 0ad4a02..c919850 100644 --- a/models/telegram.go +++ b/models/telegram.go @@ -7,7 +7,7 @@ import ( type Telegram struct { gorm.Model UserID *uint `gorm:"index"` - User User `gorm:"foreignKey:UserID"` + User User `gorm:"foreignKey:UserID" json:"-"` ChatID int64 `gorm:"uniqueIndex;not null"` Otp string `gorm:"size:255;not null"` } diff --git a/port_forward.sh b/port_forward.sh index 5a8573e..31acf75 100644 --- a/port_forward.sh +++ b/port_forward.sh @@ -1,4 +1,2 @@ #!/bin/bash -kubectl port-forward service/app 8080:8080& -kubectl port-forward service/keycloak 8081:8081 & -kubectl port-forward service/mailcatcher 1080:1080 \ No newline at end of file +kubectl port-forward service/app 4747:80 -n ingress-nginx \ No newline at end of file From 8c1c3d279c60a4d3e04e49695d47e265e300c330 Mon Sep 17 00:00:00 2001 From: MD Towhidul Islam Date: Sun, 17 Aug 2025 21:59:55 +0600 Subject: [PATCH 15/21] feat: Update port forwarding, Prometheus configuration, and worker resource limits --- ingress-nginx-deploy.yaml | 663 -------------------------------------- k8/prometheus.yaml | 2 +- k8/worker.yaml | 9 +- port_forward.sh | 2 +- prometheus/prometheus.yml | 7 +- 5 files changed, 14 insertions(+), 669 deletions(-) delete mode 100644 ingress-nginx-deploy.yaml diff --git a/ingress-nginx-deploy.yaml b/ingress-nginx-deploy.yaml deleted file mode 100644 index 13e1163..0000000 --- a/ingress-nginx-deploy.yaml +++ /dev/null @@ -1,663 +0,0 @@ -apiVersion: v1 -kind: Namespace -metadata: - labels: - app.kubernetes.io/instance: ingress-nginx - app.kubernetes.io/name: ingress-nginx - name: ingress-nginx ---- -apiVersion: v1 -automountServiceAccountToken: true -kind: ServiceAccount -metadata: - labels: - app.kubernetes.io/component: controller - app.kubernetes.io/instance: ingress-nginx - app.kubernetes.io/name: ingress-nginx - app.kubernetes.io/part-of: ingress-nginx - app.kubernetes.io/version: 1.11.1 - name: ingress-nginx - namespace: ingress-nginx ---- -apiVersion: v1 -automountServiceAccountToken: true -kind: ServiceAccount -metadata: - labels: - app.kubernetes.io/component: admission-webhook - app.kubernetes.io/instance: ingress-nginx - app.kubernetes.io/name: ingress-nginx - app.kubernetes.io/part-of: ingress-nginx - app.kubernetes.io/version: 1.11.1 - name: ingress-nginx-admission - namespace: ingress-nginx ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - labels: - app.kubernetes.io/component: controller - app.kubernetes.io/instance: ingress-nginx - app.kubernetes.io/name: ingress-nginx - app.kubernetes.io/part-of: ingress-nginx - app.kubernetes.io/version: 1.11.1 - name: ingress-nginx - namespace: ingress-nginx -rules: -- apiGroups: - - "" - resources: - - namespaces - verbs: - - get -- apiGroups: - - "" - resources: - - configmaps - - pods - - secrets - - endpoints - verbs: - - get - - list - - watch -- apiGroups: - - "" - resources: - - services - verbs: - - get - - list - - watch -- apiGroups: - - networking.k8s.io - resources: - - ingresses - verbs: - - get - - list - - watch -- apiGroups: - - networking.k8s.io - resources: - - ingresses/status - verbs: - - update -- apiGroups: - - networking.k8s.io - resources: - - ingressclasses - verbs: - - get - - list - - watch -- apiGroups: - - coordination.k8s.io - resourceNames: - - ingress-nginx-leader - resources: - - leases - verbs: - - get - - update -- apiGroups: - - coordination.k8s.io - resources: - - leases - verbs: - - create -- apiGroups: - - "" - resources: - - events - verbs: - - create - - patch -- apiGroups: - - discovery.k8s.io - resources: - - endpointslices - verbs: - - list - - watch - - get ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - labels: - app.kubernetes.io/component: admission-webhook - app.kubernetes.io/instance: ingress-nginx - app.kubernetes.io/name: ingress-nginx - app.kubernetes.io/part-of: ingress-nginx - app.kubernetes.io/version: 1.11.1 - name: ingress-nginx-admission - namespace: ingress-nginx -rules: -- apiGroups: - - "" - resources: - - secrets - verbs: - - get - - create ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - labels: - app.kubernetes.io/instance: ingress-nginx - app.kubernetes.io/name: ingress-nginx - app.kubernetes.io/part-of: ingress-nginx - app.kubernetes.io/version: 1.11.1 - name: ingress-nginx -rules: -- apiGroups: - - "" - resources: - - configmaps - - endpoints - - nodes - - pods - - secrets - - namespaces - verbs: - - list - - watch -- apiGroups: - - coordination.k8s.io - resources: - - leases - verbs: - - list - - watch -- apiGroups: - - "" - resources: - - nodes - verbs: - - get -- apiGroups: - - "" - resources: - - services - verbs: - - get - - list - - watch -- apiGroups: - - networking.k8s.io - resources: - - ingresses - verbs: - - get - - list - - watch -- apiGroups: - - "" - resources: - - events - verbs: - - create - - patch -- apiGroups: - - networking.k8s.io - resources: - - ingresses/status - verbs: - - update -- apiGroups: - - networking.k8s.io - resources: - - ingressclasses - verbs: - - get - - list - - watch -- apiGroups: - - discovery.k8s.io - resources: - - endpointslices - verbs: - - list - - watch - - get ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - labels: - app.kubernetes.io/component: admission-webhook - app.kubernetes.io/instance: ingress-nginx - app.kubernetes.io/name: ingress-nginx - app.kubernetes.io/part-of: ingress-nginx - app.kubernetes.io/version: 1.11.1 - name: ingress-nginx-admission -rules: -- apiGroups: - - admissionregistration.k8s.io - resources: - - validatingwebhookconfigurations - verbs: - - get - - update ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - labels: - app.kubernetes.io/component: controller - app.kubernetes.io/instance: ingress-nginx - app.kubernetes.io/name: ingress-nginx - app.kubernetes.io/part-of: ingress-nginx - app.kubernetes.io/version: 1.11.1 - name: ingress-nginx - namespace: ingress-nginx -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: Role - name: ingress-nginx -subjects: -- kind: ServiceAccount - name: ingress-nginx - namespace: ingress-nginx ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - labels: - app.kubernetes.io/component: admission-webhook - app.kubernetes.io/instance: ingress-nginx - app.kubernetes.io/name: ingress-nginx - app.kubernetes.io/part-of: ingress-nginx - app.kubernetes.io/version: 1.11.1 - name: ingress-nginx-admission - namespace: ingress-nginx -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: Role - name: ingress-nginx-admission -subjects: -- kind: ServiceAccount - name: ingress-nginx-admission - namespace: ingress-nginx ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - labels: - app.kubernetes.io/instance: ingress-nginx - app.kubernetes.io/name: ingress-nginx - app.kubernetes.io/part-of: ingress-nginx - app.kubernetes.io/version: 1.11.1 - name: ingress-nginx -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: ingress-nginx -subjects: -- kind: ServiceAccount - name: ingress-nginx - namespace: ingress-nginx ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - labels: - app.kubernetes.io/component: admission-webhook - app.kubernetes.io/instance: ingress-nginx - app.kubernetes.io/name: ingress-nginx - app.kubernetes.io/part-of: ingress-nginx - app.kubernetes.io/version: 1.11.1 - name: ingress-nginx-admission -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: ingress-nginx-admission -subjects: -- kind: ServiceAccount - name: ingress-nginx-admission - namespace: ingress-nginx ---- -apiVersion: v1 -data: - allow-snippet-annotations: "false" -kind: ConfigMap -metadata: - labels: - app.kubernetes.io/component: controller - app.kubernetes.io/instance: ingress-nginx - app.kubernetes.io/name: ingress-nginx - app.kubernetes.io/part-of: ingress-nginx - app.kubernetes.io/version: 1.11.1 - name: ingress-nginx-controller - namespace: ingress-nginx ---- -apiVersion: v1 -kind: Service -metadata: - labels: - app.kubernetes.io/component: controller - app.kubernetes.io/instance: ingress-nginx - app.kubernetes.io/name: ingress-nginx - app.kubernetes.io/part-of: ingress-nginx - app.kubernetes.io/version: 1.11.1 - name: ingress-nginx-controller - namespace: ingress-nginx -spec: - externalTrafficPolicy: Local - ipFamilies: - - IPv4 - ipFamilyPolicy: SingleStack - ports: - - appProtocol: http - name: http - port: 80 - protocol: TCP - targetPort: http - - appProtocol: https - name: https - port: 443 - protocol: TCP - targetPort: https - selector: - app.kubernetes.io/component: controller - app.kubernetes.io/instance: ingress-nginx - app.kubernetes.io/name: ingress-nginx - type: LoadBalancer ---- -apiVersion: v1 -kind: Service -metadata: - labels: - app.kubernetes.io/component: controller - app.kubernetes.io/instance: ingress-nginx - app.kubernetes.io/name: ingress-nginx - app.kubernetes.io/part-of: ingress-nginx - app.kubernetes.io/version: 1.11.1 - name: ingress-nginx-controller-admission - namespace: ingress-nginx -spec: - ports: - - appProtocol: https - name: https-webhook - port: 443 - targetPort: webhook - selector: - app.kubernetes.io/component: controller - app.kubernetes.io/instance: ingress-nginx - app.kubernetes.io/name: ingress-nginx - type: ClusterIP ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - labels: - app.kubernetes.io/component: controller - app.kubernetes.io/instance: ingress-nginx - app.kubernetes.io/name: ingress-nginx - app.kubernetes.io/part-of: ingress-nginx - app.kubernetes.io/version: 1.11.1 - name: ingress-nginx-controller - namespace: ingress-nginx -spec: - minReadySeconds: 0 - revisionHistoryLimit: 10 - selector: - matchLabels: - app.kubernetes.io/component: controller - app.kubernetes.io/instance: ingress-nginx - app.kubernetes.io/name: ingress-nginx - strategy: - rollingUpdate: - maxUnavailable: 1 - type: RollingUpdate - template: - metadata: - labels: - app.kubernetes.io/component: controller - app.kubernetes.io/instance: ingress-nginx - app.kubernetes.io/name: ingress-nginx - app.kubernetes.io/part-of: ingress-nginx - app.kubernetes.io/version: 1.11.1 - spec: - containers: - - args: - - /nginx-ingress-controller - - --publish-service=$(POD_NAMESPACE)/ingress-nginx-controller - - --election-id=ingress-nginx-leader - - --controller-class=k8s.io/ingress-nginx - - --ingress-class=nginx - - --configmap=$(POD_NAMESPACE)/ingress-nginx-controller - - --validating-webhook=:8443 - - --validating-webhook-certificate=/usr/local/certificates/cert - - --validating-webhook-key=/usr/local/certificates/key - - --enable-metrics=false - env: - - name: POD_NAME - valueFrom: - fieldRef: - fieldPath: metadata.name - - name: POD_NAMESPACE - valueFrom: - fieldRef: - fieldPath: metadata.namespace - - name: LD_PRELOAD - value: /usr/local/lib/libmimalloc.so - image: registry.k8s.io/ingress-nginx/controller:v1.11.1@sha256:e6439a12b52076965928e83b7b56aae6731231677b01e81818bce7fa5c60161a - imagePullPolicy: IfNotPresent - lifecycle: - preStop: - exec: - command: - - /wait-shutdown - livenessProbe: - failureThreshold: 5 - httpGet: - path: /healthz - port: 10254 - scheme: HTTP - initialDelaySeconds: 10 - periodSeconds: 10 - successThreshold: 1 - timeoutSeconds: 1 - name: controller - ports: - - containerPort: 80 - name: http - protocol: TCP - - containerPort: 443 - name: https - protocol: TCP - - containerPort: 8443 - name: webhook - protocol: TCP - readinessProbe: - failureThreshold: 3 - httpGet: - path: /healthz - port: 10254 - scheme: HTTP - initialDelaySeconds: 10 - periodSeconds: 10 - successThreshold: 1 - timeoutSeconds: 1 - resources: - requests: - cpu: 100m - memory: 90Mi - securityContext: - allowPrivilegeEscalation: false - capabilities: - add: - - NET_BIND_SERVICE - drop: - - ALL - readOnlyRootFilesystem: false - runAsNonRoot: true - runAsUser: 101 - seccompProfile: - type: RuntimeDefault - volumeMounts: - - mountPath: /usr/local/certificates/ - name: webhook-cert - readOnly: true - dnsPolicy: ClusterFirst - nodeSelector: - kubernetes.io/os: linux - serviceAccountName: ingress-nginx - terminationGracePeriodSeconds: 300 - volumes: - - name: webhook-cert - secret: - secretName: ingress-nginx-admission ---- -apiVersion: batch/v1 -kind: Job -metadata: - labels: - app.kubernetes.io/component: admission-webhook - app.kubernetes.io/instance: ingress-nginx - app.kubernetes.io/name: ingress-nginx - app.kubernetes.io/part-of: ingress-nginx - app.kubernetes.io/version: 1.11.1 - name: ingress-nginx-admission-create - namespace: ingress-nginx -spec: - template: - metadata: - labels: - app.kubernetes.io/component: admission-webhook - app.kubernetes.io/instance: ingress-nginx - app.kubernetes.io/name: ingress-nginx - app.kubernetes.io/part-of: ingress-nginx - app.kubernetes.io/version: 1.11.1 - name: ingress-nginx-admission-create - spec: - containers: - - args: - - create - - --host=ingress-nginx-controller-admission,ingress-nginx-controller-admission.$(POD_NAMESPACE).svc - - --namespace=$(POD_NAMESPACE) - - --secret-name=ingress-nginx-admission - env: - - name: POD_NAMESPACE - valueFrom: - fieldRef: - fieldPath: metadata.namespace - image: registry.k8s.io/ingress-nginx/kube-webhook-certgen:v1.4.1@sha256:36d05b4077fb8e3d13663702fa337f124675ba8667cbd949c03a8e8ea6fa4366 - imagePullPolicy: IfNotPresent - name: create - securityContext: - allowPrivilegeEscalation: false - capabilities: - drop: - - ALL - readOnlyRootFilesystem: true - runAsNonRoot: true - runAsUser: 65532 - seccompProfile: - type: RuntimeDefault - nodeSelector: - kubernetes.io/os: linux - restartPolicy: OnFailure - serviceAccountName: ingress-nginx-admission ---- -apiVersion: batch/v1 -kind: Job -metadata: - labels: - app.kubernetes.io/component: admission-webhook - app.kubernetes.io/instance: ingress-nginx - app.kubernetes.io/name: ingress-nginx - app.kubernetes.io/part-of: ingress-nginx - app.kubernetes.io/version: 1.11.1 - name: ingress-nginx-admission-patch - namespace: ingress-nginx -spec: - template: - metadata: - labels: - app.kubernetes.io/component: admission-webhook - app.kubernetes.io/instance: ingress-nginx - app.kubernetes.io/name: ingress-nginx - app.kubernetes.io/part-of: ingress-nginx - app.kubernetes.io/version: 1.11.1 - name: ingress-nginx-admission-patch - spec: - containers: - - args: - - patch - - --webhook-name=ingress-nginx-admission - - --namespace=$(POD_NAMESPACE) - - --patch-mutating=false - - --secret-name=ingress-nginx-admission - - --patch-failure-policy=Fail - env: - - name: POD_NAMESPACE - valueFrom: - fieldRef: - fieldPath: metadata.namespace - image: registry.k8s.io/ingress-nginx/kube-webhook-certgen:v1.4.1@sha256:36d05b4077fb8e3d13663702fa337f124675ba8667cbd949c03a8e8ea6fa4366 - imagePullPolicy: IfNotPresent - name: patch - securityContext: - allowPrivilegeEscalation: false - capabilities: - drop: - - ALL - readOnlyRootFilesystem: true - runAsNonRoot: true - runAsUser: 65532 - seccompProfile: - type: RuntimeDefault - nodeSelector: - kubernetes.io/os: linux - restartPolicy: OnFailure - serviceAccountName: ingress-nginx-admission ---- -apiVersion: networking.k8s.io/v1 -kind: IngressClass -metadata: - labels: - app.kubernetes.io/component: controller - app.kubernetes.io/instance: ingress-nginx - app.kubernetes.io/name: ingress-nginx - app.kubernetes.io/part-of: ingress-nginx - app.kubernetes.io/version: 1.11.1 - name: nginx -spec: - controller: k8s.io/ingress-nginx ---- -apiVersion: admissionregistration.k8s.io/v1 -kind: ValidatingWebhookConfiguration -metadata: - labels: - app.kubernetes.io/component: admission-webhook - app.kubernetes.io/instance: ingress-nginx - app.kubernetes.io/name: ingress-nginx - app.kubernetes.io/part-of: ingress-nginx - app.kubernetes.io/version: 1.11.1 - name: ingress-nginx-admission -webhooks: -- admissionReviewVersions: - - v1 - clientConfig: - service: - name: ingress-nginx-controller-admission - namespace: ingress-nginx - path: /networking/v1/ingresses - failurePolicy: Fail - matchPolicy: Equivalent - name: validate.nginx.ingress.kubernetes.io - rules: - - apiGroups: - - networking.k8s.io - apiVersions: - - v1 - operations: - - CREATE - - UPDATE - resources: - - ingresses - sideEffects: None diff --git a/k8/prometheus.yaml b/k8/prometheus.yaml index 7cd369c..24f5deb 100644 --- a/k8/prometheus.yaml +++ b/k8/prometheus.yaml @@ -35,7 +35,7 @@ spec: - --storage.tsdb.path=/prometheus - --web.console.libraries=/usr/share/prometheus/console_libraries - --web.console.templates=/usr/share/prometheus/consoles - - '--web.external-url=/prometheus' + - --web.external-url=/prometheus volumes: - name: prometheus-config configMap: diff --git a/k8/worker.yaml b/k8/worker.yaml index 45164c5..36ce3fc 100644 --- a/k8/worker.yaml +++ b/k8/worker.yaml @@ -16,10 +16,17 @@ spec: spec: containers: - name: worker - image: tuhin47/go-worker:lastest + image: tuhin47/go-nms:lastest imagePullPolicy: IfNotPresent command: ["/app"] args: ["worker"] + resources: + limits: + cpu: "500m" + memory: "512Mi" + requests: + cpu: "250m" + memory: "256Mi" env: - name: TELEGRAM_ENABLED value: "true" diff --git a/port_forward.sh b/port_forward.sh index 31acf75..b8729c7 100644 --- a/port_forward.sh +++ b/port_forward.sh @@ -1,2 +1,2 @@ #!/bin/bash -kubectl port-forward service/app 4747:80 -n ingress-nginx \ No newline at end of file +kubectl port-forward service/ingress-nginx 4747:80 -n ingress-nginx \ No newline at end of file diff --git a/prometheus/prometheus.yml b/prometheus/prometheus.yml index 5a4ec69..bd4722d 100755 --- a/prometheus/prometheus.yml +++ b/prometheus/prometheus.yml @@ -1,11 +1,12 @@ global: - scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute. - evaluation_interval: 15s # Evaluate rules every 15 seconds. Default is every 1 minute. + scrape_interval: 15s + evaluation_interval: 15s scrape_configs: - job_name: 'notification-management' static_configs: - - targets: ['host.docker.internal:8080'] # Assuming your Go app runs on 8080 and exposes /metrics + - targets: ['app:8080'] + metrics_path: /api/metrics # Queries # sum(notification_management_requests_total) by (url) \ No newline at end of file From 7930de2ea963f35e407b5f4e0b0b27cfe0b51804 Mon Sep 17 00:00:00 2001 From: MD Towhidul Islam Date: Mon, 18 Aug 2025 22:54:33 +0600 Subject: [PATCH 16/21] feat: Improve local dev setup and enhance observability - Add `minikube.sh` script for consistent Minikube cluster setup, including resource allocation and host directory mounting. - Hide `APISecret` from JSON serialization in `models/ai.go` to prevent accidental exposure of sensitive keys. - Expand `prometheus.yml` with comprehensive query examples for HTTP performance, Go runtime, and process metrics, aiding in monitoring and dashboard creation. --- minikube.sh | 12 ++++++++++ models/ai.go | 2 +- prometheus/prometheus.yml | 49 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 minikube.sh diff --git a/minikube.sh b/minikube.sh new file mode 100644 index 0000000..beb9ab1 --- /dev/null +++ b/minikube.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +HOST_DIR=$(pwd) +VM_DIR=$(pwd) +minikube start \ + --mount \ + --mount-string="${HOST_DIR}:${VM_DIR}" \ + --driver=docker \ + --cpus=3 \ + --memory=10g \ + --force + diff --git a/models/ai.go b/models/ai.go index d6a4c6d..9de75ac 100644 --- a/models/ai.go +++ b/models/ai.go @@ -26,7 +26,7 @@ type GeminiModel struct { AIModel `mapper:"inherit"` Name string `gorm:"size:255;not null" json:"name"` ModelName string `gorm:"size:255;not null;check:model_name <> '';index:idx_ai_model_model_secret,unique" json:"model"` - APISecret EncryptedString `gorm:"size:500;index:idx_ai_model_model_secret,unique" json:"api_secret"` + APISecret EncryptedString `gorm:"size:500;index:idx_ai_model_model_secret,unique" json:"-"` } func (d *AIModel) GetType() string { diff --git a/prometheus/prometheus.yml b/prometheus/prometheus.yml index bd4722d..b13c548 100755 --- a/prometheus/prometheus.yml +++ b/prometheus/prometheus.yml @@ -9,4 +9,51 @@ scrape_configs: metrics_path: /api/metrics # Queries -# sum(notification_management_requests_total) by (url) \ No newline at end of file +# sum(notification_management_requests_total) by (url) +# +# More queries for Grafana dashboard: + +# HTTP Request Performance Reports +# Request Volume/Rate: +# sum(notification_management_requests_total) by (url, method) +# rate(notification_management_requests_total[5m]) by (url, method) + +# Error Rates: +# sum(rate(notification_management_requests_total{code=~"5.."}[5m])) by (url) +# sum(rate(notification_management_requests_total{code=~"4.."}[5m])) by (url) +# sum(notification_management_requests_total{code=~"5.."}) by (url) + +# Request Latency/Duration: +# histogram_quantile(0.99, sum(rate(notification_management_request_duration_seconds_bucket[5m])) by (le, url, method)) +# rate(notification_management_request_duration_seconds_sum[5m]) / rate(notification_management_request_duration_seconds_count[5m]) + +# Request and Response Sizes: +# histogram_quantile(0.99, sum(rate(notification_management_request_size_bytes_bucket[5m])) by (le, url, method)) +# histogram_quantile(0.99, sum(rate(notification_management_response_size_bytes_bucket[5m])) by (le, url, method)) + +# Go Application Runtime Reports +# Goroutine Count: +# go_goroutines + +# Memory Usage: +# go_memstats_alloc_bytes +# go_memstats_sys_bytes +# rate(go_memstats_alloc_bytes_total[5m]) + +# Garbage Collection (GC) Performance: +# go_gc_duration_seconds{quantile="0.99"} +# rate(go_gc_duration_seconds_count[5m]) + +# Process-Level System Reports +# CPU Usage: +# rate(process_cpu_seconds_total[5m]) + +# Resident Memory: +# process_resident_memory_bytes + +# File Descriptors: +# process_open_fds + +# Network I/O: +# rate(process_network_receive_bytes_total[5m]) +# rate(process_network_transmit_bytes_total[5m]) From c6aa200f572e7626c02f330849b68821239651e3 Mon Sep 17 00:00:00 2001 From: MD Towhidul Islam Date: Mon, 18 Aug 2025 22:58:59 +0600 Subject: [PATCH 17/21] Switch Keycloak and Prometheus to hostPath volumes --- deploy-kubernates.sh | 3 --- k8/config-maps.yaml | 29 +++++++++++++---------------- k8/keycloak.yaml | 7 ++++--- k8/prometheus.yaml | 7 ++++--- 4 files changed, 21 insertions(+), 25 deletions(-) diff --git a/deploy-kubernates.sh b/deploy-kubernates.sh index 8bb208f..e2ff9e9 100644 --- a/deploy-kubernates.sh +++ b/deploy-kubernates.sh @@ -58,9 +58,6 @@ else kubectl apply -f k8/config-maps.yaml fi -kubectl create configmap prometheus-config --from-file=prometheus.yml=prometheus/prometheus.yml -kubectl create configmap keycloak-import --from-file=gocloak-realm.json=keycloak/import/gocloak-realm.json - for file in k8/*.yaml; do if [ "$file" == "k8/secrets.yaml" ] || [ "$file" == "k8/config-maps.yaml" ]; then echo "Skipping $file as it's already processed." diff --git a/k8/config-maps.yaml b/k8/config-maps.yaml index 7fcb5d2..92306e3 100644 --- a/k8/config-maps.yaml +++ b/k8/config-maps.yaml @@ -3,24 +3,21 @@ kind: ConfigMap metadata: name: nms-config data: - SERVICES: "ssm,config" - POSTGRES_USER: "user" - POSTGRES_DB: "notification_management" - KC_HOSTNAME: "keycloak" - KEYCLOAK_USER: "admin" - KEYCLOAK_ADMIN: "admin" - KC_HEALTH_ENABLED: "true" - KC_FEATURES: "account-api,account3,authorization,client-policies,impersonation,docker,scripts,admin-fine-grained-authz" ALLOW_EMPTY_PASSWORD: "yes" - HTTPPATH: "/mails" - GF_SECURITY_ADMIN_USER: "admin" - LOG_LEVEL: "debug" - AWS_ENDPOINT: "http://config-server:4566" AWS_CONFIG_SERVICE_ENABLED: "false" - REDIS_HOST: "redis" - DB_HOST: "postgres" - DB_PORT: "5432" - KEYCLOAK_SERVER_URL: "http://keycloak:8080" + AWS_ENDPOINT: "http://config-server:4566" + GF_SECURITY_ADMIN_USER: "admin" GF_SERVER_ROOT_URL: "http://localhost:3000/grafana" GF_SERVER_SERVE_FROM_SUB_PATH: "true" + HTTPPATH: "/mails" + KC_FEATURES: "account-api,account3,authorization,client-policies,impersonation,docker,scripts,admin-fine-grained-authz" + KC_HEALTH_ENABLED: "true" + KC_HOSTNAME: "keycloak" KC_HTTP_RELATIVE_PATH: "/keycloak" + KEYCLOAK_ADMIN: "admin" + KEYCLOAK_SERVER_URL: "http://keycloak:8080" + KEYCLOAK_USER: "admin" + LOG_LEVEL: "debug" + POSTGRES_DB: "notification_management" + POSTGRES_USER: "user" + SERVICES: "ssm,config" diff --git a/k8/keycloak.yaml b/k8/keycloak.yaml index 68fc056..3ee64ee 100644 --- a/k8/keycloak.yaml +++ b/k8/keycloak.yaml @@ -41,11 +41,12 @@ spec: - containerPort: 9000 volumeMounts: - name: keycloak-import - mountPath: /opt/keycloak/data/import/ + mountPath: /opt/keycloak/data/import volumes: - name: keycloak-import - configMap: - name: keycloak-import + hostPath: + path: __PROJECT_BASE_PATH__/keycloak/import + type: DirectoryOrCreate --- apiVersion: v1 kind: Service diff --git a/k8/prometheus.yaml b/k8/prometheus.yaml index 24f5deb..8f909a8 100644 --- a/k8/prometheus.yaml +++ b/k8/prometheus.yaml @@ -21,7 +21,7 @@ spec: - containerPort: 9090 volumeMounts: - name: prometheus-config - mountPath: /etc/prometheus/ + mountPath: /etc/prometheus resources: requests: memory: "256Mi" @@ -38,8 +38,9 @@ spec: - --web.external-url=/prometheus volumes: - name: prometheus-config - configMap: - name: prometheus-config + hostPath: + path: __PROJECT_BASE_PATH__/prometheus + type: DirectoryOrCreate --- apiVersion: v1 kind: Service From 3b9fbae5881a2e58ac7a716d8e0ac2c11bff67ec Mon Sep 17 00:00:00 2001 From: MD Towhidul Islam Date: Tue, 19 Aug 2025 00:14:45 +0600 Subject: [PATCH 18/21] Refactor: Localize Kubernetes environment variables Move service-specific environment variables from the shared `nms-config` ConfigMap to the `env` section of their respective Kubernetes deployments. --- k8/config-maps.yaml | 19 +++---------------- k8/config-server.yaml | 14 +++++++++++--- k8/grafana.yaml | 9 ++++++--- k8/keycloak.yaml | 10 ++++++++++ k8/mailcatcher.yaml | 3 +++ k8/postgres.yaml | 4 ++++ k8/redis.yaml | 6 +++--- 7 files changed, 40 insertions(+), 25 deletions(-) diff --git a/k8/config-maps.yaml b/k8/config-maps.yaml index 92306e3..493f22e 100644 --- a/k8/config-maps.yaml +++ b/k8/config-maps.yaml @@ -3,21 +3,8 @@ kind: ConfigMap metadata: name: nms-config data: - ALLOW_EMPTY_PASSWORD: "yes" - AWS_CONFIG_SERVICE_ENABLED: "false" +# AWS_CONFIG_SERVICE_ENABLED: "false" AWS_ENDPOINT: "http://config-server:4566" - GF_SECURITY_ADMIN_USER: "admin" - GF_SERVER_ROOT_URL: "http://localhost:3000/grafana" - GF_SERVER_SERVE_FROM_SUB_PATH: "true" - HTTPPATH: "/mails" - KC_FEATURES: "account-api,account3,authorization,client-policies,impersonation,docker,scripts,admin-fine-grained-authz" - KC_HEALTH_ENABLED: "true" KC_HOSTNAME: "keycloak" - KC_HTTP_RELATIVE_PATH: "/keycloak" - KEYCLOAK_ADMIN: "admin" - KEYCLOAK_SERVER_URL: "http://keycloak:8080" - KEYCLOAK_USER: "admin" - LOG_LEVEL: "debug" - POSTGRES_DB: "notification_management" - POSTGRES_USER: "user" - SERVICES: "ssm,config" + KEYCLOAK_SERVER_URL: "http://keycloak:8080/keycloak" + LOG_LEVEL: "" diff --git a/k8/config-server.yaml b/k8/config-server.yaml index 574840b..6c7702b 100644 --- a/k8/config-server.yaml +++ b/k8/config-server.yaml @@ -19,9 +19,17 @@ spec: image: localstack/localstack:3.2.0 ports: - containerPort: 4566 - envFrom: - - configMapRef: - name: nms-config + env: + - name: SERVICES + value: "ssm,config" + - name: DEBUG + value: "1" + - name: PERSISTENCE + value: "1" + - name: DATA_DIR + value: "/tmp/localstack/data" + - name: LAMBDA_EXECUTOR + value: "docker" volumeMounts: - name: docker-sock mountPath: /var/run/docker.sock diff --git a/k8/grafana.yaml b/k8/grafana.yaml index 02aa382..0b40a3d 100644 --- a/k8/grafana.yaml +++ b/k8/grafana.yaml @@ -21,14 +21,17 @@ spec: ports: - containerPort: 3000 env: + - name: GF_SECURITY_ADMIN_USER + value: "admin" + - name: GF_SERVER_ROOT_URL + value: "http://localhost:3000/grafana" + - name: GF_SERVER_SERVE_FROM_SUB_PATH + value: "true" - name: GF_SECURITY_ADMIN_PASSWORD valueFrom: secretKeyRef: name: nms-secrets key: GF_SECURITY_ADMIN_PASSWORD - envFrom: - - configMapRef: - name: nms-config volumeMounts: - name: grafana-data mountPath: /var/lib/grafana diff --git a/k8/keycloak.yaml b/k8/keycloak.yaml index 3ee64ee..315bca3 100644 --- a/k8/keycloak.yaml +++ b/k8/keycloak.yaml @@ -33,6 +33,16 @@ spec: secretKeyRef: name: nms-secrets key: KEYCLOAK_ADMIN_PASSWORD + - name: KC_FEATURES + value: "account-api,account3,authorization,client-policies,impersonation,docker,scripts,admin-fine-grained-authz" + - name: KC_HEALTH_ENABLED + value: "true" + - name: KC_HTTP_RELATIVE_PATH + value: "/keycloak" + - name: KEYCLOAK_ADMIN + value: "admin" + - name: KEYCLOAK_USER + value: "admin" envFrom: - configMapRef: name: nms-config diff --git a/k8/mailcatcher.yaml b/k8/mailcatcher.yaml index f76fa50..cd7c950 100644 --- a/k8/mailcatcher.yaml +++ b/k8/mailcatcher.yaml @@ -20,6 +20,9 @@ spec: ports: - containerPort: 1080 - containerPort: 1025 + env: + - name: HTTPPATH + value: "/mails" envFrom: - configMapRef: name: nms-config diff --git a/k8/postgres.yaml b/k8/postgres.yaml index f24a837..c496cfd 100644 --- a/k8/postgres.yaml +++ b/k8/postgres.yaml @@ -26,6 +26,10 @@ spec: secretKeyRef: name: nms-secrets key: POSTGRES_PASSWORD + - name: POSTGRES_DB + value: "notification_management" + - name: POSTGRES_USER + value: "user" envFrom: - configMapRef: name: nms-config diff --git a/k8/redis.yaml b/k8/redis.yaml index ef71ea6..1e1a43b 100644 --- a/k8/redis.yaml +++ b/k8/redis.yaml @@ -20,9 +20,9 @@ spec: image: bitnami/redis:6.0.9 ports: - containerPort: 6379 - envFrom: - - configMapRef: - name: nms-config + env: + - name: ALLOW_EMPTY_PASSWORD + value: "yes" volumeMounts: - name: redis-data mountPath: /bitnami/redis/data From 084d3e76430382fafa26ef1b81b8b115037bc485 Mon Sep 17 00:00:00 2001 From: MD Towhidul Islam Date: Tue, 19 Aug 2025 00:37:31 +0600 Subject: [PATCH 19/21] refactor --- k8/config-maps.yaml | 1 + k8/grafana.yaml | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/k8/config-maps.yaml b/k8/config-maps.yaml index 493f22e..a44356f 100644 --- a/k8/config-maps.yaml +++ b/k8/config-maps.yaml @@ -8,3 +8,4 @@ data: KC_HOSTNAME: "keycloak" KEYCLOAK_SERVER_URL: "http://keycloak:8080/keycloak" LOG_LEVEL: "" + GF_SERVER_ROOT_URL: "http://localhost:3000/grafana" diff --git a/k8/grafana.yaml b/k8/grafana.yaml index 0b40a3d..033d505 100644 --- a/k8/grafana.yaml +++ b/k8/grafana.yaml @@ -23,8 +23,6 @@ spec: env: - name: GF_SECURITY_ADMIN_USER value: "admin" - - name: GF_SERVER_ROOT_URL - value: "http://localhost:3000/grafana" - name: GF_SERVER_SERVE_FROM_SUB_PATH value: "true" - name: GF_SECURITY_ADMIN_PASSWORD From 22b2abefe8047c96ea3edeed680a684e3f9018f0 Mon Sep 17 00:00:00 2001 From: MD Towhidul Islam Date: Tue, 19 Aug 2025 01:05:45 +0600 Subject: [PATCH 20/21] feat: Convert config-server and prometheus to StatefulSets --- deploy-kubernates.sh | 4 ++-- k8/config-maps.yaml | 10 +++++----- k8/config-server.yaml | 14 +++++++++++++- k8/grafana.yaml | 5 +++++ k8/prometheus.yaml | 13 ++++++++++++- 5 files changed, 37 insertions(+), 9 deletions(-) diff --git a/deploy-kubernates.sh b/deploy-kubernates.sh index e2ff9e9..b550a03 100644 --- a/deploy-kubernates.sh +++ b/deploy-kubernates.sh @@ -74,8 +74,8 @@ for file in k8/*.yaml; do done echo "All Kubernetes manifests in 'k8' directory processed with dynamic path: $PROJECT_BASE_PATH" -echo "Waiting for config-server deployment to be ready..." -kubectl wait --for=condition=available deployment/config-server --timeout=300s +echo "Waiting for config-server statefulset to be ready..." +kubectl rollout status --watch --timeout=300s statefulset/config-server CONFIG_SERVER_POD=$(kubectl get pods -l app=config-server -o jsonpath='{.items[0].metadata.name}') diff --git a/k8/config-maps.yaml b/k8/config-maps.yaml index a44356f..b3df376 100644 --- a/k8/config-maps.yaml +++ b/k8/config-maps.yaml @@ -3,9 +3,9 @@ kind: ConfigMap metadata: name: nms-config data: -# AWS_CONFIG_SERVICE_ENABLED: "false" - AWS_ENDPOINT: "http://config-server:4566" - KC_HOSTNAME: "keycloak" - KEYCLOAK_SERVER_URL: "http://keycloak:8080/keycloak" + AWS_CONFIG_SERVICE_ENABLED: "false" + KC_HOSTNAME: "hostname" + AWS_ENDPOINT: "http://hostname:4566" + KEYCLOAK_SERVER_URL: "http://hostname:8080/keycloak" LOG_LEVEL: "" - GF_SERVER_ROOT_URL: "http://localhost:3000/grafana" + GF_SERVER_ROOT_URL: "http://hostname:3000/grafana" diff --git a/k8/config-server.yaml b/k8/config-server.yaml index 6c7702b..83e5e79 100644 --- a/k8/config-server.yaml +++ b/k8/config-server.yaml @@ -1,10 +1,11 @@ apiVersion: apps/v1 -kind: Deployment +kind: StatefulSet metadata: name: config-server labels: app: config-server spec: + serviceName: "config-server" replicas: 1 selector: matchLabels: @@ -33,10 +34,21 @@ spec: volumeMounts: - name: docker-sock mountPath: /var/run/docker.sock + - name: config-data + mountPath: /var/lib/localstack volumes: - name: docker-sock hostPath: path: /var/run/docker.sock + volumeClaimTemplates: + - metadata: + name: config-data + spec: + accessModes: [ "ReadWriteOnce" ] + resources: + requests: + storage: 1Gi + --- apiVersion: v1 kind: Service diff --git a/k8/grafana.yaml b/k8/grafana.yaml index 033d505..afd53a6 100644 --- a/k8/grafana.yaml +++ b/k8/grafana.yaml @@ -23,6 +23,11 @@ spec: env: - name: GF_SECURITY_ADMIN_USER value: "admin" + - name: GF_SERVER_ROOT_URL + valueFrom: + configMapKeyRef: + name: nms-config + key: GF_SERVER_ROOT_URL - name: GF_SERVER_SERVE_FROM_SUB_PATH value: "true" - name: GF_SECURITY_ADMIN_PASSWORD diff --git a/k8/prometheus.yaml b/k8/prometheus.yaml index 8f909a8..3f39d0c 100644 --- a/k8/prometheus.yaml +++ b/k8/prometheus.yaml @@ -1,10 +1,11 @@ apiVersion: apps/v1 -kind: Deployment +kind: StatefulSet metadata: name: prometheus labels: app: prometheus spec: + serviceName: "prometheus" replicas: 1 selector: matchLabels: @@ -22,6 +23,8 @@ spec: volumeMounts: - name: prometheus-config mountPath: /etc/prometheus + - name: prometheus-data + mountPath: /prometheus resources: requests: memory: "256Mi" @@ -41,6 +44,14 @@ spec: hostPath: path: __PROJECT_BASE_PATH__/prometheus type: DirectoryOrCreate + volumeClaimTemplates: + - metadata: + name: prometheus-data + spec: + accessModes: [ "ReadWriteOnce" ] + resources: + requests: + storage: 2Gi --- apiVersion: v1 kind: Service From 899865e2cd1e8c39e0d874b4d7ab99e0d9d9f18a Mon Sep 17 00:00:00 2001 From: MD Towhidul Islam Date: Tue, 19 Aug 2025 19:57:29 +0600 Subject: [PATCH 21/21] Some refactor --- server/server.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/server/server.go b/server/server.go index 9b7b73a..3fdec1d 100644 --- a/server/server.go +++ b/server/server.go @@ -12,13 +12,12 @@ import ( "NotificationManagement/services/notifier" "github.com/labstack/echo/v4" "go.uber.org/fx" - "gorm.io/gorm" // Import gorm + "gorm.io/gorm" ) func NewEcho(db *gorm.DB) *echo.Echo { e := echo.New() e.Use(interceptLogger) - //e.Use(errutil.ErrorHandler()) e.Use(middleware.TransactionMiddleware(db)) return e }