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  }