github.com/nais/knorten@v0.0.0-20240104110906-55926958e361/pkg/api/chart_test.go (about) 1 package api 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "io" 8 "net/http" 9 "net/url" 10 "reflect" 11 "strconv" 12 "strings" 13 "testing" 14 15 "github.com/google/go-cmp/cmp" 16 "github.com/nais/knorten/pkg/chart" 17 "github.com/nais/knorten/pkg/database" 18 "github.com/nais/knorten/pkg/database/gensql" 19 ) 20 21 func TestJupyterAPI(t *testing.T) { 22 ctx := context.Background() 23 24 team, err := prepareChartTests(ctx, "jupyter-team") 25 if err != nil { 26 t.Fatalf("preparing jupyter chart tests: %v", err) 27 } 28 29 t.Cleanup(func() { 30 if err := repo.TeamDelete(ctx, team.ID); err != nil { 31 t.Errorf("cleaning up after jupyter tests %v", err) 32 } 33 }) 34 35 t.Run("get new jupyterhub html", func(t *testing.T) { 36 resp, err := server.Client().Get(fmt.Sprintf("%v/team/%v/jupyterhub/new", server.URL, team.Slug)) 37 if err != nil { 38 t.Error(err) 39 } 40 defer resp.Body.Close() 41 42 if resp.StatusCode != http.StatusOK { 43 t.Errorf("Status code is %v, should be %v", resp.StatusCode, http.StatusOK) 44 } 45 46 if resp.Header.Get("Content-Type") != htmlContentType { 47 t.Errorf("Content-Type header is %v, should be %v", resp.Header.Get("Content-Type"), htmlContentType) 48 } 49 50 received, err := io.ReadAll(resp.Body) 51 if err != nil { 52 t.Error(err) 53 } 54 receivedMinimized, err := minimizeHTML(string(received)) 55 if err != nil { 56 t.Error(err) 57 } 58 59 expected, err := createExpectedHTML("charts/jupyterhub", map[string]any{ 60 "team": team.Slug, 61 }) 62 if err != nil { 63 t.Error(err) 64 } 65 expectedMinimized, err := minimizeHTML(expected) 66 if err != nil { 67 t.Error(err) 68 } 69 70 if diff := cmp.Diff(expectedMinimized, receivedMinimized); diff != "" { 71 t.Errorf("mismatch (-want +got):\n%s", diff) 72 } 73 }) 74 75 t.Run("create new jupyterhub", func(t *testing.T) { 76 cpu := "1.0" 77 memory := "2G" 78 culltimeout := "3600" 79 80 data := url.Values{"cpu": {cpu}, "memory": {memory}, "imagename": {""}, "imagetag": {""}, "culltimeout": {culltimeout}} 81 resp, err := server.Client().PostForm(fmt.Sprintf("%v/team/%v/jupyterhub/new", server.URL, team.Slug), data) 82 if err != nil { 83 t.Error(err) 84 } 85 defer resp.Body.Close() 86 87 events, err := repo.EventsGetType(ctx, database.EventTypeCreateJupyter) 88 if err != nil { 89 t.Error(err) 90 } 91 92 eventPayload, err := getEventForJupyterhub(events, team.ID) 93 if err != nil { 94 t.Error(err) 95 } 96 97 if eventPayload.TeamID == "" { 98 t.Errorf("create jupyterhub: no event registered for team %v", team.ID) 99 } 100 if eventPayload.CPU != cpu { 101 t.Errorf("create jupyterhub: cpu value - expected %v, got %v", cpu, eventPayload.CPU) 102 } 103 104 if eventPayload.Memory != memory { 105 t.Errorf("create jupyterhub: memory value - expected %v, got %v", memory, eventPayload.Memory) 106 } 107 108 if eventPayload.CullTimeout != culltimeout { 109 t.Errorf("create jupyterhub: culltimeout value - expected %v, got %v", culltimeout, eventPayload.CullTimeout) 110 } 111 112 if len(eventPayload.UserIdents) != 3 { 113 t.Errorf("create jupyterhub: expected 3 users, got %v", len(eventPayload.UserIdents)) 114 } 115 }) 116 117 expectedValues := chart.JupyterConfigurableValues{ 118 TeamID: team.ID, 119 CPU: "1.0", 120 Memory: "1G", 121 CullTimeout: "3600", 122 } 123 124 if err := createChartForTeam(ctx, team.ID, expectedValues, gensql.ChartTypeJupyterhub); err != nil { 125 t.Error(err) 126 } 127 128 t.Run("get edit jupyterhub html", func(t *testing.T) { 129 resp, err := server.Client().Get(fmt.Sprintf("%v/team/%v/jupyterhub/edit", server.URL, team.Slug)) 130 if err != nil { 131 t.Error(err) 132 } 133 defer resp.Body.Close() 134 135 if resp.StatusCode != http.StatusOK { 136 t.Errorf("Status code is %v, should be %v", resp.StatusCode, http.StatusOK) 137 } 138 139 if resp.Header.Get("Content-Type") != htmlContentType { 140 t.Errorf("Content-Type header is %v, should be %v", resp.Header.Get("Content-Type"), htmlContentType) 141 } 142 143 received, err := io.ReadAll(resp.Body) 144 if err != nil { 145 t.Error(err) 146 } 147 receivedMinimized, err := minimizeHTML(string(received)) 148 if err != nil { 149 t.Error(err) 150 } 151 152 expected, err := createExpectedHTML("charts/jupyterhub", map[string]any{ 153 "team": team.Slug, 154 "values": &jupyterForm{ 155 CPU: expectedValues.CPU, 156 Memory: expectedValues.Memory, 157 CullTimeout: expectedValues.CullTimeout, 158 }, 159 }) 160 if err != nil { 161 t.Error(err) 162 } 163 expectedMinimized, err := minimizeHTML(expected) 164 if err != nil { 165 t.Error(err) 166 } 167 168 if diff := cmp.Diff(expectedMinimized, receivedMinimized); diff != "" { 169 t.Errorf("mismatch (-want +got):\n%s", diff) 170 } 171 }) 172 173 t.Run("edit jupyterhub", func(t *testing.T) { 174 newCPU := "2.0" 175 newMemory := "2G" 176 imageName := "ghcr.io/org/repo/image" 177 imageTag := "v1" 178 newCullTimeout := "7200" 179 data := url.Values{"cpu": {newCPU}, "memory": {newMemory}, "imagename": {imageName}, "imagetag": {imageTag}, "culltimeout": {newCullTimeout}} 180 resp, err := server.Client().PostForm(fmt.Sprintf("%v/team/%v/jupyterhub/edit", server.URL, team.Slug), data) 181 if err != nil { 182 t.Error(err) 183 } 184 defer resp.Body.Close() 185 186 events, err := repo.EventsGetType(ctx, database.EventTypeUpdateJupyter) 187 if err != nil { 188 t.Error(err) 189 } 190 191 eventPayload, err := getEventForJupyterhub(events, team.ID) 192 if err != nil { 193 t.Error(err) 194 } 195 196 if eventPayload.TeamID == "" { 197 t.Errorf("create jupyterhub: no event registered for team %v", team.ID) 198 } 199 200 if eventPayload.CPU != newCPU { 201 t.Errorf("create jupyterhub: cpu value - expected %v, got %v", newCPU, eventPayload.CPU) 202 } 203 204 if eventPayload.Memory != newMemory { 205 t.Errorf("create jupyterhub: memory value - expected %v, got %v", newMemory, eventPayload.Memory) 206 } 207 208 if eventPayload.CullTimeout != newCullTimeout { 209 t.Errorf("create jupyterhub: culltimeout value - expected %v, got %v", newCullTimeout, eventPayload.CullTimeout) 210 } 211 212 if eventPayload.ImageName != imageName { 213 t.Errorf("create jupyterhub: image name value - expected %v, got %v", imageName, eventPayload.ImageName) 214 } 215 216 if eventPayload.ImageTag != imageTag { 217 t.Errorf("create jupyterhub: image tag value - expected %v, got %v", imageTag, eventPayload.ImageTag) 218 } 219 220 if len(eventPayload.UserIdents) != 3 { 221 t.Errorf("create jupyterhub: expected 3 users, got %v", len(eventPayload.UserIdents)) 222 } 223 }) 224 225 t.Run("delete jupyterhub", func(t *testing.T) { 226 resp, err := server.Client().PostForm(fmt.Sprintf("%v/team/%v/jupyterhub/delete", server.URL, team.Slug), nil) 227 if err != nil { 228 t.Error(err) 229 } 230 resp.Body.Close() 231 232 if resp.StatusCode != http.StatusOK { 233 t.Errorf("delete team: expected status code 200, got %v", resp.StatusCode) 234 } 235 236 events, err := repo.EventsGetType(ctx, database.EventTypeDeleteJupyter) 237 if err != nil { 238 t.Error(err) 239 } 240 241 if !deleteEventCreatedForTeam(events, team.ID) { 242 t.Errorf("delete jupyterhub: no event registered for team %v", team.ID) 243 } 244 }) 245 } 246 247 func TestAirflowAPI(t *testing.T) { 248 ctx := context.Background() 249 250 team, err := prepareChartTests(ctx, "airflow-team") 251 if err != nil { 252 t.Errorf("preparing airflow chart tests: %v", err) 253 } 254 255 t.Cleanup(func() { 256 if err := repo.TeamDelete(ctx, team.ID); err != nil { 257 t.Errorf("cleaning up after airflow tests %v", err) 258 } 259 }) 260 261 t.Run("get new airflow html", func(t *testing.T) { 262 resp, err := server.Client().Get(fmt.Sprintf("%v/team/%v/airflow/new", server.URL, team.Slug)) 263 if err != nil { 264 t.Error(err) 265 } 266 defer resp.Body.Close() 267 268 if resp.StatusCode != http.StatusOK { 269 t.Errorf("Status code is %v, should be %v", resp.StatusCode, http.StatusOK) 270 } 271 272 if resp.Header.Get("Content-Type") != htmlContentType { 273 t.Errorf("Content-Type header is %v, should be %v", resp.Header.Get("Content-Type"), htmlContentType) 274 } 275 276 received, err := io.ReadAll(resp.Body) 277 if err != nil { 278 t.Error(err) 279 } 280 receivedMinimized, err := minimizeHTML(string(received)) 281 if err != nil { 282 t.Error(err) 283 } 284 285 expected, err := createExpectedHTML("charts/airflow", map[string]any{ 286 "team": team.Slug, 287 }) 288 if err != nil { 289 t.Error(err) 290 } 291 expectedMinimized, err := minimizeHTML(expected) 292 if err != nil { 293 t.Error(err) 294 } 295 296 if diff := cmp.Diff(expectedMinimized, receivedMinimized); diff != "" { 297 t.Errorf("mismatch (-want +got):\n%s", diff) 298 } 299 }) 300 301 t.Run("create new airflow", func(t *testing.T) { 302 dagRepo := "navikt/repo" 303 dagRepoBranch := "main" 304 305 data := url.Values{"dagrepo": {dagRepo}, "dagrepobranch": {dagRepoBranch}, "restrictairflowegress": {""}} 306 resp, err := server.Client().PostForm(fmt.Sprintf("%v/team/%v/airflow/new", server.URL, team.Slug), data) 307 if err != nil { 308 t.Error(err) 309 } 310 defer resp.Body.Close() 311 312 events, err := repo.EventsGetType(ctx, database.EventTypeCreateAirflow) 313 if err != nil { 314 t.Error(err) 315 } 316 317 eventPayload, err := getEventForAirflow(events, team.ID) 318 if err != nil { 319 t.Error(err) 320 } 321 322 if eventPayload.TeamID == "" { 323 t.Errorf("create airflow: no event registered for team %v", team.ID) 324 } 325 326 if eventPayload.DagRepo != dagRepo { 327 t.Errorf("create airflow: dag repo value, expected %v, got %v", dagRepo, eventPayload.DagRepo) 328 } 329 330 if eventPayload.DagRepoBranch != dagRepoBranch { 331 t.Errorf("create airflow: dag repo branch value, expected %v, got %v", dagRepoBranch, eventPayload.DagRepoBranch) 332 } 333 334 if eventPayload.RestrictEgress { 335 t.Errorf("create airflow: restrict egress value, expected %v, got %v", false, eventPayload.RestrictEgress) 336 } 337 }) 338 339 dagRepo := "navikt/repo" 340 branch := "main" 341 expectedRestrictEgress := false 342 expectedValues := chart.AirflowConfigurableValues{ 343 TeamID: team.ID, 344 DagRepo: dagRepo, 345 DagRepoBranch: branch, 346 RestrictEgress: expectedRestrictEgress, 347 } 348 349 if err := createChartForTeam(ctx, team.ID, expectedValues, gensql.ChartTypeAirflow); err != nil { 350 t.Error(err) 351 } 352 if err := repo.TeamChartValueInsert(ctx, chart.TeamValueKeyRestrictEgress, strconv.FormatBool(expectedRestrictEgress), team.ID, gensql.ChartTypeAirflow); err != nil { 353 t.Error(err) 354 } 355 356 t.Run("get edit airflow html", func(t *testing.T) { 357 resp, err := server.Client().Get(fmt.Sprintf("%v/team/%v/airflow/edit", server.URL, team.Slug)) 358 if err != nil { 359 t.Error(err) 360 } 361 defer resp.Body.Close() 362 363 if resp.StatusCode != http.StatusOK { 364 t.Errorf("Status code is %v, should be %v", resp.StatusCode, http.StatusOK) 365 } 366 367 if resp.Header.Get("Content-Type") != htmlContentType { 368 t.Errorf("Content-Type header is %v, should be %v", resp.Header.Get("Content-Type"), htmlContentType) 369 } 370 371 received, err := io.ReadAll(resp.Body) 372 if err != nil { 373 t.Error(err) 374 } 375 receivedMinimized, err := minimizeHTML(string(received)) 376 if err != nil { 377 t.Error(err) 378 } 379 380 expected, err := createExpectedHTML("charts/airflow", map[string]any{ 381 "team": team.Slug, 382 "values": &airflowForm{ 383 DagRepo: expectedValues.DagRepo, 384 DagRepoBranch: expectedValues.DagRepoBranch, 385 }, 386 }) 387 if err != nil { 388 t.Error(err) 389 } 390 expectedMinimized, err := minimizeHTML(expected) 391 if err != nil { 392 t.Error(err) 393 } 394 395 if diff := cmp.Diff(expectedMinimized, receivedMinimized); diff != "" { 396 t.Errorf("mismatch (-want +got):\n%s", diff) 397 } 398 }) 399 400 t.Run("edit airflow", func(t *testing.T) { 401 newDagRepo := "navikt/newrepo" 402 newDagRepoBranch := "master" 403 customImage := "ghcr.io/navikt/myimage:v1" 404 405 data := url.Values{"dagrepo": {newDagRepo}, "dagrepobranch": {newDagRepoBranch}, "airflowimage": {customImage}} 406 resp, err := server.Client().PostForm(fmt.Sprintf("%v/team/%v/airflow/edit", server.URL, team.Slug), data) 407 if err != nil { 408 t.Error(err) 409 } 410 defer resp.Body.Close() 411 412 events, err := repo.EventsGetType(ctx, database.EventTypeUpdateAirflow) 413 if err != nil { 414 t.Error(err) 415 } 416 417 eventPayload, err := getEventForAirflow(events, team.ID) 418 if err != nil { 419 t.Error(err) 420 } 421 422 if eventPayload.TeamID == "" { 423 t.Errorf("edit airflow: no event registered for team %v", team.ID) 424 } 425 426 if eventPayload.DagRepo != newDagRepo { 427 t.Errorf("edit airflow: dag repo value, expected %v, got %v", newDagRepo, eventPayload.DagRepo) 428 } 429 430 if eventPayload.DagRepoBranch != newDagRepoBranch { 431 t.Errorf("edit airflow: dag repo branch value, expected %v, got %v", newDagRepoBranch, eventPayload.DagRepoBranch) 432 } 433 434 if strings.Join([]string{eventPayload.AirflowImage, eventPayload.AirflowTag}, ":") != customImage { 435 t.Errorf("edit airflow: custom image, expected %v, got %v", customImage, strings.Join([]string{eventPayload.AirflowImage, eventPayload.AirflowTag}, ":")) 436 } 437 }) 438 439 t.Run("delete airflow", func(t *testing.T) { 440 resp, err := server.Client().PostForm(fmt.Sprintf("%v/team/%v/airflow/delete", server.URL, team.Slug), nil) 441 if err != nil { 442 t.Error(err) 443 } 444 resp.Body.Close() 445 446 if resp.StatusCode != http.StatusOK { 447 t.Errorf("delete team: expected status code 200, got %v", resp.StatusCode) 448 } 449 450 events, err := repo.EventsGetType(ctx, database.EventTypeDeleteAirflow) 451 if err != nil { 452 t.Error(err) 453 } 454 455 if !deleteEventCreatedForTeam(events, team.ID) { 456 t.Errorf("delete airflow: no event registered for team %v", team.ID) 457 } 458 }) 459 } 460 461 func prepareChartTests(ctx context.Context, teamName string) (gensql.Team, error) { 462 team := gensql.Team{ 463 ID: teamName + "-1234", 464 Slug: teamName, 465 Users: []string{testUser.Email, "user.one@nav.no", "user.two@nav.no"}, 466 } 467 468 return team, repo.TeamCreate(ctx, team) 469 } 470 471 func getEventForJupyterhub(events []gensql.Event, team string) (chart.JupyterConfigurableValues, error) { 472 for _, event := range events { 473 payload := chart.JupyterConfigurableValues{} 474 err := json.Unmarshal(event.Payload, &payload) 475 if err != nil { 476 return chart.JupyterConfigurableValues{}, err 477 } 478 479 if payload.TeamID == team { 480 return payload, nil 481 } 482 } 483 484 return chart.JupyterConfigurableValues{}, nil 485 } 486 487 func getEventForAirflow(events []gensql.Event, team string) (chart.AirflowConfigurableValues, error) { 488 for _, event := range events { 489 payload := chart.AirflowConfigurableValues{} 490 err := json.Unmarshal(event.Payload, &payload) 491 if err != nil { 492 return chart.AirflowConfigurableValues{}, err 493 } 494 495 if payload.TeamID == team { 496 return payload, nil 497 } 498 } 499 500 return chart.AirflowConfigurableValues{}, nil 501 } 502 503 func createChartForTeam(ctx context.Context, teamID string, chartValues any, chartType gensql.ChartType) error { 504 values := reflect.ValueOf(chartValues) 505 for i := 0; i < values.NumField(); i++ { 506 key := values.Type().Field(i).Tag.Get("helm") 507 value := values.Field(i).Interface() 508 if key != "" && value != "" { 509 if err := repo.TeamChartValueInsert(ctx, key, value.(string), teamID, chartType); err != nil { 510 return err 511 } 512 } 513 } 514 515 return nil 516 }