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