package gridbug import ( "context" "embed" _ "embed" "encoding/json" "flag" "fmt" "html/template" "io/fs" "math/rand" "net/http" "net/url" "slices" "strings" "time" "github.com/bazelbuild/rules_go/go/runfiles" "github.com/gin-gonic/contrib/gzip" "github.com/gin-gonic/gin" "github.com/go-logr/logr" "edge-infra.dev/pkg/f8n/devinfra/gcp/job/storage" tisql "edge-infra.dev/pkg/f8n/devinfra/testinfra/sql" "edge-infra.dev/pkg/lib/fog" "edge-infra.dev/pkg/lib/runtime/manager" ) //go:embed public/index.html var index embed.FS //go:embed public/favicon/* var icons embed.FS type Gridbug struct { Logger logr.Logger db *tisql.DBHandle } func init() { gin.SetMode(gin.ReleaseMode) } func New() *Gridbug { log := fog.New() _db, err := tisql.FromEnv() if err != nil { log.Error(err, "failed to initialize sql conn") } return &Gridbug{Logger: log, db: _db} } func (g *Gridbug) Start() error { addr := flag.String("http", ":8080", "Serving address") router := gin.Default() err := router.SetTrustedProxies(nil) if err != nil { return err } router.Use(gzip.Gzip(gzip.DefaultCompression)) path, err := runfiles.Rlocation("_main/pkg/f8n/devinfra/gridbug/bundle") if err != nil { return err } // gonna leave this in for now just so we can see whats going on in the container fmt.Println("path", path) // setup favicons iconsFS, err := fs.Sub(icons, "public/favicon") if err != nil { return err } router.StaticFS("/icons", http.FS(iconsFS)) // setup the static route // this includes .js and .css files router.StaticFS("/static", http.Dir(path)) templ := template.Must(template.New("").ParseFS(index, "public/index.html")) router.SetHTMLTemplate(templ) hash := randStringBytes(12) router.GET("/", func(c *gin.Context) { c.HTML(http.StatusOK, "index.html", gin.H{ "hash": hash, }) }) router.GET("/livez", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "status": "ok", }) }) grp1 := router.Group("/api/v1") { grp1.GET("/job/:repo/:workflow/:job/:run", g.getJobData) grp1.GET("/job/logs/:repo/:workflow/:job/:run", g.getLogs) grp1.GET("/recent/jobs", g.getRecentJobs) grp1.GET("/runs/:repo/:workflow/:job", g.getRuns) grp1.GET("/platform", g.getPlatformJobs) } router.NoRoute(func(c *gin.Context) { if !strings.HasPrefix(c.Request.RequestURI, "/api") || !strings.HasPrefix(c.Request.RequestURI, "/static") || !strings.HasPrefix(c.Request.RequestURI, "/public") || !strings.HasPrefix(c.Request.RequestURI, "/livez") { c.HTML(http.StatusOK, "index.html", gin.H{ "hash": hash, }) } // todo: add a 404 page }) s := manager.NewServer(router) s.Addr = *addr return s.ListenAndServe() } const letterBytes = "123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" func randStringBytes(n int) string { b := make([]byte, n) for i := range b { timeNow := time.Now().UnixNano() r := rand.New(rand.NewSource(timeNow)) // #nosec G404 - cosmetic id string only b[i] = letterBytes[r.Intn(len(letterBytes))] } return string(b) } func getName() string { return "steve" } func (g *Gridbug) getLogs(c *gin.Context) { workflow := c.Param("workflow") job := c.Param("job") repo := c.Param("repo") run := c.Param("run") ctx := context.Background() s, err := storage.New(ctx) if err != nil { g.Logger.Error(err, "failed to create storage client") c.JSON(http.StatusOK, gin.H{ "error": err.Error(), }) return } b, err := s.NewBucket(ctx) if err != nil { g.Logger.Error(err, "failed to create bucket handler") c.JSON(http.StatusOK, gin.H{ "error": err.Error(), }) return } path := storage.BasePath(repo, workflow, run, job) if job == "argo" { path = storage.ArgoBasePath(repo, workflow, run) } g.Logger.Info("attempting to find logs", "path", path) // attempt to upload the tmp file logs, err := b.RetrieveLogs(ctx, path) if err != nil { g.Logger.Error(err, "failed to retrieve logs") c.JSON(http.StatusOK, gin.H{ "error": err.Error(), }) return } c.JSON(http.StatusOK, gin.H{ "logs": string(logs["logs.txt"]), }) } func (g *Gridbug) getRuns(c *gin.Context) { workflow := c.Param("workflow") repo := c.Param("repo") job := c.Param("job") // If job is argo, it needs to be set to an empty string for the query to work if job == "argo" { job = "" } runs, err := g.db.GetRecentEdgeJobRuns(repo, workflow, job) if err != nil { g.Logger.Error(err, "failed to get recent runs") return } b, err := json.Marshal(runs) if err != nil { g.Logger.Error(err, "failed to get marshal recent runs") return } c.JSON(http.StatusOK, string(b)) } func (g *Gridbug) getRecentJobs(c *gin.Context) { r := c.Request.Referer() u, err := url.Parse(r) if err != nil { g.Logger.Error(err, "failed to parse url") return } // TODO(wc185097): send validation errors to frontend // If any keys in the query parameters are invalid or not supported, // the parameters are not included in the sql query. params := u.Query() err = validateQuery(params) if err != nil { g.Logger.Error(err, "failed to validate url query parameters") params = nil } jobs, err := g.db.GetRecentEdgeJobs(params) if err != nil { g.Logger.Error(err, "failed to get recent jobs") return } b, err := json.Marshal(jobs) if err != nil { g.Logger.Error(err, "failed to get marshal recent jobs") return } g.Logger.Info(string(b)) c.JSON(http.StatusOK, string(b)) } func (g *Gridbug) getJobData(c *gin.Context) { // workflow := c.Param("workflow") // job := c.Param("job") // repo := c.Param("repo") run := c.Param("run") jobs, err := g.db.GetEdgeJob(c.Request.Context(), run) if err != nil { g.Logger.Error(err, "failed to get specific jobs") return } b, err := json.Marshal(jobs) if err != nil { g.Logger.Error(err, "failed to get marshal recent jobs") return } g.Logger.Info(string(b)) c.JSON(http.StatusOK, string(b)) } func (g *Gridbug) getPlatformJobs(c *gin.Context) { jobs, err := g.db.GetRecentEdgeJobsByPlatform() if err != nil { g.Logger.Error(err, "failed to get recent jobs") return } b, err := json.Marshal(jobs) if err != nil { g.Logger.Error(err, "failed to get marshal recent jobs") return } g.Logger.Info(string(b)) c.JSON(http.StatusOK, string(b)) } // validateQuery validates parameter keys present, returning an error if any invalid keys are present. // Keys that can have integer values and can use arithmetic operators are currently excluded. // (e.g. tests_failed, tests_run) func validateQuery(params url.Values) error { validKeys := []string{ "elapsed", "started", "finished", "version", "path", "job", "workflow", "number", "passed", "repos", "repo_commit", "key", "value", } invalidKeys := []string{} for key := range params { if !slices.Contains(validKeys, key) { invalidKeys = append(invalidKeys, key) } } if len(invalidKeys) > 0 { return fmt.Errorf("%s are invalid keys", invalidKeys) } return nil }