package service import ( "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "time" ) func TestFlashOnlyWorkflowRequiresPublishedArtifact(t *testing.T) { kube := remoteWorkflowKubeServer(t, remoteKubeOptions{}) installKubeFactory(t, kube) app := remoteTestApp(t, nil) if _, err := app.Flash("titan-15", "titan-22", "/dev/sdz"); err == nil || !strings.Contains(err.Error(), "run a build first") { t.Fatalf("expected missing artifact error, got %v", err) } } func TestFlashOnlyWorkflowCompletesAndRecordsVerification(t *testing.T) { kube := remoteWorkflowKubeServer(t, remoteKubeOptions{flashMessage: `{"dest_path":"/dev/sdz","verified":true,"verification_kind":"block-device","verification_summary":"Verified /dev/sdz; system-boot and writable are present and the boot files look correct.","boot_partition":"/dev/sdz1","root_partition":"/dev/sdz2","boot_label":"system-boot","root_label":"writable","boot_fstype":"vfat","root_fstype":"ext4","checked_files":["config.txt","cmdline.txt","boot.scr"]}`}) installKubeFactory(t, kube) app := remoteTestApp(t, nil) app.artifactStore["titan-15"] = ArtifactSummary{Node: "titan-15", Ref: "registry.example/metis/titan-15:latest"} job, err := app.Flash("titan-15", "titan-22", "/dev/sdz") if err != nil { t.Fatalf("Flash: %v", err) } waitForJobState(t, app, job.ID, JobDone) got := app.job(job.ID) if got == nil || got.Kind != "flash" || got.Status != JobDone { t.Fatalf("unexpected flash job: %#v", got) } if !strings.Contains(got.Message, "Move the card into titan-15") { t.Fatalf("unexpected completion message: %#v", got) } if got.ProgressPct != 100 { t.Fatalf("expected complete flash progress, got %#v", got) } events := app.recentEvents(5) if len(events) == 0 || events[0].Kind != "image.flash" { t.Fatalf("expected flash event, got %#v", events) } if verified, ok := events[0].Details["verified"].(bool); !ok || !verified { t.Fatalf("expected verified flash details, got %#v", events[0].Details) } if kind, ok := events[0].Details["verification_kind"].(string); !ok || kind != "block-device" { t.Fatalf("expected block-device verification, got %#v", events[0].Details) } if bootPartition, ok := events[0].Details["boot_partition"].(string); !ok || bootPartition != "/dev/sdz1" { t.Fatalf("expected boot partition details, got %#v", events[0].Details) } } func TestHandleFlashRouteStartsFlashOnlyJob(t *testing.T) { kube := remoteWorkflowKubeServer(t, remoteKubeOptions{}) installKubeFactory(t, kube) app := remoteTestApp(t, nil) app.artifactStore["titan-15"] = ArtifactSummary{Node: "titan-15", Ref: "registry.example/metis/titan-15:latest"} handler := app.Handler() req := httptest.NewRequest(http.MethodPost, "/api/jobs/flash", strings.NewReader(`{"node":"titan-15","host":"titan-22","device":"/dev/sdz"}`)) req.Header.Set("Content-Type", "application/json") req.Header.Set("X-Auth-Request-User", "brad") req.Header.Set("X-Auth-Request-Groups", "admin") resp := httptest.NewRecorder() handler.ServeHTTP(resp, req) if resp.Code != http.StatusAccepted { t.Fatalf("flash route response: %d %s", resp.Code, resp.Body.String()) } var job Job if err := json.Unmarshal(resp.Body.Bytes(), &job); err != nil { t.Fatalf("decode flash job: %v", err) } waitForJobState(t, app, job.ID, JobDone) if got := app.job(job.ID); got == nil || got.Kind != "flash" { t.Fatalf("expected flash job, got %#v", got) } } func TestFlashAndReplaceDefaultHostBranches(t *testing.T) { kube := remoteWorkflowKubeServer(t, remoteKubeOptions{}) harbor := fakeHarborServer(t, true) installKubeFactory(t, kube) app := remoteTestApp(t, harbor) app.artifactStore["titan-15"] = ArtifactSummary{Node: "titan-15", Ref: "registry.example/metis/titan-15:latest"} flashJob, err := app.Flash("titan-15", "", hostTmpDevicePath) if err != nil { t.Fatalf("Flash default host: %v", err) } waitForJobState(t, app, flashJob.ID, JobDone) if got := app.job(flashJob.ID); got == nil || got.Host != "titan-22" || !strings.Contains(got.Message, "host /tmp") { t.Fatalf("unexpected flash default host job: %#v", got) } replaceJob, err := app.Replace("titan-15", "", hostTmpDevicePath) if err != nil { t.Fatalf("Replace default host: %v", err) } waitForJobState(t, app, replaceJob.ID, JobDone) if got := app.job(replaceJob.ID); got == nil || got.Host != "titan-22" { t.Fatalf("unexpected replace default host job: %#v", got) } } func TestFlashRejectsDuplicateActiveNodeJob(t *testing.T) { kube := remoteWorkflowKubeServer(t, remoteKubeOptions{}) installKubeFactory(t, kube) app := remoteTestApp(t, nil) app.artifactStore["titan-15"] = ArtifactSummary{Node: "titan-15", Ref: "registry.example/metis/titan-15:latest"} app.newJob("flash", "titan-15", "titan-22", "/dev/sdz") if _, err := app.Flash("titan-15", "titan-22", "/dev/sdz"); err == nil || !strings.Contains(err.Error(), "already has an active") { t.Fatalf("expected flash conflict, got %v", err) } } func TestFlashArtifactUpdatesProgressWhenVerificationSummaryIsMissing(t *testing.T) { kube := remoteWorkflowKubeServer(t, remoteKubeOptions{flashMessage: `{"dest_path":"/dev/sdz","size_bytes":4096,"verified":true}`}) installKubeFactory(t, kube) app := remoteTestApp(t, nil) job := app.newJob("flash", "titan-15", "titan-22", "/dev/sdz") result, err := app.flashArtifact(job.ID, "registry.example/metis/titan-15") if err != nil { t.Fatalf("flashArtifact: %v", err) } if result.SizeBytes != 4096 { t.Fatalf("unexpected flash result: %#v", result) } got := app.job(job.ID) if got == nil || got.Written != 4096 || got.Total != 4096 || !strings.Contains(got.Message, "Verified the latest image write") { t.Fatalf("unexpected flash artifact state: %#v", got) } } func TestFlashOnlyWorkflowFailsVerification(t *testing.T) { kube := remoteWorkflowKubeServer(t, remoteKubeOptions{flashMessage: `{"dest_path":"/dev/sdz","verified":false}`}) installKubeFactory(t, kube) app := remoteTestApp(t, nil) app.artifactStore["titan-15"] = ArtifactSummary{Node: "titan-15", Ref: "registry.example/metis/titan-15:latest"} job, err := app.Flash("titan-15", "titan-22", "/dev/sdz") if err != nil { t.Fatalf("Flash: %v", err) } deadline := time.Now().Add(5 * time.Second) for time.Now().Before(deadline) { if got := app.job(job.ID); got != nil && got.Status == JobError { if !strings.Contains(got.Error, "verification did not succeed") { t.Fatalf("unexpected flash verification error: %#v", got) } return } time.Sleep(10 * time.Millisecond) } t.Fatalf("flash job %s never failed verification: %#v", job.ID, app.job(job.ID)) } func TestBuildFlashAndReplaceRejectInvalidInputs(t *testing.T) { kube := remoteWorkflowKubeServer(t, remoteKubeOptions{}) installKubeFactory(t, kube) app := remoteTestApp(t, nil) app.artifactStore["titan-15"] = ArtifactSummary{Node: "titan-15", Ref: "registry.example/metis/titan-15:latest"} if _, err := app.Build("missing-node"); err == nil || !strings.Contains(err.Error(), "missing-node") { t.Fatalf("expected build inventory error, got %v", err) } if _, err := app.Flash("titan-15", "titan-22", "/dev/sda"); err == nil || !strings.Contains(err.Error(), "not a current flash candidate") { t.Fatalf("expected flash device validation error, got %v", err) } if _, err := app.Replace("titan-15", "titan-22", "/dev/sda"); err == nil || !strings.Contains(err.Error(), "not a current flash candidate") { t.Fatalf("expected replace device validation error, got %v", err) } }