github.com/nais/knorten@v0.0.0-20240104110906-55926958e361/pkg/api/admin.go (about) 1 package api 2 3 import ( 4 "context" 5 "encoding/gob" 6 "fmt" 7 "net/http" 8 "net/url" 9 "strings" 10 11 "github.com/google/uuid" 12 "github.com/nais/knorten/pkg/chart" 13 "github.com/nais/knorten/pkg/database" 14 "github.com/nais/knorten/pkg/database/gensql" 15 "github.com/nais/knorten/pkg/k8s" 16 17 "github.com/gin-contrib/sessions" 18 "github.com/gin-gonic/gin" 19 ) 20 21 type diffValue struct { 22 Old string 23 New string 24 Encrypted string 25 } 26 27 type teamInfo struct { 28 gensql.Team 29 Namespace string 30 Apps []gensql.ChartType 31 Events []gensql.Event 32 } 33 34 func (c *client) setupAdminRoutes() { 35 c.router.GET("/admin", func(ctx *gin.Context) { 36 session := sessions.Default(ctx) 37 flashes := session.Flashes() 38 err := session.Save() 39 if err != nil { 40 c.log.WithError(err).Error("problem saving session") 41 ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": err}) 42 return 43 } 44 45 teams, err := c.repo.TeamsGet(ctx) 46 if err != nil { 47 session := sessions.Default(ctx) 48 session.AddFlash(err.Error()) 49 err = session.Save() 50 if err != nil { 51 c.log.WithError(err).Error("problem saving session") 52 ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": err}) 53 return 54 } 55 56 ctx.Redirect(http.StatusSeeOther, "/admin") 57 return 58 } 59 60 teamApps := map[string]teamInfo{} 61 for _, team := range teams { 62 apps, err := c.repo.ChartsForTeamGet(ctx, team.ID) 63 if err != nil { 64 c.log.WithError(err).Error("problem retrieving apps for teams") 65 ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": err}) 66 return 67 } 68 events, err := c.repo.EventsByOwnerGet(ctx, team.ID, 5) 69 if err != nil { 70 c.log.WithError(err).Error("problem retrieving apps for teams") 71 ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": err}) 72 return 73 } 74 75 teamApps[team.ID] = teamInfo{ 76 Team: team, 77 Namespace: k8s.TeamIDToNamespace(team.ID), 78 Apps: apps, 79 Events: events, 80 } 81 } 82 83 c.htmlResponseWrapper(ctx, http.StatusOK, "admin/index", gin.H{ 84 "errors": flashes, 85 "teams": teamApps, 86 "gcpProject": c.gcpProject, 87 }) 88 }) 89 90 c.router.GET("/admin/:chart", func(ctx *gin.Context) { 91 chartType := getChartType(ctx.Param("chart")) 92 93 values, err := c.repo.GlobalValuesGet(ctx, chartType) 94 if err != nil { 95 session := sessions.Default(ctx) 96 session.AddFlash(err.Error()) 97 err = session.Save() 98 if err != nil { 99 c.log.WithError(err).Error("problem saving session") 100 ctx.Redirect(http.StatusSeeOther, "/admin") 101 return 102 } 103 104 ctx.Redirect(http.StatusSeeOther, "/admin") 105 return 106 } 107 108 session := sessions.Default(ctx) 109 flashes := session.Flashes() 110 err = session.Save() 111 if err != nil { 112 c.log.WithError(err).Error("problem saving session") 113 ctx.Redirect(http.StatusSeeOther, "/admin") 114 return 115 } 116 117 c.htmlResponseWrapper(ctx, http.StatusOK, "admin/chart", gin.H{ 118 "values": values, 119 "errors": flashes, 120 "chart": string(chartType), 121 }) 122 }) 123 124 c.router.POST("/admin/:chart", func(ctx *gin.Context) { 125 session := sessions.Default(ctx) 126 chartType := getChartType(ctx.Param("chart")) 127 128 err := ctx.Request.ParseForm() 129 if err != nil { 130 session.AddFlash(err.Error()) 131 err = session.Save() 132 if err != nil { 133 c.log.WithError(err).Error("problem saving session") 134 ctx.Redirect(http.StatusSeeOther, "admin") 135 return 136 } 137 ctx.Redirect(http.StatusSeeOther, "admin") 138 return 139 } 140 141 changedValues, err := c.findGlobalValueChanges(ctx, ctx.Request.PostForm, chartType) 142 if err != nil { 143 session := sessions.Default(ctx) 144 session.AddFlash(err.Error()) 145 err = session.Save() 146 if err != nil { 147 c.log.WithError(err).Error("problem saving session") 148 ctx.Redirect(http.StatusSeeOther, fmt.Sprintf("/admin/%v", chartType)) 149 return 150 } 151 152 ctx.Redirect(http.StatusSeeOther, fmt.Sprintf("/admin/%v", chartType)) 153 return 154 } 155 156 if len(changedValues) == 0 { 157 session.AddFlash("Ingen endringer lagret") 158 err = session.Save() 159 if err != nil { 160 c.log.WithError(err).Error("problem saving session") 161 ctx.Redirect(http.StatusSeeOther, fmt.Sprintf("/admin/%v", chartType)) 162 return 163 } 164 ctx.Redirect(http.StatusSeeOther, "/admin") 165 return 166 } 167 168 gob.Register(changedValues) 169 session.AddFlash(changedValues) 170 err = session.Save() 171 if err != nil { 172 c.log.WithError(err).Error("problem saving session") 173 ctx.Redirect(http.StatusSeeOther, fmt.Sprintf("/admin/%v", chartType)) 174 return 175 } 176 177 ctx.Redirect(http.StatusSeeOther, fmt.Sprintf("/admin/%v/confirm", chartType)) 178 }) 179 180 c.router.GET("/admin/:chart/confirm", func(ctx *gin.Context) { 181 chartType := getChartType(ctx.Param("chart")) 182 session := sessions.Default(ctx) 183 changedValues := session.Flashes() 184 err := session.Save() 185 if err != nil { 186 c.log.WithError(err).Error("problem saving session") 187 ctx.Redirect(http.StatusSeeOther, fmt.Sprintf("/admin/%v", chartType)) 188 return 189 } 190 191 c.htmlResponseWrapper(ctx, http.StatusOK, "admin/confirm", gin.H{ 192 "changedValues": changedValues, 193 "chart": string(chartType), 194 }) 195 }) 196 197 c.router.POST("/admin/:chart/confirm", func(ctx *gin.Context) { 198 session := sessions.Default(ctx) 199 chartType := getChartType(ctx.Param("chart")) 200 201 err := ctx.Request.ParseForm() 202 if err != nil { 203 c.log.WithError(err) 204 session.AddFlash(err.Error()) 205 err = session.Save() 206 if err != nil { 207 c.log.WithError(err).Error("problem saving session") 208 ctx.Redirect(http.StatusSeeOther, fmt.Sprintf("/admin/%v/confirm", chartType)) 209 return 210 } 211 ctx.Redirect(http.StatusSeeOther, fmt.Sprintf("/admin/%v/confirm", chartType)) 212 return 213 } 214 215 if err := c.updateGlobalValues(ctx, ctx.Request.PostForm, chartType); err != nil { 216 c.log.WithError(err) 217 session.AddFlash(err.Error()) 218 err = session.Save() 219 if err != nil { 220 c.log.WithError(err).Error("problem saving session") 221 ctx.Redirect(http.StatusSeeOther, fmt.Sprintf("/admin/%v", chartType)) 222 return 223 } 224 ctx.Redirect(http.StatusSeeOther, fmt.Sprintf("/admin/%v", chartType)) 225 return 226 } 227 228 if err != nil { 229 c.log.WithError(err) 230 session.AddFlash(err.Error()) 231 err = session.Save() 232 if err != nil { 233 c.log.WithError(err).Error("problem saving session") 234 ctx.Redirect(http.StatusSeeOther, fmt.Sprintf("/admin/%v/confirm", chartType)) 235 return 236 } 237 ctx.Redirect(http.StatusSeeOther, fmt.Sprintf("/admin/%v/confirm", chartType)) 238 return 239 } 240 241 ctx.Redirect(http.StatusSeeOther, "/admin") 242 }) 243 244 c.router.POST("/admin/:chart/sync", func(ctx *gin.Context) { 245 session := sessions.Default(ctx) 246 chartType := getChartType(ctx.Param("chart")) 247 team := ctx.PostForm("team") 248 249 if err := c.syncChart(ctx, team, chartType); err != nil { 250 c.log.WithError(err).Errorf("syncing %v", chartType) 251 session.AddFlash(err.Error()) 252 err = session.Save() 253 if err != nil { 254 c.log.WithError(err).Error("problem saving session") 255 } 256 } 257 258 ctx.Redirect(http.StatusSeeOther, "/admin") 259 }) 260 261 c.router.POST("/admin/:chart/sync/all", func(ctx *gin.Context) { 262 session := sessions.Default(ctx) 263 chartType := getChartType(ctx.Param("chart")) 264 265 if err := c.syncChartForAllTeams(ctx, chartType); err != nil { 266 c.log.WithError(err).Errorf("resyncing all instances of %v", chartType) 267 session.AddFlash(err.Error()) 268 err = session.Save() 269 if err != nil { 270 c.log.WithError(err).Error("problem saving session") 271 } 272 } 273 274 ctx.Redirect(http.StatusSeeOther, "/admin") 275 }) 276 277 c.router.POST("/admin/team/sync/all", func(ctx *gin.Context) { 278 session := sessions.Default(ctx) 279 280 if err := c.syncTeams(ctx); err != nil { 281 c.log.WithError(err).Errorf("resyncing all teams") 282 session.AddFlash(err.Error()) 283 err = session.Save() 284 if err != nil { 285 c.log.WithError(err).Error("problem saving session") 286 } 287 } 288 289 ctx.Redirect(http.StatusSeeOther, "/admin") 290 }) 291 292 c.router.POST("/admin/team/:team/delete", func(ctx *gin.Context) { 293 session := sessions.Default(ctx) 294 slug := ctx.Param("team") 295 296 team, err := c.repo.TeamBySlugGet(ctx, slug) 297 if err != nil { 298 c.log.WithError(err).Errorf("deleting team") 299 session.AddFlash(err.Error()) 300 err = session.Save() 301 if err != nil { 302 c.log.WithError(err).Error("problem saving session") 303 } 304 } 305 306 if err := c.repo.RegisterDeleteTeamEvent(ctx, team.ID); err != nil { 307 c.log.WithError(err).Errorf("registering delete team event") 308 session.AddFlash(err.Error()) 309 err = session.Save() 310 if err != nil { 311 c.log.WithError(err).Error("problem saving session") 312 } 313 } 314 315 ctx.Redirect(http.StatusSeeOther, "/admin") 316 }) 317 318 c.router.GET("/admin/event/:id", func(ctx *gin.Context) { 319 header, err := c.getEvent(ctx) 320 if err != nil { 321 c.log.WithError(err).Errorf("getting event logs") 322 session := sessions.Default(ctx) 323 session.AddFlash(err.Error()) 324 err = session.Save() 325 if err != nil { 326 c.log.WithError(err).Error("problem saving session") 327 } 328 ctx.Redirect(http.StatusSeeOther, "/admin") 329 } 330 331 c.htmlResponseWrapper(ctx, http.StatusOK, "admin/event", header) 332 }) 333 334 c.router.POST("/admin/event/:id", func(ctx *gin.Context) { 335 err := c.setEventStatus(ctx) 336 if err != nil { 337 c.log.WithError(err).Errorf("setting event status") 338 session := sessions.Default(ctx) 339 session.AddFlash(err.Error()) 340 err = session.Save() 341 if err != nil { 342 c.log.WithError(err).Error("problem saving session") 343 } 344 ctx.Redirect(http.StatusSeeOther, "/admin/event/"+ctx.Param("id")) 345 } 346 347 ctx.Redirect(http.StatusSeeOther, "/admin/event/"+ctx.Param("id")) 348 }) 349 } 350 351 func (c *client) syncTeams(ctx context.Context) error { 352 teams, err := c.repo.TeamsGet(ctx) 353 if err != nil { 354 return err 355 } 356 357 for _, team := range teams { 358 err := c.repo.RegisterUpdateTeamEvent(ctx, team) 359 if err != nil { 360 return err 361 } 362 } 363 364 return nil 365 } 366 367 func (c *client) syncChartForAllTeams(ctx context.Context, chartType gensql.ChartType) error { 368 teams, err := c.repo.TeamsForChartGet(ctx, chartType) 369 if err != nil { 370 return err 371 } 372 373 for _, teamID := range teams { 374 err := c.syncChart(ctx, teamID, chartType) 375 if err != nil { 376 return err 377 } 378 } 379 380 return nil 381 } 382 383 func (c *client) syncChart(ctx context.Context, teamID string, chartType gensql.ChartType) error { 384 switch chartType { 385 case gensql.ChartTypeJupyterhub: 386 values := chart.JupyterConfigurableValues{ 387 TeamID: teamID, 388 } 389 return c.repo.RegisterUpdateJupyterEvent(ctx, teamID, values) 390 case gensql.ChartTypeAirflow: 391 values := chart.AirflowConfigurableValues{ 392 TeamID: teamID, 393 } 394 return c.repo.RegisterUpdateAirflowEvent(ctx, teamID, values) 395 } 396 397 return nil 398 } 399 400 func (c *client) findGlobalValueChanges(ctx context.Context, formValues url.Values, chartType gensql.ChartType) (map[string]diffValue, error) { 401 originals, err := c.repo.GlobalValuesGet(ctx, chartType) 402 if err != nil { 403 return nil, err 404 } 405 406 changed := findChangedValues(originals, formValues) 407 findDeletedValues(changed, originals, formValues) 408 409 return changed, nil 410 } 411 412 func (c *client) updateGlobalValues(ctx context.Context, formValues url.Values, chartType gensql.ChartType) error { 413 for key, values := range formValues { 414 if values[0] == "" { 415 err := c.repo.GlobalValueDelete(ctx, key, chartType) 416 if err != nil { 417 return err 418 } 419 } else { 420 value, encrypted, err := c.parseValue(values) 421 if err != nil { 422 return err 423 } 424 425 err = c.repo.GlobalChartValueInsert(ctx, key, value, encrypted, chartType) 426 if err != nil { 427 return err 428 } 429 } 430 } 431 432 return c.syncChartForAllTeams(ctx, chartType) 433 } 434 435 func (c *client) parseValue(values []string) (string, bool, error) { 436 if len(values) == 2 { 437 value, err := c.repo.EncryptValue(values[0]) 438 if err != nil { 439 return "", false, err 440 } 441 return value, true, nil 442 } 443 444 return values[0], false, nil 445 } 446 447 func findDeletedValues(changedValues map[string]diffValue, originals []gensql.ChartGlobalValue, formValues url.Values) { 448 for _, original := range originals { 449 notFound := true 450 for key := range formValues { 451 if original.Key == key { 452 notFound = false 453 break 454 } 455 } 456 457 if notFound { 458 changedValues[original.Key] = diffValue{ 459 Old: original.Value, 460 } 461 } 462 } 463 } 464 465 func findChangedValues(originals []gensql.ChartGlobalValue, formValues url.Values) map[string]diffValue { 466 changedValues := map[string]diffValue{} 467 468 for key, values := range formValues { 469 var encrypted string 470 value := values[0] 471 if len(values) == 2 { 472 encrypted = values[1] 473 } 474 475 if strings.HasPrefix(key, "key") { 476 correctValue := valueForKey(changedValues, key) 477 if correctValue != nil { 478 changedValues[value] = *correctValue 479 delete(changedValues, key) 480 } else { 481 key := strings.Replace(key, "key", "value", 1) 482 diff := diffValue{ 483 New: key, 484 Encrypted: encrypted, 485 } 486 changedValues[value] = diff 487 } 488 } else if strings.HasPrefix(key, "value") { 489 correctKey := keyForValue(changedValues, key) 490 if correctKey != "" { 491 diff := diffValue{ 492 New: value, 493 Encrypted: encrypted, 494 } 495 changedValues[correctKey] = diff 496 } else { 497 key := strings.Replace(key, "value", "key", 1) 498 diff := diffValue{ 499 New: value, 500 Encrypted: encrypted, 501 } 502 changedValues[key] = diff 503 } 504 } else { 505 for _, originalValue := range originals { 506 if originalValue.Key == key { 507 if originalValue.Value != value { 508 diff := diffValue{ 509 Old: originalValue.Value, 510 New: value, 511 Encrypted: encrypted, 512 } 513 changedValues[key] = diff 514 break 515 } 516 } 517 } 518 } 519 } 520 521 return changedValues 522 } 523 524 func valueForKey(values map[string]diffValue, needle string) *diffValue { 525 for key, value := range values { 526 if key == needle { 527 return &value 528 } 529 } 530 531 return nil 532 } 533 534 func keyForValue(values map[string]diffValue, needle string) string { 535 for key, value := range values { 536 if value.New == needle { 537 return key 538 } 539 } 540 541 return "" 542 } 543 544 func (c *client) getEvent(ctx *gin.Context) (gin.H, error) { 545 eventID, err := uuid.Parse(ctx.Param("id")) 546 if err != nil { 547 return gin.H{}, err 548 } 549 550 event, err := c.repo.EventGet(ctx, eventID) 551 if err != nil { 552 return gin.H{}, err 553 } 554 555 eventLogs, err := c.repo.EventLogsForEventGet(ctx, eventID) 556 if err != nil { 557 return gin.H{}, err 558 } 559 560 return gin.H{ 561 "event": event, 562 "logs": eventLogs, 563 }, nil 564 } 565 566 func (c *client) setEventStatus(ctx *gin.Context) error { 567 eventID, err := uuid.Parse(ctx.Param("id")) 568 if err != nil { 569 return err 570 } 571 572 var status database.EventStatus 573 switch ctx.PostForm("status") { 574 case string(database.EventStatusNew): 575 status = database.EventStatusNew 576 case string(database.EventStatusFailed): 577 status = database.EventStatusFailed 578 default: 579 return fmt.Errorf("invalid status %v", ctx.PostForm("status")) 580 } 581 582 return c.repo.EventSetStatus(ctx, eventID, status) 583 }