Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
349 changes: 220 additions & 129 deletions Readme.md

Large diffs are not rendered by default.

23 changes: 23 additions & 0 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
FROM golang:1.24.10-alpine AS builder

WORKDIR /app

COPY go.mod go.sum ./
RUN go mod download && go mod verify

COPY . .

# Build both migrator and app binaries
RUN go build -o migrator ./cmd/migrator/main.go
RUN go build -o app ./cmd/app/main.go

FROM alpine:latest

WORKDIR /app

# Copy both binaries
COPY --from=builder /app/migrator .
COPY --from=builder /app/app .

# Default to running the app
CMD ["./app"]
10 changes: 9 additions & 1 deletion backend/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,12 @@ lint:
.PHONY: fmt
format:
@gofmt -w .
@gofumpt -w .
@gofumpt -w .

.PHONY: run-migrator
run-migrator:
@docker compose run migrator

.PHONY: run-app
run-app:
@docker compose run app
60 changes: 36 additions & 24 deletions backend/main.go → backend/cmd/app/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,27 +35,37 @@ func main() {
appLogger.Fatal("Failed to setup OpenTelemetry", zap.Error(err))
}
defer func() {
if err := shutdownOTel(ctx); err != nil {
appLogger.Error("Failed to shutdown OpenTelemetry", zap.Error(err))
if shutdownErr := shutdownOTel(ctx); shutdownErr != nil {
appLogger.Error("Failed to shutdown OpenTelemetry", zap.Error(shutdownErr))
}
}()

session := setupDatabase(cfg, appLogger)
session, err := setupDatabase(cfg, appLogger)
if err != nil {
appLogger.Fatal("Failed to setup database", zap.Error(err))
}
defer session.Close()

redisClient := setupRedis(ctx, cfg, appLogger)
redisClient, err := setupRedis(ctx, cfg, appLogger)
if err != nil {
appLogger.Fatal("Failed to setup Redis", zap.Error(err))
}

defer func() {
err := redisClient.Close()
if err != nil {
appLogger.Error("Failed to close Redis client", zap.Error(err))
if closeErr := redisClient.Close(); closeErr != nil {
appLogger.Error("Failed to close Redis client", zap.Error(closeErr))
}
}()

initializeCounter(ctx, redisClient, cfg, appLogger)

err = initializeCounter(ctx, redisClient, cfg, appLogger)
if err != nil {
appLogger.Fatal("Failed to initialize counter", zap.Error(err))
}
useCase := createUseCase(cfg, appLogger, session, redisClient)
server := createAndStartServer(cfg, appLogger, useCase)
server, err := createAndStartServer(cfg, appLogger, useCase)
if err != nil {
appLogger.Fatal("Failed to create and start server", zap.Error(err))
}

shutdownServer(ctx, appLogger, server)
}
Expand Down Expand Up @@ -84,29 +94,31 @@ func setupConfigAndLogger() (*config.Config, *zap.Logger) {
return cfg, appLogger
}

func setupDatabase(cfg *config.Config, appLogger *zap.Logger) *gocql.Session {
session, err := gocqlPackage.SetupDatabase(&cfg.Gocql, appLogger)
func setupDatabase(cfg *config.Config, appLogger *zap.Logger) (*gocql.Session, error) {
session, err := gocqlPackage.SetupDatabase(&cfg.Gocql, appLogger, false)
if err != nil {
appLogger.Fatal("Failed to setup database", zap.Error(err))
return nil, fmt.Errorf("failed to setup database: %w", err)
}

return session
return session, nil
}

func setupRedis(ctx context.Context, cfg *config.Config, appLogger *zap.Logger) *redis.Client {
func setupRedis(ctx context.Context, cfg *config.Config, appLogger *zap.Logger) (*redis.Client, error) {
redisClient, err := redisPackage.SetupRedis(ctx, &cfg.Redis, appLogger)
if err != nil {
appLogger.Fatal("Failed to setup Redis", zap.Error(err))
return nil, fmt.Errorf("failed to setup Redis: %w", err)
}

return redisClient
return redisClient, nil
}

func initializeCounter(ctx context.Context, redisClient *redis.Client, cfg *config.Config, appLogger *zap.Logger) {
setInitialCounter, err := redisPackage.SetInitialCounterValue(ctx, redisClient, &cfg.Redis, appLogger)
if err != nil && !setInitialCounter {
appLogger.Fatal("Failed to set initial counter", zap.Error(err))
func initializeCounter(ctx context.Context, redisClient *redis.Client, cfg *config.Config, appLogger *zap.Logger) error {
_, err := redisPackage.SetInitialCounterValue(ctx, redisClient, &cfg.Redis, appLogger)
if err != nil {
return fmt.Errorf("failed to set initial counter: %w", err)
}

return nil
}

func createUseCase(cfg *config.Config, appLogger *zap.Logger, session *gocql.Session, redisClient *redis.Client) *usecases.UseCase {
Expand All @@ -122,7 +134,7 @@ func createUseCase(cfg *config.Config, appLogger *zap.Logger, session *gocql.Ses
})
}

func createAndStartServer(cfg *config.Config, appLogger *zap.Logger, useCase *usecases.UseCase) *httpServer.Server {
func createAndStartServer(cfg *config.Config, appLogger *zap.Logger, useCase *usecases.UseCase) (*httpServer.Server, error) {
httpHandlers := handlers.NewHandlers(appLogger, useCase)

router := httpServer.NewRouter(httpServer.RouterConfig{
Expand All @@ -136,10 +148,10 @@ func createAndStartServer(cfg *config.Config, appLogger *zap.Logger, useCase *us

err := server.Start()
if err != nil {
appLogger.Fatal("Failed to start HTTP server", zap.Error(err))
return nil, fmt.Errorf("failed to start HTTP server: %w", err)
}

return server
return server, nil
}

func shutdownServer(ctx context.Context, appLogger *zap.Logger, server *httpServer.Server) {
Expand Down
51 changes: 51 additions & 0 deletions backend/cmd/migrator/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package main

import (
"fmt"
"log"

"lnk/extensions/config"
"lnk/extensions/logger"
gocqlPackage "lnk/gateways/gocql"

gocql "github.com/apache/cassandra-gocql-driver/v2"
"go.uber.org/zap"
)

func main() {
cfg, appLogger := setupConfigAndLogger()

session, err := setupDatabase(cfg, appLogger)
if err != nil {
log.Fatalf("Failed to setup database: %v", err)
}

defer session.Close()

appLogger.Info("Migrations completed successfully")
}

func setupConfigAndLogger() (*config.Config, *zap.Logger) {
cfg, err := config.LoadConfig()
if err != nil {
log.Fatalf("Failed to load config: %v", err)
}

appLogger, err := logger.NewLogger(cfg.Logger)
if err != nil {
log.Fatalf("Failed to create logger: %v", err)
}

appLogger.Info("Starting application")

return cfg, appLogger
}

func setupDatabase(cfg *config.Config, appLogger *zap.Logger) (*gocql.Session, error) {
session, err := gocqlPackage.SetupDatabase(&cfg.Gocql, appLogger, cfg.Gocql.AutoMigrate)
if err != nil {
return nil, fmt.Errorf("failed to setup database: %w", err)
}

return session, nil
}
108 changes: 97 additions & 11 deletions backend/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,23 +1,103 @@
services:
redis:
image: redis:latest
container_name: lnk-redis
nginx:
image: nginx:alpine
container_name: lnk-nginx
ports:
- "6379:6379"
- "8888:80"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
- app
networks:
- lnk-network

cassandra:
image: cassandra:latest
container_name: lnk-cassandra
migrator:
build: .
container_name: lnk-migrator
command: ["./migrator"]
volumes:
- ./.env:/app/.env:ro
networks:
- lnk-network
depends_on:
cassandra-lb:
condition: service_healthy
env_file:
- .env
restart: "no"

app:
build: .
volumes:
- ./.env:/app/.env:ro
networks:
- lnk-network
depends_on:
migrator:
condition: service_completed_successfully
cassandra-lb:
condition: service_healthy
redis:
condition: service_started
deploy:
replicas: 4
env_file:
- .env

cassandra-lb:
image: haproxy:2.8-alpine
container_name: lnk-cassandra-lb
command: ["haproxy", "-f", "/usr/local/etc/haproxy/haproxy.cfg"]
ports:
- "9042:9042"
- "8404:8404"
volumes:
- ./haproxy/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro
depends_on:
cassandra:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "pgrep haproxy > /dev/null"]
interval: 5s
timeout: 3s
retries: 10
start_period: 10s
networks:
- lnk-network

cassandra:
image: cassandra:latest
environment:
- CASSANDRA_CLUSTER_NAME=lnk-cluster
- CASSANDRA_DC=datacenter1
- CASSANDRA_RACK=rack1
- CASSANDRA_ENDPOINT_SNITCH=GossipingPropertyFileSnitch
- CASSANDRA_AUTHENTICATOR=PasswordAuthenticator
- CASSANDRA_AUTHORIZER=CassandraAuthorizer
- CASSANDRA_USER=cassandra
- CASSANDRA_PASSWORD=cassandra
volumes:
- cassandra-data:/var/lib/cassandra
healthcheck:
test: ["CMD-SHELL", "nodetool status | grep -E '^UN'"]
interval: 10s
timeout: 5s
retries: 30
start_period: 60s
networks:
- lnk-network

redis:
image: redis:latest
container_name: lnk-redis
ports:
- "6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
networks:
- lnk-network

grafana:
image: grafana/otel-lgtm:latest
Expand All @@ -27,6 +107,12 @@ services:
- "4317:4317"
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
depends_on:
- cassandra
- redis
networks:
- lnk-network

volumes:
cassandra-data:

networks:
lnk-network:
driver: bridge
7 changes: 4 additions & 3 deletions backend/gateways/gocql/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@ import (
"strconv"
"time"

"lnk/gateways/gocql/migrations"

gocql "github.com/apache/cassandra-gocql-driver/v2"
"github.com/golang-migrate/migrate/v4"
_ "github.com/golang-migrate/migrate/v4/database/cassandra"
"github.com/golang-migrate/migrate/v4/source/iofs"
"go.uber.org/zap"
"lnk/gateways/gocql/migrations"
)

const (
Expand All @@ -21,7 +22,7 @@ const (
shutdownTimeout = 5 * time.Second
)

func SetupDatabase(config *Config, logger *zap.Logger) (*gocql.Session, error) {
func SetupDatabase(config *Config, logger *zap.Logger, autoMigrate bool) (*gocql.Session, error) {
cluster := gocql.NewCluster(config.Host)
cluster.Port = config.Port
cluster.Authenticator = gocql.PasswordAuthenticator{
Expand Down Expand Up @@ -57,7 +58,7 @@ func SetupDatabase(config *Config, logger *zap.Logger) (*gocql.Session, error) {
session.Close()
session = sessionWithKeyspace

if config.AutoMigrate {
if autoMigrate {
err := runMigrations(config, logger)
if err != nil {
return nil, fmt.Errorf("failed to run migrations: %w", err)
Expand Down
1 change: 1 addition & 0 deletions backend/gateways/http/middleware/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ func Recovery(logger *zap.Logger) gin.HandlerFunc {
func CORS() gin.HandlerFunc {
return func(c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")

c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE, PATCH")
Expand Down
Loading
Loading