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
31 var index embed.FS
32
33
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
73 fmt.Println("path", path)
74
75
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
83
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
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))
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
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
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
227
228
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
255
256
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
295
296
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