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/pgxpool" "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/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) { 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) }) 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() } 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) setupProfiler() addSeedData(database, usersRepository, projectsRepository) setupApi(logger, cacheM, usersService, projectsService) } func setupProfiler() { go func() { log.Println(http.ListenAndServe(":6060", nil)) }() } func addSeedData(database *db.Client, ur users.Repository, pr projects.Repository) { 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(conn, ur, pr) } } func addTestData(conn *pgxpool.Conn, ur users.Repository, pr projects.Repository) { ctx := context.Background() for jobs := 0; jobs < 100; jobs++ { go func() { for i := 0; i < 1_000; i++ { var ( user *users.CreateUser err error userId int ) user, err = users.NewCreateUser(fmt.Sprintf("%s@test.com", uuid.New().String()), "password", users.NewPlainTextPasswordHasher()) if err != nil { panic(err) } userId, err = ur.Create(ctx, user) if err != nil { panic(err) } _, err = pr.Create(ctx, projects.NewCreateProject(uuid.New().String()[:20], userId)) if err != nil { panic(err) } } }() } }