diff --git a/services/entry/cmd/app/main.go b/services/entry/cmd/app/main.go new file mode 100644 index 0000000..b5c639b --- /dev/null +++ b/services/entry/cmd/app/main.go @@ -0,0 +1,19 @@ +package app + +import ( + "serverctl/pkg/api" + "serverctl/pkg/infrastructure" + "serverctl/pkg/infrastructure/dependencies" +) + +func Run() { + d := dependencies.New() + d.Logger.Info("Starting serverctl") + + infrastructure.AddSeedData(d.Database, d.Logger) + + api. + NewServerctlApi(d). + SetupApi(). + RunApi() +} diff --git a/services/entry/main.go b/services/entry/main.go index 647b572..c274f9b 100644 --- a/services/entry/main.go +++ b/services/entry/main.go @@ -1,363 +1,7 @@ package main -import ( - "context" - "fmt" - "github.com/dgraph-io/ristretto" - "github.com/docker/docker/api/types" - "github.com/docker/docker/client" - "github.com/eko/gocache/cache" - "github.com/eko/gocache/metrics" - "github.com/eko/gocache/store" - "github.com/gin-gonic/gin" - "github.com/go-co-op/gocron" - "github.com/google/uuid" - "github.com/jackc/pgx/v4" - "github.com/prometheus/client_golang/prometheus/promhttp" - "go.uber.org/zap" - "go.uber.org/zap/zapcore" - "io/ioutil" - "log" - "net/http" - "os" - "serverctl/pkg/application/applications" - "serverctl/pkg/application/projects" - "serverctl/pkg/application/users" - "serverctl/pkg/db" - "serverctl/pkg/db/postgres" - "time" -) - -import _ "net/http/pprof" - -func setupLogger() *zap.Logger { - highPriority := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool { - return lvl >= zapcore.ErrorLevel - }) - lowPriority := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool { - return lvl < zapcore.ErrorLevel - }) - fileDebugging := zapcore.AddSync(ioutil.Discard) - fileErrors := zapcore.AddSync(ioutil.Discard) - - consoleDebugging := zapcore.Lock(os.Stdout) - consoleErrors := zapcore.Lock(os.Stderr) - - fileEncoder := zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()) - _ = zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig()) - - core := zapcore.NewTee( - zapcore.NewCore(fileEncoder, fileErrors, highPriority), - zapcore.NewCore(fileEncoder, consoleErrors, highPriority), - zapcore.NewCore(fileEncoder, fileDebugging, lowPriority), - zapcore.NewCore(fileEncoder, consoleDebugging, lowPriority), - ) - - logger := zap.New(core) - defer logger.Sync() - return logger -} -func BasicAuthMiddleware(l *zap.Logger, us *users.Service) gin.HandlerFunc { - return func(c *gin.Context) { - username, password, hasAuth := c.Request.BasicAuth() - if !hasAuth { - l.Info("user could not be authenticated", - zap.String("username", username)) - c.Header("WWW-Authenticate", "Basic realm=serverctl") - c.Abort() - c.JSON(http.StatusUnauthorized, gin.H{"message": "credentials were invalid (authorization header missing)"}) - return - } - - user, err := us.Authenticate(c.Request.Context(), username, password) - if err != nil { - l.Info("user could not be authenticated", - zap.String("username", username)) - c.Abort() - c.Header("WWW-Authenticate", "Basic realm=serverctl") - c.JSON(http.StatusUnauthorized, gin.H{"message": "credentials were invalid (credentials didn't match)"}) - return - } - - l.Debug("user has been authenticated", - zap.Int("userId", user.Id), - zap.String("email", user.Email)) - c.Set("userId", user.Id) - c.Next() - } -} -func setupApi(l *zap.Logger, cc *cache.MetricCache, us *users.Service, ps *projects.Service, as *applications.Service) { - l.Info("Setting up serverctl setupApi (using gin)") - - r := gin.Default() - - promHandler := func() gin.HandlerFunc { - h := promhttp.Handler() - - return func(c *gin.Context) { - h.ServeHTTP(c.Writer, c.Request) - } - } - r.GET("/metrics", promHandler()) - - r.POST("/auth/register", func(c *gin.Context) { - type RegisterUser struct { - Email string `json:"email" binding:"required"` - Password string `json:"password" binding:"required"` - } - var registerUser RegisterUser - if err := c.BindJSON(®isterUser); err != nil { - return - } - - createUser, err := us.Create(registerUser.Email, registerUser.Password) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"message": "you have provided invalid input"}) - return - } - - c.JSON(http.StatusCreated, gin.H{"message": "user has been registered", "userId": createUser}) - }) - - projectsApi := r.Group("/projects", BasicAuthMiddleware(l, us)) - projectsApi.POST("/", func(c *gin.Context) { - type CreateProjectRequest struct { - Name string `json:"name" binding:"required"` - } - var createProjectRequest CreateProjectRequest - if err := c.BindJSON(&createProjectRequest); err != nil { - return - } - - userId, _ := c.Get("userId") - createProjectId, err := ps.CreateProject(c.Request.Context(), userId.(int), createProjectRequest.Name) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"message": "you have provided invalid input"}) - return - } - - c.JSON(http.StatusCreated, gin.H{"message": "project has been created", "projectId": createProjectId}) - }) - projectsApi.GET("/", func(c *gin.Context) { - userId, _ := c.Get("userId") - - projectsArr, err := ps.Get(c.Request.Context(), userId.(int)) - if err != nil { - l.Warn(err.Error()) - return - } - - type GetProjectMembers struct { - MemberId int `json:"memberId" binding:"required"` - MemberRole string `json:"memberRole" binding:"required"` - } - type GetProject struct { - Id int `json:"id" binding:"required"` - Name string `json:"name" binding:"required"` - Members []*GetProjectMembers `json:"members" binding:"required"` - } - - membersAsGetProjectMembers := func(projectMembers []projects.ProjectMember) []*GetProjectMembers { - gpm := make([]*GetProjectMembers, len(projectMembers)) - - for i, pm := range projectMembers { - gpm[i] = &GetProjectMembers{ - MemberId: pm.MemberId, - MemberRole: pm.Role, - } - } - - return gpm - } - - getProject := make([]GetProject, 0) - for _, p := range projectsArr { - getProject = append(getProject, GetProject{ - Id: p.Id, - Name: p.Name, - Members: membersAsGetProjectMembers(p.Members), - }) - } - c.JSON(http.StatusOK, getProject) - }) - - applications := r.Group("/applications", BasicAuthMiddleware(l, us)) - applications.POST("/", func(c *gin.Context) { - type CreateApplicationRequest struct { - ProjectId int `json:"projectId" binding:"required"` - Name string `json:"name" binding:"required"` - } - - var createApplicationRequest CreateApplicationRequest - if err := c.BindJSON(&createApplicationRequest); err != nil { - return - } - - userId, _ := c.Get("userId") - applicationId, err := as.CreateApplication(c.Request.Context(), createApplicationRequest.Name, userId.(int), createApplicationRequest.ProjectId) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"message": "you have provided invalid input"}) - return - } - - c.JSON(http.StatusCreated, gin.H{"message": "application has been created", "applicationId": applicationId}) - }) - - containers := r.Group("/containers", BasicAuthMiddleware(l, us)) - containers.GET("/", func(c *gin.Context) { - type container struct { - Name string `json:"name"` - } - var msg struct { - Containers []container `json:"containers"` - } - - get, err := cc.Get("docker-containers") - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"message": "could not get containers from container runtime"}) - return - } - - msg.Containers = []container{} - for _, cont := range get.([]types.Container) { - msg.Containers = append(msg.Containers, container{ - Name: cont.Names[0], - }) - } - - c.JSON(http.StatusOK, msg) - }) - - r.Run(":8080") - -} -func setupDocker(l *zap.Logger) *client.Client { - l.Info("Setting up Docker") - cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) - if err != nil { - panic(err) - } - - return cli -} -func setupCache(l *zap.Logger) *cache.MetricCache { - l.Info("Setting up cache") - ristrettoCache, err := ristretto.NewCache(&ristretto.Config{ - NumCounters: 1000, - MaxCost: 100_000_000, - BufferItems: 64, - }) - promMetrics := metrics.NewPrometheus("serverctl") - - if err != nil { - panic(err) - } - ristrettoStore := store.NewRistretto(ristrettoCache, nil) - - cacheManager := cache.New(ristrettoStore) - metricsCache := cache.NewMetric(promMetrics, cacheManager) - - return metricsCache -} -func setupCron(l *zap.Logger, cm *cache.MetricCache, cc *client.Client) { - l.Info("Setting up job scheduler (cron)") - - s := gocron.NewScheduler(time.UTC) - - s.Every(10).Second().Do(func() { - l.Debug("getting container list") - list, err := cc.ContainerList(context.Background(), types.ContainerListOptions{}) - if err != nil { - l.Warn(err.Error()) - return - } - - err = cm.Set("docker-containers", list, &store.Options{ - Cost: 2, - }) - if err != nil { - l.Warn(err.Error()) - return - } - }) - - s.StartAsync() -} +import "serverctl/cmd/app" func main() { - logger := setupLogger() - logger.Info("Starting serverctl") - - cacheM := setupCache(logger) - containerClient := setupDocker(logger) - setupCron(logger, cacheM, containerClient) - - database := db.NewClient(logger) - usersRepository := postgres.NewUsersRepository(database) - usersService := users.NewService(logger, usersRepository, cacheM) - projectsRepository := postgres.NewProjectsRepository(database) - projectsService := projects.NewService(logger, projectsRepository, cacheM) - applicationsRepository := postgres.NewApplicationRepository(logger, database) - applicationsService := applications.NewService(logger, applicationsRepository) - - setupProfiler() - addSeedData(database, usersRepository, projectsRepository, logger) - setupApi(logger, cacheM, usersService, projectsService, applicationsService) -} - -func setupProfiler() { - go func() { - log.Println(http.ListenAndServe(":6060", nil)) - }() -} - -func addSeedData(database *db.Client, ur users.Repository, pr projects.Repository, logger *zap.Logger) { - conn := database.GetConn(context.Background()) - defer conn.Release() - - var numRows int - err := conn.QueryRow(context.Background(), "select count(id) from sctl_user").Scan(&numRows) - if err != nil { - panic(err) - } - - if numRows == 0 { - addTestData(database, ur, pr, logger) - } -} - -func addTestData(database *db.Client, ur users.Repository, pr projects.Repository, logger *zap.Logger) { - ctx := context.Background() - - for jobs := 0; jobs < 10; jobs++ { - go func(batchNr int) { - conn := database.GetConn(ctx) - defer conn.Release() - batch := &pgx.Batch{} - numInserts := 5_000 - for i := 0; i < numInserts; i++ { - var ( - user *users.CreateUser - err error - ) - - user, err = users.NewCreateUser(fmt.Sprintf("%s@test.com", uuid.New().String()), "password", users.NewPlainTextPasswordHasher()) - if err != nil { - panic(err) - } - - batch.Queue("INSERT INTO sctl_user(email, password_hash) values ($1, $2)", user.Email, user.PasswordHash) - } - res := conn.SendBatch(ctx, batch) - for i := 0; i < numInserts; i++ { - _, err := res.Exec() - if err != nil { - return - } - } - - logger.Debug("sent batch", - zap.Int("batchId", batchNr)) - }(jobs) - } + app.Run() } diff --git a/services/entry/pkg/api/api.go b/services/entry/pkg/api/api.go new file mode 100644 index 0000000..60b53a4 --- /dev/null +++ b/services/entry/pkg/api/api.go @@ -0,0 +1,58 @@ +package api + +import ( + "github.com/gin-gonic/gin" + "go.uber.org/zap" + "log" + "net/http" + "serverctl/pkg/api/routers" + "serverctl/pkg/infrastructure/dependencies" +) + +// Used for profiling +import _ "net/http/pprof" + +type ServerctlApi struct { + logger *zap.Logger + router *gin.Engine + routingTable *routers.RoutingTable + dependencies *dependencies.Dependencies +} + +func NewServerctlApi(dependencies *dependencies.Dependencies) *ServerctlApi { + return &ServerctlApi{dependencies: dependencies} +} + +func (a *ServerctlApi) SetupApi() *ServerctlApi { + a.dependencies.Logger.Info("Setting up serverctl setupApi (using gin)") + a.router = gin.Default() + + a.setupCommonMiddleware().setupRoutingTable() + + return a +} + +func (a *ServerctlApi) RunApi() { + runProfilerHttpServer() + + err := a.router.Run(":8080") + if err != nil { + panic(err) + } +} + +func (a *ServerctlApi) setupCommonMiddleware() *ServerctlApi { + return a +} + +func (a *ServerctlApi) setupRoutingTable() { + a.routingTable = routers. + NewRoutingTable(a.router, a.dependencies). + Setup() +} + +func runProfilerHttpServer() { + go func() { + log.Println(http.ListenAndServe(":6060", nil)) + }() +} diff --git a/services/entry/pkg/api/middleware/auth.go b/services/entry/pkg/api/middleware/auth.go new file mode 100644 index 0000000..5852402 --- /dev/null +++ b/services/entry/pkg/api/middleware/auth.go @@ -0,0 +1,38 @@ +package middleware + +import ( + "github.com/gin-gonic/gin" + "go.uber.org/zap" + "net/http" + "serverctl/pkg/application/users" +) + +func BasicAuthMiddleware(l *zap.Logger, us *users.Service) gin.HandlerFunc { + return func(c *gin.Context) { + username, password, hasAuth := c.Request.BasicAuth() + if !hasAuth { + l.Info("user could not be authenticated", + zap.String("username", username)) + c.Header("WWW-Authenticate", "Basic realm=serverctl") + c.Abort() + c.JSON(http.StatusUnauthorized, gin.H{"message": "credentials were invalid (authorization header missing)"}) + return + } + + user, err := us.Authenticate(c.Request.Context(), username, password) + if err != nil { + l.Info("user could not be authenticated", + zap.String("username", username)) + c.Abort() + c.Header("WWW-Authenticate", "Basic realm=serverctl") + c.JSON(http.StatusUnauthorized, gin.H{"message": "credentials were invalid (credentials didn't match)"}) + return + } + + l.Debug("user has been authenticated", + zap.Int("userId", user.Id), + zap.String("email", user.Email)) + c.Set("userId", user.Id) + c.Next() + } +} diff --git a/services/entry/pkg/api/routers/applications.go b/services/entry/pkg/api/routers/applications.go new file mode 100644 index 0000000..4b001ba --- /dev/null +++ b/services/entry/pkg/api/routers/applications.go @@ -0,0 +1,32 @@ +package routers + +import ( + "github.com/gin-gonic/gin" + "net/http" + "serverctl/pkg/api/middleware" + "serverctl/pkg/infrastructure/dependencies" +) + +func applicationsRouter(router *gin.Engine, d *dependencies.Dependencies) { + applications := router.Group("/applications", middleware.BasicAuthMiddleware(d.Logger, d.UsersService)) + applications.POST("/", func(c *gin.Context) { + type CreateApplicationRequest struct { + ProjectId int `json:"projectId" binding:"required"` + Name string `json:"name" binding:"required"` + } + + var createApplicationRequest CreateApplicationRequest + if err := c.BindJSON(&createApplicationRequest); err != nil { + return + } + + userId, _ := c.Get("userId") + applicationId, err := d.ApplicationsService.CreateApplication(c.Request.Context(), createApplicationRequest.Name, userId.(int), createApplicationRequest.ProjectId) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"message": "you have provided invalid input"}) + return + } + + c.JSON(http.StatusCreated, gin.H{"message": "application has been created", "applicationId": applicationId}) + }) +} diff --git a/services/entry/pkg/api/routers/auth.go b/services/entry/pkg/api/routers/auth.go new file mode 100644 index 0000000..3143b8b --- /dev/null +++ b/services/entry/pkg/api/routers/auth.go @@ -0,0 +1,28 @@ +package routers + +import ( + "github.com/gin-gonic/gin" + "net/http" + "serverctl/pkg/infrastructure/dependencies" +) + +func authRouter(router *gin.Engine, d *dependencies.Dependencies) { + router.POST("/auth/register", func(c *gin.Context) { + type RegisterUser struct { + Email string `json:"email" binding:"required"` + Password string `json:"password" binding:"required"` + } + var registerUser RegisterUser + if err := c.BindJSON(®isterUser); err != nil { + return + } + + createUser, err := d.UsersService.Create(registerUser.Email, registerUser.Password) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"message": "you have provided invalid input"}) + return + } + + c.JSON(http.StatusCreated, gin.H{"message": "user has been registered", "userId": createUser}) + }) +} diff --git a/services/entry/pkg/api/routers/containers.go b/services/entry/pkg/api/routers/containers.go new file mode 100644 index 0000000..9255bea --- /dev/null +++ b/services/entry/pkg/api/routers/containers.go @@ -0,0 +1,36 @@ +package routers + +import ( + "github.com/docker/docker/api/types" + "github.com/gin-gonic/gin" + "net/http" + "serverctl/pkg/api/middleware" + "serverctl/pkg/infrastructure/dependencies" +) + +func containersRouter(router *gin.Engine, d *dependencies.Dependencies) { + containers := router.Group("/containers", middleware.BasicAuthMiddleware(d.Logger, d.UsersService)) + containers.GET("/", func(c *gin.Context) { + type container struct { + Name string `json:"name"` + } + var msg struct { + Containers []container `json:"containers"` + } + + get, err := d.Cache.Get("docker-containers") + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"message": "could not get containers from container runtime"}) + return + } + + msg.Containers = []container{} + for _, cont := range get.([]types.Container) { + msg.Containers = append(msg.Containers, container{ + Name: cont.Names[0], + }) + } + + c.JSON(http.StatusOK, msg) + }) +} diff --git a/services/entry/pkg/api/routers/lib.go b/services/entry/pkg/api/routers/lib.go new file mode 100644 index 0000000..5932e64 --- /dev/null +++ b/services/entry/pkg/api/routers/lib.go @@ -0,0 +1,25 @@ +package routers + +import ( + "github.com/gin-gonic/gin" + "serverctl/pkg/infrastructure/dependencies" +) + +type RoutingTable struct { + router *gin.Engine + dependencies *dependencies.Dependencies +} + +func NewRoutingTable(router *gin.Engine, dependencies *dependencies.Dependencies) *RoutingTable { + return &RoutingTable{router: router, dependencies: dependencies} +} + +func (t *RoutingTable) Setup() *RoutingTable { + metricsRouter(t.router) + authRouter(t.router, t.dependencies) + projectsRouter(t.router, t.dependencies) + applicationsRouter(t.router, t.dependencies) + containersRouter(t.router, t.dependencies) + + return t +} diff --git a/services/entry/pkg/api/routers/metrics.go b/services/entry/pkg/api/routers/metrics.go new file mode 100644 index 0000000..4defcbf --- /dev/null +++ b/services/entry/pkg/api/routers/metrics.go @@ -0,0 +1,18 @@ +package routers + +import ( + "github.com/gin-gonic/gin" + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +func metrics() gin.HandlerFunc { + h := promhttp.Handler() + + return func(c *gin.Context) { + h.ServeHTTP(c.Writer, c.Request) + } +} + +func metricsRouter(router *gin.Engine) { + router.GET("/metrics", metrics()) +} diff --git a/services/entry/pkg/api/routers/projects.go b/services/entry/pkg/api/routers/projects.go new file mode 100644 index 0000000..7ea9c2c --- /dev/null +++ b/services/entry/pkg/api/routers/projects.go @@ -0,0 +1,73 @@ +package routers + +import ( + "github.com/gin-gonic/gin" + "net/http" + "serverctl/pkg/api/middleware" + "serverctl/pkg/application/projects" + "serverctl/pkg/infrastructure/dependencies" +) + +func projectsRouter(router *gin.Engine, d *dependencies.Dependencies) { + projectsApi := router.Group("/projects", middleware.BasicAuthMiddleware(d.Logger, d.UsersService)) + projectsApi.POST("/", func(c *gin.Context) { + type CreateProjectRequest struct { + Name string `json:"name" binding:"required"` + } + var createProjectRequest CreateProjectRequest + if err := c.BindJSON(&createProjectRequest); err != nil { + return + } + + userId, _ := c.Get("userId") + createProjectId, err := d.ProjectsService.CreateProject(c.Request.Context(), userId.(int), createProjectRequest.Name) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"message": "you have provided invalid input"}) + return + } + + c.JSON(http.StatusCreated, gin.H{"message": "project has been created", "projectId": createProjectId}) + }) + projectsApi.GET("/", func(c *gin.Context) { + userId, _ := c.Get("userId") + + projectsArr, err := d.ProjectsService.Get(c.Request.Context(), userId.(int)) + if err != nil { + d.Logger.Warn(err.Error()) + return + } + + type GetProjectMembers struct { + MemberId int `json:"memberId" binding:"required"` + MemberRole string `json:"memberRole" binding:"required"` + } + type GetProject struct { + Id int `json:"id" binding:"required"` + Name string `json:"name" binding:"required"` + Members []*GetProjectMembers `json:"members" binding:"required"` + } + + membersAsGetProjectMembers := func(projectMembers []projects.ProjectMember) []*GetProjectMembers { + gpm := make([]*GetProjectMembers, len(projectMembers)) + + for i, pm := range projectMembers { + gpm[i] = &GetProjectMembers{ + MemberId: pm.MemberId, + MemberRole: pm.Role, + } + } + + return gpm + } + + getProject := make([]GetProject, 0) + for _, p := range projectsArr { + getProject = append(getProject, GetProject{ + Id: p.Id, + Name: p.Name, + Members: membersAsGetProjectMembers(p.Members), + }) + } + c.JSON(http.StatusOK, getProject) + }) +} diff --git a/services/entry/pkg/infrastructure/dependencies/cache.go b/services/entry/pkg/infrastructure/dependencies/cache.go new file mode 100644 index 0000000..f1c100f --- /dev/null +++ b/services/entry/pkg/infrastructure/dependencies/cache.go @@ -0,0 +1,29 @@ +package dependencies + +import ( + "github.com/dgraph-io/ristretto" + "github.com/eko/gocache/cache" + "github.com/eko/gocache/metrics" + "github.com/eko/gocache/store" + "go.uber.org/zap" +) + +func setupCache(l *zap.Logger) *cache.MetricCache { + l.Info("Setting up Cache") + ristrettoCache, err := ristretto.NewCache(&ristretto.Config{ + NumCounters: 1000, + MaxCost: 100_000_000, + BufferItems: 64, + }) + promMetrics := metrics.NewPrometheus("serverctl") + + if err != nil { + panic(err) + } + ristrettoStore := store.NewRistretto(ristrettoCache, nil) + + cacheManager := cache.New(ristrettoStore) + metricsCache := cache.NewMetric(promMetrics, cacheManager) + + return metricsCache +} diff --git a/services/entry/pkg/infrastructure/dependencies/cron.go b/services/entry/pkg/infrastructure/dependencies/cron.go new file mode 100644 index 0000000..a89567c --- /dev/null +++ b/services/entry/pkg/infrastructure/dependencies/cron.go @@ -0,0 +1,37 @@ +package dependencies + +import ( + "context" + "github.com/docker/docker/api/types" + "github.com/docker/docker/client" + "github.com/eko/gocache/cache" + "github.com/eko/gocache/store" + "github.com/go-co-op/gocron" + "go.uber.org/zap" + "time" +) + +func setupCron(l *zap.Logger, cm *cache.MetricCache, cc *client.Client) { + l.Info("Setting up job scheduler (cron)") + + s := gocron.NewScheduler(time.UTC) + + s.Every(10).Second().Do(func() { + l.Debug("getting container list") + list, err := cc.ContainerList(context.Background(), types.ContainerListOptions{}) + if err != nil { + l.Warn(err.Error()) + return + } + + err = cm.Set("docker-containers", list, &store.Options{ + Cost: 2, + }) + if err != nil { + l.Warn(err.Error()) + return + } + }) + + s.StartAsync() +} diff --git a/services/entry/pkg/infrastructure/dependencies/dependencies.go b/services/entry/pkg/infrastructure/dependencies/dependencies.go new file mode 100644 index 0000000..28368a5 --- /dev/null +++ b/services/entry/pkg/infrastructure/dependencies/dependencies.go @@ -0,0 +1,52 @@ +package dependencies + +import ( + "github.com/eko/gocache/cache" + "go.uber.org/zap" + "serverctl/pkg/application/applications" + "serverctl/pkg/application/projects" + "serverctl/pkg/application/users" + "serverctl/pkg/db" + "serverctl/pkg/db/postgres" +) + +type Dependencies struct { + Logger *zap.Logger + Cache *cache.MetricCache + Database *db.Client + UsersRepository users.Repository + UsersService *users.Service + ProjectsRepository projects.Repository + ProjectsService *projects.Service + ApplicationsRepository applications.Repository + ApplicationsService *applications.Service +} + +func New() *Dependencies { + + logger := setupLogger() + + cacheM := setupCache(logger) + containerClient := setupDocker(logger) + setupCron(logger, cacheM, containerClient) + + database := db.NewClient(logger) + usersRepository := postgres.NewUsersRepository(database) + usersService := users.NewService(logger, usersRepository, cacheM) + projectsRepository := postgres.NewProjectsRepository(database) + projectsService := projects.NewService(logger, projectsRepository, cacheM) + applicationsRepository := postgres.NewApplicationRepository(logger, database) + applicationsService := applications.NewService(logger, applicationsRepository) + + return &Dependencies{ + Logger: logger, + Cache: cacheM, + Database: database, + UsersRepository: usersRepository, + UsersService: usersService, + ProjectsRepository: projectsRepository, + ProjectsService: projectsService, + ApplicationsRepository: applicationsRepository, + ApplicationsService: applicationsService, + } +} diff --git a/services/entry/pkg/infrastructure/dependencies/docker.go b/services/entry/pkg/infrastructure/dependencies/docker.go new file mode 100644 index 0000000..e4f1004 --- /dev/null +++ b/services/entry/pkg/infrastructure/dependencies/docker.go @@ -0,0 +1,16 @@ +package dependencies + +import ( + "github.com/docker/docker/client" + "go.uber.org/zap" +) + +func setupDocker(l *zap.Logger) *client.Client { + l.Info("Setting up Docker") + cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + if err != nil { + panic(err) + } + + return cli +} diff --git a/services/entry/pkg/infrastructure/dependencies/logger.go b/services/entry/pkg/infrastructure/dependencies/logger.go new file mode 100644 index 0000000..e9cb0de --- /dev/null +++ b/services/entry/pkg/infrastructure/dependencies/logger.go @@ -0,0 +1,36 @@ +package dependencies + +import ( + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "io/ioutil" + "os" +) + +func setupLogger() *zap.Logger { + highPriority := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool { + return lvl >= zapcore.ErrorLevel + }) + lowPriority := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool { + return lvl < zapcore.ErrorLevel + }) + fileDebugging := zapcore.AddSync(ioutil.Discard) + fileErrors := zapcore.AddSync(ioutil.Discard) + + consoleDebugging := zapcore.Lock(os.Stdout) + consoleErrors := zapcore.Lock(os.Stderr) + + fileEncoder := zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()) + _ = zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig()) + + core := zapcore.NewTee( + zapcore.NewCore(fileEncoder, fileErrors, highPriority), + zapcore.NewCore(fileEncoder, consoleErrors, highPriority), + zapcore.NewCore(fileEncoder, fileDebugging, lowPriority), + zapcore.NewCore(fileEncoder, consoleDebugging, lowPriority), + ) + + logger := zap.New(core) + defer logger.Sync() + return logger +} diff --git a/services/entry/pkg/infrastructure/seedDatabase.go b/services/entry/pkg/infrastructure/seedDatabase.go new file mode 100644 index 0000000..5c2c13d --- /dev/null +++ b/services/entry/pkg/infrastructure/seedDatabase.go @@ -0,0 +1,62 @@ +package infrastructure + +import ( + "context" + "fmt" + "github.com/google/uuid" + "github.com/jackc/pgx/v4" + "go.uber.org/zap" + "serverctl/pkg/application/users" + "serverctl/pkg/db" +) + +func AddSeedData(database *db.Client, logger *zap.Logger) { + conn := database.GetConn(context.Background()) + defer conn.Release() + + var numRows int + err := conn.QueryRow(context.Background(), "select count(id) from sctl_user").Scan(&numRows) + if err != nil { + panic(err) + } + + if numRows == 0 { + addTestData(database, logger) + } +} + +func addTestData(database *db.Client, logger *zap.Logger) { + ctx := context.Background() + + for jobs := 0; jobs < 10; jobs++ { + go func(batchNr int) { + conn := database.GetConn(ctx) + defer conn.Release() + batch := &pgx.Batch{} + numInserts := 5_000 + for i := 0; i < numInserts; i++ { + var ( + user *users.CreateUser + err error + ) + + user, err = users.NewCreateUser(fmt.Sprintf("%s@test.com", uuid.New().String()), "password", users.NewPlainTextPasswordHasher()) + if err != nil { + panic(err) + } + + batch.Queue("INSERT INTO sctl_user(email, password_hash) values ($1, $2)", user.Email, user.PasswordHash) + } + res := conn.SendBatch(ctx, batch) + for i := 0; i < numInserts; i++ { + _, err := res.Exec() + if err != nil { + return + } + } + + logger.Debug("sent batch", + zap.Int("batchId", batchNr)) + }(jobs) + } +}