...

Source file src/edge-infra.dev/pkg/f8n/devinfra/gridbug/gridbug.go

Documentation: edge-infra.dev/pkg/f8n/devinfra/gridbug

     1  package gridbug
     2  
     3  import (
     4  	"context"
     5  	"embed"
     6  	_ "embed"
     7  	"encoding/json"
     8  	"flag"
     9  	"fmt"
    10  	"html/template"
    11  	"io/fs"
    12  	"math/rand"
    13  	"net/http"
    14  	"net/url"
    15  	"slices"
    16  	"strings"
    17  	"time"
    18  
    19  	"github.com/bazelbuild/rules_go/go/runfiles"
    20  	"github.com/gin-gonic/contrib/gzip"
    21  	"github.com/gin-gonic/gin"
    22  	"github.com/go-logr/logr"
    23  
    24  	"edge-infra.dev/pkg/f8n/devinfra/gcp/job/storage"
    25  	tisql "edge-infra.dev/pkg/f8n/devinfra/testinfra/sql"
    26  	"edge-infra.dev/pkg/lib/fog"
    27  	"edge-infra.dev/pkg/lib/runtime/manager"
    28  )
    29  
    30  //go:embed public/index.html
    31  var index embed.FS
    32  
    33  //go:embed public/favicon/*
    34  var icons embed.FS
    35  
    36  type Gridbug struct {
    37  	Logger logr.Logger
    38  	db     *tisql.DBHandle
    39  }
    40  
    41  func init() {
    42  	gin.SetMode(gin.ReleaseMode)
    43  }
    44  
    45  func New() *Gridbug {
    46  	log := fog.New()
    47  
    48  	_db, err := tisql.FromEnv()
    49  	if err != nil {
    50  		log.Error(err, "failed to initialize sql conn")
    51  	}
    52  
    53  	return &Gridbug{Logger: log, db: _db}
    54  }
    55  
    56  func (g *Gridbug) Start() error {
    57  	addr := flag.String("http", ":8080", "Serving address")
    58  
    59  	router := gin.Default()
    60  	err := router.SetTrustedProxies(nil)
    61  	if err != nil {
    62  		return err
    63  	}
    64  
    65  	router.Use(gzip.Gzip(gzip.DefaultCompression))
    66  
    67  	path, err := runfiles.Rlocation("_main/pkg/f8n/devinfra/gridbug/bundle")
    68  	if err != nil {
    69  		return err
    70  	}
    71  
    72  	// gonna leave this in for now just so we can see whats going on in the container
    73  	fmt.Println("path", path)
    74  
    75  	// setup favicons
    76  	iconsFS, err := fs.Sub(icons, "public/favicon")
    77  	if err != nil {
    78  		return err
    79  	}
    80  	router.StaticFS("/icons", http.FS(iconsFS))
    81  
    82  	// setup the static route
    83  	// this includes .js and .css files
    84  	router.StaticFS("/static", http.Dir(path))
    85  
    86  	templ := template.Must(template.New("").ParseFS(index, "public/index.html"))
    87  	router.SetHTMLTemplate(templ)
    88  
    89  	hash := randStringBytes(12)
    90  	router.GET("/", func(c *gin.Context) {
    91  		c.HTML(http.StatusOK, "index.html", gin.H{
    92  			"hash": hash,
    93  		})
    94  	})
    95  
    96  	router.GET("/livez", func(c *gin.Context) {
    97  		c.JSON(http.StatusOK, gin.H{
    98  			"status": "ok",
    99  		})
   100  	})
   101  
   102  	grp1 := router.Group("/api/v1")
   103  	{
   104  		grp1.GET("/job/:repo/:workflow/:job/:run", g.getJobData)
   105  		grp1.GET("/job/logs/:repo/:workflow/:job/:run", g.getLogs)
   106  		grp1.GET("/recent/jobs", g.getRecentJobs)
   107  		grp1.GET("/runs/:repo/:workflow/:job", g.getRuns)
   108  		grp1.GET("/platform", g.getPlatformJobs)
   109  	}
   110  
   111  	router.NoRoute(func(c *gin.Context) {
   112  		if !strings.HasPrefix(c.Request.RequestURI, "/api") ||
   113  			!strings.HasPrefix(c.Request.RequestURI, "/static") ||
   114  			!strings.HasPrefix(c.Request.RequestURI, "/public") ||
   115  			!strings.HasPrefix(c.Request.RequestURI, "/livez") {
   116  			c.HTML(http.StatusOK, "index.html", gin.H{
   117  				"hash": hash,
   118  			})
   119  		}
   120  		// todo: add a 404 page
   121  	})
   122  
   123  	s := manager.NewServer(router)
   124  	s.Addr = *addr
   125  
   126  	return s.ListenAndServe()
   127  }
   128  
   129  const letterBytes = "123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
   130  
   131  func randStringBytes(n int) string {
   132  	b := make([]byte, n)
   133  	for i := range b {
   134  		timeNow := time.Now().UnixNano()
   135  		r := rand.New(rand.NewSource(timeNow)) // #nosec G404 - cosmetic id string only
   136  		b[i] = letterBytes[r.Intn(len(letterBytes))]
   137  	}
   138  	return string(b)
   139  }
   140  
   141  func getName() string {
   142  	return "steve"
   143  }
   144  
   145  func (g *Gridbug) getLogs(c *gin.Context) {
   146  	workflow := c.Param("workflow")
   147  	job := c.Param("job")
   148  	repo := c.Param("repo")
   149  	run := c.Param("run")
   150  
   151  	ctx := context.Background()
   152  
   153  	s, err := storage.New(ctx)
   154  	if err != nil {
   155  		g.Logger.Error(err, "failed to create storage client")
   156  		c.JSON(http.StatusOK, gin.H{
   157  			"error": err.Error(),
   158  		})
   159  		return
   160  	}
   161  
   162  	b, err := s.NewBucket(ctx)
   163  	if err != nil {
   164  		g.Logger.Error(err, "failed to create bucket handler")
   165  		c.JSON(http.StatusOK, gin.H{
   166  			"error": err.Error(),
   167  		})
   168  		return
   169  	}
   170  
   171  	path := storage.BasePath(repo, workflow, run, job)
   172  	if job == "argo" {
   173  		path = storage.ArgoBasePath(repo, workflow, run)
   174  	}
   175  
   176  	g.Logger.Info("attempting to find logs", "path", path)
   177  
   178  	// attempt to upload the tmp file
   179  	logs, err := b.RetrieveLogs(ctx, path)
   180  	if err != nil {
   181  		g.Logger.Error(err, "failed to retrieve logs")
   182  		c.JSON(http.StatusOK, gin.H{
   183  			"error": err.Error(),
   184  		})
   185  		return
   186  	}
   187  
   188  	c.JSON(http.StatusOK, gin.H{
   189  		"logs": string(logs["logs.txt"]),
   190  	})
   191  }
   192  
   193  func (g *Gridbug) getRuns(c *gin.Context) {
   194  	workflow := c.Param("workflow")
   195  	repo := c.Param("repo")
   196  	job := c.Param("job")
   197  
   198  	// If job is argo, it needs to be set to an empty string for the query to work
   199  	if job == "argo" {
   200  		job = ""
   201  	}
   202  
   203  	runs, err := g.db.GetRecentEdgeJobRuns(repo, workflow, job)
   204  	if err != nil {
   205  		g.Logger.Error(err, "failed to get recent runs")
   206  		return
   207  	}
   208  
   209  	b, err := json.Marshal(runs)
   210  	if err != nil {
   211  		g.Logger.Error(err, "failed to get marshal recent runs")
   212  		return
   213  	}
   214  
   215  	c.JSON(http.StatusOK, string(b))
   216  }
   217  
   218  func (g *Gridbug) getRecentJobs(c *gin.Context) {
   219  	r := c.Request.Referer()
   220  	u, err := url.Parse(r)
   221  	if err != nil {
   222  		g.Logger.Error(err, "failed to parse url")
   223  		return
   224  	}
   225  
   226  	// TODO(wc185097): send validation errors to frontend
   227  	// If any keys in the query parameters are invalid or not supported,
   228  	// the parameters are not included in the sql query.
   229  	params := u.Query()
   230  	err = validateQuery(params)
   231  	if err != nil {
   232  		g.Logger.Error(err, "failed to validate url query parameters")
   233  		params = nil
   234  	}
   235  
   236  	jobs, err := g.db.GetRecentEdgeJobs(params)
   237  	if err != nil {
   238  		g.Logger.Error(err, "failed to get recent jobs")
   239  		return
   240  	}
   241  
   242  	b, err := json.Marshal(jobs)
   243  	if err != nil {
   244  		g.Logger.Error(err, "failed to get marshal recent jobs")
   245  		return
   246  	}
   247  
   248  	g.Logger.Info(string(b))
   249  
   250  	c.JSON(http.StatusOK, string(b))
   251  }
   252  
   253  func (g *Gridbug) getJobData(c *gin.Context) {
   254  	// workflow := c.Param("workflow")
   255  	// job := c.Param("job")
   256  	// repo := c.Param("repo")
   257  	run := c.Param("run")
   258  
   259  	jobs, err := g.db.GetEdgeJob(c.Request.Context(), run)
   260  	if err != nil {
   261  		g.Logger.Error(err, "failed to get specific jobs")
   262  		return
   263  	}
   264  
   265  	b, err := json.Marshal(jobs)
   266  	if err != nil {
   267  		g.Logger.Error(err, "failed to get marshal recent jobs")
   268  		return
   269  	}
   270  
   271  	g.Logger.Info(string(b))
   272  
   273  	c.JSON(http.StatusOK, string(b))
   274  }
   275  
   276  func (g *Gridbug) getPlatformJobs(c *gin.Context) {
   277  	jobs, err := g.db.GetRecentEdgeJobsByPlatform()
   278  	if err != nil {
   279  		g.Logger.Error(err, "failed to get recent jobs")
   280  		return
   281  	}
   282  
   283  	b, err := json.Marshal(jobs)
   284  	if err != nil {
   285  		g.Logger.Error(err, "failed to get marshal recent jobs")
   286  		return
   287  	}
   288  
   289  	g.Logger.Info(string(b))
   290  
   291  	c.JSON(http.StatusOK, string(b))
   292  }
   293  
   294  // validateQuery validates parameter keys present, returning an error if any invalid keys are present.
   295  // Keys that can have integer values and can use arithmetic operators are currently excluded.
   296  // (e.g. tests_failed, tests_run)
   297  func validateQuery(params url.Values) error {
   298  	validKeys := []string{
   299  		"elapsed",
   300  		"started",
   301  		"finished",
   302  		"version",
   303  		"path",
   304  		"job",
   305  		"workflow",
   306  		"number",
   307  		"passed",
   308  		"repos",
   309  		"repo_commit",
   310  		"key",
   311  		"value",
   312  	}
   313  	invalidKeys := []string{}
   314  
   315  	for key := range params {
   316  		if !slices.Contains(validKeys, key) {
   317  			invalidKeys = append(invalidKeys, key)
   318  		}
   319  	}
   320  
   321  	if len(invalidKeys) > 0 {
   322  		return fmt.Errorf("%s are invalid keys", invalidKeys)
   323  	}
   324  	return nil
   325  }
   326  

View as plain text