github.com/quickfeed/quickfeed@v0.0.0-20240507093252-ed8ca812a09c/main.go (about) 1 package main 2 3 import ( 4 "context" 5 "flag" 6 "fmt" 7 "log" 8 "mime" 9 "os" 10 "os/signal" 11 "syscall" 12 "time" 13 14 "github.com/quickfeed/quickfeed/ci" 15 "github.com/quickfeed/quickfeed/database" 16 "github.com/quickfeed/quickfeed/doc" 17 "github.com/quickfeed/quickfeed/internal/env" 18 "github.com/quickfeed/quickfeed/internal/qlog" 19 "github.com/quickfeed/quickfeed/scm" 20 "github.com/quickfeed/quickfeed/web" 21 "github.com/quickfeed/quickfeed/web/auth" 22 "github.com/quickfeed/quickfeed/web/manifest" 23 "golang.org/x/net/http2" 24 "golang.org/x/net/http2/h2c" 25 ) 26 27 func init() { 28 mustAddExtensionType := func(ext, typ string) { 29 if err := mime.AddExtensionType(ext, typ); err != nil { 30 panic(err) 31 } 32 } 33 34 // On Windows, mime types are read from the registry, which often has 35 // outdated content qf. This enforces that the correct mime types 36 // are used on all platforms. 37 mustAddExtensionType(".html", "text/html") 38 mustAddExtensionType(".css", "text/css") 39 mustAddExtensionType(".js", "application/javascript") 40 mustAddExtensionType(".jsx", "application/javascript") 41 mustAddExtensionType(".map", "application/json") 42 mustAddExtensionType(".ts", "application/x-typescript") 43 } 44 45 func main() { 46 var ( 47 dbFile = flag.String("database.file", env.DatabasePath(), "database file") 48 public = flag.String("http.public", env.PublicDir(), "path to content to serve") 49 httpAddr = flag.String("http.addr", ":443", "HTTP listen address") 50 dev = flag.Bool("dev", false, "run development server with self-signed certificates") 51 newApp = flag.Bool("new", false, "create new GitHub app") 52 ) 53 flag.Parse() 54 55 // Load environment variables from $QUICKFEED/.env. 56 // Will not override variables already defined in the environment. 57 const envFile = ".env" 58 if err := env.Load(env.RootEnv(envFile)); err != nil { 59 log.Fatal(err) 60 } 61 62 if env.Domain() == "localhost" { 63 log.Fatal(`Domain "localhost" is unsupported; use "127.0.0.1" instead.`) 64 } 65 66 var srvFn web.ServerType 67 if *dev { 68 srvFn = web.NewDevelopmentServer 69 } else { 70 srvFn = web.NewProductionServer 71 } 72 log.Printf("Starting QuickFeed on %s%s", env.Domain(), *httpAddr) 73 74 if *newApp { 75 if err := manifest.ReadyForAppCreation(envFile, checkDomain); err != nil { 76 log.Fatal(err) 77 } 78 if err := manifest.CreateNewQuickFeedApp(srvFn, *httpAddr, envFile); err != nil { 79 log.Fatal(err) 80 } 81 } 82 83 logger, err := qlog.Zap() 84 if err != nil { 85 log.Fatalf("Can't initialize logger: %v", err) 86 } 87 defer func() { _ = logger.Sync() }() 88 89 db, err := database.NewGormDB(*dbFile, logger) 90 if err != nil { 91 log.Fatalf("Can't connect to database: %v", err) 92 } 93 94 // Holds references for activated providers for current user token 95 bh := web.BaseHookOptions{ 96 BaseURL: env.Domain(), 97 Secret: os.Getenv("QUICKFEED_WEBHOOK_SECRET"), 98 } 99 100 scmConfig, err := scm.NewSCMConfig() 101 if err != nil { 102 log.Fatal(err) 103 } 104 105 tokenManager, err := auth.NewTokenManager(db) 106 if err != nil { 107 log.Fatal(err) 108 } 109 authConfig := auth.NewGitHubConfig(env.Domain(), scmConfig) 110 log.Print("Callback: ", authConfig.RedirectURL) 111 scmManager := scm.NewSCMManager(scmConfig) 112 113 runner, err := ci.NewDockerCI(logger.Sugar()) 114 if err != nil { 115 log.Fatalf("Failed to set up docker client: %v", err) 116 } 117 defer runner.Close() 118 119 qfService := web.NewQuickFeedService(logger, db, scmManager, bh, runner) 120 // Register HTTP endpoints and webhooks 121 router := qfService.RegisterRouter(tokenManager, authConfig, *public) 122 handler := h2c.NewHandler(router, &http2.Server{}) 123 124 srv, err := srvFn(*httpAddr, handler) 125 if err != nil { 126 log.Fatal(err) 127 } 128 129 ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) 130 defer stop() 131 go func() { 132 <-ctx.Done() 133 ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) 134 defer cancel() 135 if err := srv.Shutdown(ctx); err != nil { 136 log.Fatalf("Graceful shutdown failed: %v", err) 137 } 138 }() 139 140 if err := srv.Serve(); err != nil { 141 log.Fatalf("Failed to start QuickFeed server: %v", err) 142 } 143 log.Println("QuickFeed shut down gracefully") 144 } 145 146 func checkDomain() error { 147 if env.Domain() == "127.0.0.1" { 148 msg := ` 149 WARNING: You are creating a GitHub app on "127.0.0.1". 150 This is only for development purposes. 151 In this mode, QuickFeed will not be able to receive webhook events from GitHub. 152 To receive webhook events, you must run QuickFeed on a public domain or use a tunneling service like ngrok. 153 ` 154 fmt.Println(msg) 155 fmt.Printf("Read more here: %s\n\n", doc.DeployURL) 156 fmt.Print("Do you want to continue? (Y/n) ") 157 var answer string 158 fmt.Scanln(&answer) 159 if !(answer == "Y" || answer == "y") { 160 return fmt.Errorf("aborting %s GitHub App creation", env.AppName()) 161 } 162 } 163 return nil 164 }