package main import ( "context" "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/prometheus/client_golang/prometheus/promhttp" "go.uber.org/zap" "go.uber.org/zap/zapcore" "io/ioutil" "net/http" "os" "serverctl/pkg/application/projects" "serverctl/pkg/application/users" "serverctl/pkg/db" "serverctl/pkg/db/postgres" "time" ) 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()) consoleEncoder := zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig()) core := zapcore.NewTee( zapcore.NewCore(fileEncoder, fileErrors, highPriority), zapcore.NewCore(consoleEncoder, consoleErrors, highPriority), zapcore.NewCore(fileEncoder, fileDebugging, lowPriority), zapcore.NewCore(consoleEncoder, 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 GetProject struct { Id int `json:"id" binding:"required"` Name string `json:"name" binding:"required"` MemberIds []int `json:"memberIds" binding:"required"` AdminIds []int `json:"adminIds" binding:"required"` } getProject := make([]GetProject, 0) for _, p := range projectsArr { getProject = append(getProject, GetProject{ Id: p.Id, Name: p.Name, MemberIds: p.MemberIds, AdminIds: p.AdminIds, }) } 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) setupApi(logger, cacheM, usersService, projectsService) }