This is an automated email from the ASF dual-hosted git repository. github-merge-queue[bot] pushed a commit to branch gh-readonly-queue/main/pr-8546-378a3b54ec645e57760011bca0e08c1829c86680 in repository https://gitbox.apache.org/repos/asf/texera.git
commit 0cd5598d2600d1af37b25a7cc20a0796b6ecad88 Author: Tanishq Gandhi <[email protected]> AuthorDate: Fri Sep 18 00:45:59 2026 +0000 feat(frontend): pick a curated image when creating a computing unit (#8546) ### What changes were proposed in this PR? The last piece of #8466: an **Image** dropdown in the create-computing-unit dialog. It lists the images that reached READY and defaults to **Default**, the deployment's own image. A user who ignores it sends no image at all, so nothing about today's behaviour changes. The dropdown appears only when an administrator has curated something, and a tooltip states the two things the issue calls out as surprising: the choice is fixed for the unit's lifetime, and it affects only that unit.   The chosen image travels as `iid` on the create request, which #8475's backend resolves to a pinned digest. **Curated images are on by default now.** `curatedImages.enabled` moves to `true` in `values.yaml` and `kubernetes.conf`. They were off pending the pages to manage and to choose them, and both have shipped. A deployment short of node disk, or unwilling to offer them, sets it back to false. The spec that asserted no unit starts while the feature is off goes with it: that path now reaches the database, which a unit test has none of, so it could no longer be exercised. Its companion now asserts the flag ships on. ### Any related issues, documentation, discussions? Closes #8471 Part of #8466 Stacked on #8470, which supplies the image list this dropdown is built from, and on #8475 beneath it. ### How was this PR tested? Three new tests, plus the whole path exercised against a real backend. | Case | What it pins | | --- | --- | | only ready images | one still being checked is not offered | | nothing preselected | the default is the deployment's own image | | feature off | the API's 503 leaves the dropdown empty and hidden | | chosen image is sent | `iid` reaches the create call | | reopening forgets it | the choice is per unit, not sticky | Two existing specs were updated: both assert the exact argument list to `createKubernetesBasedComputingUnit`, which now carries `imageId`, and both now assert it is `undefined` when no image is chosen. ``` yarn --cwd frontend format:ci clean yarn --cwd frontend run build:ci 0 errors yarn --cwd frontend run test:ci 84 passed sbt "ComputingUnitManagingService/test" "Config/test" scalafmtCheckAll 173 + 72 passed, clean ``` **Against a real stack** (`bin/local-dev.sh up`, computing-unit manager talking to minikube), driven through the dialog itself: ``` choose "AlphaFold3", Create -> pod computing-unit-5 Running -> image tagandhi19/texera-cu-alphafold3@sha256:7fc542531d90... -> securityContext {runAsNonRoot, runAsUser 1001, drop ALL, no escalation} choose "Python ML (sklearn)", Create -> image tagandhi19/texera-cu-sklearn@sha256:7e0a8816... create against a FAILED image -> 403 "Image 7 is not available. It must exist and have passed its check." ``` Each pod ran the exact digest recorded for that image, and the unit row stored its `imageName`. ### Was this PR authored or co-authored using generative AI tooling? Generated-by: Claude Code (Claude Opus 5) --- bin/k8s/values.yaml | 3 +- common/config/src/main/resources/kubernetes.conf | 3 +- .../service/resource/CuratedImageResource.scala | 32 +++++-- .../resource/CuratedImageResourceSpec.scala | 47 +++++++-- .../computing-unit-create-modal.component.html | 26 +++++ .../computing-unit-create-modal.component.scss | 3 +- .../computing-unit-create-modal.component.spec.ts | 105 ++++++++++++++++++++- .../computing-unit-create-modal.component.ts | 45 ++++++++- .../computing-unit-actions.service.spec.ts | 4 +- .../computing-unit-actions.service.ts | 5 +- .../workflow-computing-unit-managing.service.ts | 22 ++++- .../service/admin/cu-image/cu-image.service.ts | 5 + 12 files changed, 275 insertions(+), 25 deletions(-) diff --git a/bin/k8s/values.yaml b/bin/k8s/values.yaml index 85819ab006..8cd3a94d18 100644 --- a/bin/k8s/values.yaml +++ b/bin/k8s/values.yaml @@ -374,8 +374,7 @@ litellm: # Images an administrator registers, which a computing unit can then be started from. curatedImages: - # Off until the UI to manage these ships. - enabled: false + enabled: true prepull: # Pull every ready image onto every node, so the first unit there does not wait for it. # Costs node disk: each node holds each ready image. Turning it off also removes the diff --git a/common/config/src/main/resources/kubernetes.conf b/common/config/src/main/resources/kubernetes.conf index 57964fc83d..6278679a7c 100644 --- a/common/config/src/main/resources/kubernetes.conf +++ b/common/config/src/main/resources/kubernetes.conf @@ -125,8 +125,7 @@ kubernetes { } curated-images { - # Off until the UI to manage these ships. - enabled = false + enabled = true enabled = ${?TEXERA_CURATED_IMAGES_ENABLED} # Reads a manifest to check the start command and resolve the digest. No layers are diff --git a/computing-unit-managing-service/src/main/scala/org/apache/texera/service/resource/CuratedImageResource.scala b/computing-unit-managing-service/src/main/scala/org/apache/texera/service/resource/CuratedImageResource.scala index e3f7c713b5..948120966c 100644 --- a/computing-unit-managing-service/src/main/scala/org/apache/texera/service/resource/CuratedImageResource.scala +++ b/computing-unit-managing-service/src/main/scala/org/apache/texera/service/resource/CuratedImageResource.scala @@ -181,16 +181,36 @@ object CuratedImageResource extends LazyLogging { * The image a computing unit should start from, or None if it cannot be started from. * No ownership check: these are offered to every user by design. */ - def readyImageFor(iid: Int): Option[String] = { + /** + * What a unit started from this image would run, or None if it cannot be started from. + * + * Separate from the lookup so the disabled case can be stated in a test: the flag is a + * val read once at class load, so a test cannot turn the feature off around a call. + */ + private[service] def startableRef( + enabled: Boolean, + status: String, + sourceRef: String, + sourceDigest: String + ): Option[String] = // A disabled deployment starts nothing, including from a row left behind by an // earlier enabled run. + if (!enabled) None + else if (status != Status.Ready) None + else pinnedRefOf(sourceRef, sourceDigest) + + def readyImageFor(iid: Int): Option[String] = { + // Checked here too, which the rule below repeats: it makes the query pointless. if (!CuratedImageConfig.enabled) return None - val record = Option( + Option( context.select(STATUS, SOURCE_REF, SOURCE_DIGEST).from(CU_IMAGE).where(IID.eq(iid)).fetchOne() - ) - record.flatMap { r => - if (r.get(STATUS) != Status.Ready) None - else pinnedRefOf(r.get(SOURCE_REF), r.get(SOURCE_DIGEST)) + ).flatMap { r => + startableRef( + CuratedImageConfig.enabled, + r.get(STATUS), + r.get(SOURCE_REF), + r.get(SOURCE_DIGEST) + ) } } diff --git a/computing-unit-managing-service/src/test/scala/org/apache/texera/service/resource/CuratedImageResourceSpec.scala b/computing-unit-managing-service/src/test/scala/org/apache/texera/service/resource/CuratedImageResourceSpec.scala index 3508cd09d4..69eb095e13 100644 --- a/computing-unit-managing-service/src/test/scala/org/apache/texera/service/resource/CuratedImageResourceSpec.scala +++ b/computing-unit-managing-service/src/test/scala/org/apache/texera/service/resource/CuratedImageResourceSpec.scala @@ -282,8 +282,6 @@ class CuratedImageResourceSpec extends AnyFlatSpec with Matchers { ImageValidationClient.sourceDigestFrom(log) shouldBe None } - // Off until the UI ships, so a deployment that has not opted in starts no unit from a - // curated image -- including from a row left behind if it was enabled and turned off. // The regression this guards: the digest was read from the first marker line, while the // image's own start command -- which its author controls -- is echoed earlier. An image // whose Cmd carries a newline and a marker of its own could pass the check and still @@ -301,12 +299,49 @@ class CuratedImageResourceSpec extends AnyFlatSpec with Matchers { "sha256:1111111111111111111111111111111111111111111111111111111111111111" } - "the feature flag" should "be off unless a deployment turns it on" in { - CuratedImageConfig.enabled shouldBe false + private val ReadyDigest = "sha256:" + "a" * 64 + + // The guard this covers: a deployment that turned the feature off, or left a row behind + // from when it was on, must start no unit from a curated image. It was covered by a test + // that drove readyImageFor while the flag was off, which stopped being possible once the + // flag shipped on -- the flag is read once at class load, so a test cannot turn it off. + "an image" should "start nothing while the feature is off, ready or not" in { + CuratedImageResource.startableRef( + enabled = false, + status = "READY", + sourceRef = "owner/name:1.0", + sourceDigest = ReadyDigest + ) shouldBe None + } + + it should "start nothing until its check has passed" in { + Seq("PENDING", "VALIDATING", "FAILED").foreach { status => + withClue(s"$status: ") { + CuratedImageResource.startableRef( + enabled = true, + status = status, + sourceRef = "owner/name:1.0", + sourceDigest = ReadyDigest + ) shouldBe None + } + } + } + + it should "run the digest its check resolved once it is ready" in { + CuratedImageResource + .startableRef( + enabled = true, + status = "READY", + sourceRef = "owner/name:1.0", + sourceDigest = ReadyDigest + ) + .value shouldBe s"owner/name@$ReadyDigest" } - it should "start no unit from a curated image while it is off" in { - CuratedImageResource.readyImageFor(1) shouldBe None + // On by default now that the pages to manage and choose images have shipped. A + // deployment short of node disk, or unwilling to offer them, sets it back to false. + "the feature flag" should "be on unless a deployment turns it off" in { + CuratedImageConfig.enabled shouldBe true } // The states below are the ones a real cluster produces; the DeadlineExceeded shape was diff --git a/frontend/src/app/common/component/computing-unit-create-modal/computing-unit-create-modal.component.html b/frontend/src/app/common/component/computing-unit-create-modal/computing-unit-create-modal.component.html index 1ab9f0bd18..9ec733d03b 100644 --- a/frontend/src/app/common/component/computing-unit-create-modal/computing-unit-create-modal.component.html +++ b/frontend/src/app/common/component/computing-unit-create-modal/computing-unit-create-modal.component.html @@ -78,6 +78,32 @@ </nz-select> </div> + <!-- Only when an administrator has curated something to choose. --> + <div + *ngIf="curatedImages.length > 0" + class="select-unit"> + <span + >Image + <span + nz-icon + nzType="info-circle" + nz-tooltip="Chosen when the unit is created and fixed for its lifetime. Affects only this unit."></span> + </span> + <nz-select + class="image-selection" + [(ngModel)]="selectedImageId"> + <nz-option + [nzValue]="DEPLOYMENT_IMAGE" + nzLabel="Default"> + </nz-option> + <nz-option + *ngFor="let img of curatedImages" + [nzValue]="img.iid" + [nzLabel]="img.name"> + </nz-option> + </nz-select> + </div> + <div *ngIf="showGpuSelection()" class="select-unit"> diff --git a/frontend/src/app/common/component/computing-unit-create-modal/computing-unit-create-modal.component.scss b/frontend/src/app/common/component/computing-unit-create-modal/computing-unit-create-modal.component.scss index d519455a90..c326c91999 100644 --- a/frontend/src/app/common/component/computing-unit-create-modal/computing-unit-create-modal.component.scss +++ b/frontend/src/app/common/component/computing-unit-create-modal/computing-unit-create-modal.component.scss @@ -19,7 +19,8 @@ .memory-selection, .cpu-selection, -.gpu-selection { +.gpu-selection, +.image-selection { width: 100%; } diff --git a/frontend/src/app/common/component/computing-unit-create-modal/computing-unit-create-modal.component.spec.ts b/frontend/src/app/common/component/computing-unit-create-modal/computing-unit-create-modal.component.spec.ts index dd4eaf0231..5760e7d6c9 100644 --- a/frontend/src/app/common/component/computing-unit-create-modal/computing-unit-create-modal.component.spec.ts +++ b/frontend/src/app/common/component/computing-unit-create-modal/computing-unit-create-modal.component.spec.ts @@ -21,10 +21,12 @@ import { ApplicationRef, DebugElement, getDebugNode, SimpleChange } from "@angul import { NgModel } from "@angular/forms"; import { CdkVirtualScrollViewport } from "@angular/cdk/scrolling"; import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { CuImage, CuImageService } from "../../../dashboard/service/admin/cu-image/cu-image.service"; import { HttpClientTestingModule } from "@angular/common/http/testing"; import { NzModalService } from "ng-zorro-antd/modal"; import { NoopAnimationsModule } from "@angular/platform-browser/animations"; import { of, throwError } from "rxjs"; +import { HttpErrorResponse } from "@angular/common/http"; import type { Mocked } from "vitest"; import { ComputingUnitCreateModalComponent } from "./computing-unit-create-modal.component"; import { WorkflowComputingUnitManagingService } from "../../service/computing-unit/workflow-computing-unit/workflow-computing-unit-managing.service"; @@ -38,12 +40,29 @@ import { buildLocalComputingUnitUri, getJvmMemorySliderConfig } from "../../util describe("ComputingUnitCreateModalComponent", () => { let component: ComputingUnitCreateModalComponent; let fixture: ComponentFixture<ComputingUnitCreateModalComponent>; + const curatedImage = (over: Partial<CuImage> = {}): CuImage => ({ + iid: 1, + name: "Python ML", + sourceRef: "owner/name:1", + sourceDigest: "sha256:abc", + status: "READY", + imageTag: "owner/name@sha256:abc", + attempt: 1, + creationTime: 0, + updateTime: 0, + ...over, + }); + const mockCuImageService = { list: vi.fn() }; let mockComputingUnitService: Mocked<WorkflowComputingUnitManagingService>; let mockNotificationService: Mocked<NotificationService>; const createdUnit = { computingUnit: { cuid: 42 } } as unknown as DashboardWorkflowComputingUnit; beforeEach(async () => { + // No curated images by default, so the existing tests see today's behaviour. Cleared + // as well as stubbed: the mock is shared, so call counts would carry between tests. + mockCuImageService.list.mockClear(); + mockCuImageService.list.mockReturnValue(of([])); mockComputingUnitService = { getComputingUnitTypes: vi.fn(), getComputingUnitLimitOptions: vi.fn(), @@ -65,6 +84,7 @@ describe("ComputingUnitCreateModalComponent", () => { await TestBed.configureTestingModule({ providers: [ + { provide: CuImageService, useValue: mockCuImageService }, // The real NzModalService is required here: the declarative <nz-modal> // in this component's template delegates opening to NzModalService.create(), // so a stub breaks every test that renders the modal open. @@ -81,6 +101,87 @@ describe("ComputingUnitCreateModalComponent", () => { component = fixture.componentInstance; }); + /** Both hosts render this modal always, so the list is read when it opens, not on init. */ + const openDialog = () => { + fixture.detectChanges(); + component.visible = true; + component.ngOnChanges({ visible: { currentValue: true } as never }); + fixture.detectChanges(); + }; + + it("offers only ready images, and none when there are none", () => { + mockCuImageService.list.mockReturnValue( + of([curatedImage({ iid: 1 }), curatedImage({ iid: 2, name: "Still checking", status: "VALIDATING" })]) + ); + openDialog(); + // A unit cannot start from an image that has not passed its check. + expect(component.curatedImages.map(i => i.iid)).toEqual([1]); + // Nothing is preselected: the default is the deployment's own image. + expect(component.selectedImageId).toBe(component.DEPLOYMENT_IMAGE); + }); + + // A 503 means the feature is off and is expected; anything else means the picker + // vanished for a reason the user should hear about. + it("says so when the images cannot be loaded for a real reason", () => { + mockCuImageService.list.mockReturnValue( + throwError(() => new HttpErrorResponse({ status: 500, statusText: "Server Error" })) + ); + openDialog(); + expect(component.curatedImages).toEqual([]); + expect(mockNotificationService.error).toHaveBeenCalled(); + }); + + it("shows no images when the deployment has the feature switched off", () => { + // The API answers 503 there. The dropdown is hidden and nothing else changes. + mockCuImageService.list.mockReturnValue( + throwError(() => new HttpErrorResponse({ status: 503, statusText: "Service Unavailable" })) + ); + openDialog(); + expect(component.curatedImages).toEqual([]); + // Expected on such a deployment, so it is not reported. + expect(mockNotificationService.error).not.toHaveBeenCalled(); + }); + + it("sends the chosen image, and forgets it when the modal reopens", () => { + mockCuImageService.list.mockReturnValue(of([curatedImage({ iid: 7 })])); + mockComputingUnitService.getComputingUnitTypes.mockReturnValue( + of({ typeOptions: ["kubernetes"] as WorkflowComputingUnitType[] }) + ); + mockComputingUnitService.createKubernetesBasedComputingUnit.mockReturnValue(of(createdUnit)); + openDialog(); + + component.newComputingUnitName = "On a curated image"; + component.selectedImageId = 7; + component.startComputingUnit(); + expect(mockComputingUnitService.createKubernetesBasedComputingUnit).toHaveBeenCalledWith( + "On a curated image", + expect.anything(), + expect.anything(), + expect.anything(), + expect.anything(), + expect.anything(), + 7 + ); + + // The image is chosen per unit, so the next one must not inherit it. + component.visible = true; + component.ngOnChanges({ visible: { currentValue: true } as never }); + expect(component.selectedImageId).toBe(component.DEPLOYMENT_IMAGE); + }); + + // The regression this guards: read in ngOnInit, which runs at page load because neither + // host wraps this modal in an *ngIf, an image that became ready since then never showed. + it("re-reads the images each time the dialog opens", () => { + mockCuImageService.list.mockReturnValue(of([])); + fixture.detectChanges(); + expect(mockCuImageService.list).not.toHaveBeenCalled(); + + mockCuImageService.list.mockReturnValue(of([curatedImage({ iid: 9, name: "Ready later" })])); + component.visible = true; + component.ngOnChanges({ visible: { currentValue: true } as never }); + expect(component.curatedImages.map(i => i.iid)).toEqual([9]); + }); + it("should create", () => { fixture.detectChanges(); expect(component).toBeTruthy(); @@ -180,7 +281,9 @@ describe("ComputingUnitCreateModalComponent", () => { "4Gi", "0", "2G", - "128Mi" + "128Mi", + // No curated image chosen, so the unit runs the deployment's own. + undefined ); expect(mockNotificationService.success).toHaveBeenCalledWith("Successfully created the new compute unit"); expect(unitCreatedSpy).toHaveBeenCalledWith(createdUnit); diff --git a/frontend/src/app/common/component/computing-unit-create-modal/computing-unit-create-modal.component.ts b/frontend/src/app/common/component/computing-unit-create-modal/computing-unit-create-modal.component.ts index 5e34bfd470..acd5152008 100644 --- a/frontend/src/app/common/component/computing-unit-create-modal/computing-unit-create-modal.component.ts +++ b/frontend/src/app/common/component/computing-unit-create-modal/computing-unit-create-modal.component.ts @@ -36,6 +36,8 @@ import { ComputingUnitActionsService } from "../../service/computing-unit/comput import { NotificationService } from "../../service/notification/notification.service"; import { DashboardWorkflowComputingUnit, WorkflowComputingUnitType } from "../../type/workflow-computing-unit"; import { extractErrorMessage } from "../../util/error"; +import { HttpErrorResponse } from "@angular/common/http"; +import { CuImage, CuImageService, isStartable } from "../../../dashboard/service/admin/cu-image/cu-image.service"; import { buildLocalComputingUnitUri, getJvmMemorySliderConfig, @@ -49,6 +51,9 @@ import { const DEFAULT_SHM_SIZE_VALUE = 64; const DEFAULT_SHM_SIZE_UNIT: "Mi" | "Gi" = "Mi"; +/** The deployment's own image. Never a real iid, which the database numbers from 1. */ +const DEPLOYMENT_IMAGE = 0; + /** * The "create computing unit" modal shared by the workspace power button and * the dashboard Computing Units page. Owns the whole form (type, name, @@ -115,6 +120,12 @@ export class ComputingUnitCreateModalComponent implements OnInit, OnChanges { }; // cpu&memory limit options from backend + /** Ready curated images. Empty when none are registered, or the feature is off. */ + curatedImages: CuImage[] = []; + /** DEPLOYMENT_IMAGE means the deployment's own image, which is the default. */ + selectedImageId: number = DEPLOYMENT_IMAGE; + readonly DEPLOYMENT_IMAGE = DEPLOYMENT_IMAGE; + cpuOptions: string[] = []; memoryOptions: string[] = []; gpuOptions: string[] = []; // Add GPU options array @@ -122,7 +133,8 @@ export class ComputingUnitCreateModalComponent implements OnInit, OnChanges { constructor( private computingUnitService: WorkflowComputingUnitManagingService, private notificationService: NotificationService, - private computingUnitActionsService: ComputingUnitActionsService + private computingUnitActionsService: ComputingUnitActionsService, + private cuImageService: CuImageService ) {} ngOnInit(): void { @@ -171,15 +183,44 @@ export class ComputingUnitCreateModalComponent implements OnInit, OnChanges { ngOnChanges(changes: SimpleChanges): void { if (changes["visible"]?.currentValue === true) { this.resetAdvancedSettings(); + this.loadCuratedImages(); } } + /** + * Read when the dialog opens rather than in ngOnInit: both hosts render this component + * unconditionally, so ngOnInit runs once at page load. An image that became ready since + * then would never appear, and one failed read would hide the field for the session. + * + * Readable by any signed-in user. A deployment with curated images off answers 503, and + * a user who never sees the dropdown gets exactly today's behaviour. + */ + private loadCuratedImages(): void { + this.cuImageService + .list() + .pipe(untilDestroyed(this)) + .subscribe({ + next: images => (this.curatedImages = images.filter(isStartable)), + error: (err: unknown) => { + // The field goes either way -- without a list there is nothing to choose from, + // and the unit falls back to the deployment's image. But only 503 means the + // feature is off; anything else is worth saying, or the picker just vanishes. + this.curatedImages = []; + if (!(err instanceof HttpErrorResponse && err.status === 503)) { + this.notificationService.error(`Could not load the available images: ${extractErrorMessage(err)}`); + } + }, + }); + } + // Runs every time the modal opens: re-collapse the advanced panel so it // always starts on the simple path (name / RAM / CPU), and restore the // advanced values so a setting from a previous open can't ride along // hidden behind the collapsed panel. private resetAdvancedSettings(): void { this.showAdvancedSettings = false; + // The image is chosen per unit, so a previous choice must not ride along. + this.selectedImageId = DEPLOYMENT_IMAGE; this.shmSizeValue = DEFAULT_SHM_SIZE_VALUE; this.shmSizeUnit = DEFAULT_SHM_SIZE_UNIT; this.resetJvmMemorySlider(); @@ -262,6 +303,8 @@ export class ComputingUnitCreateModalComponent implements OnInit, OnChanges { jvmMemorySize: this.selectedJvmMemorySize, shmSize: `${this.shmSizeValue}${this.shmSizeUnit}`, localUri: this.localComputingUnitUri, + // Left out for the deployment's own image, so the request carries no image at all. + imageId: this.selectedImageId === DEPLOYMENT_IMAGE ? undefined : this.selectedImageId, }; this.computingUnitActionsService diff --git a/frontend/src/app/common/service/computing-unit/computing-unit-actions/computing-unit-actions.service.spec.ts b/frontend/src/app/common/service/computing-unit/computing-unit-actions/computing-unit-actions.service.spec.ts index d31ce59095..94fd340d3a 100644 --- a/frontend/src/app/common/service/computing-unit/computing-unit-actions/computing-unit-actions.service.spec.ts +++ b/frontend/src/app/common/service/computing-unit/computing-unit-actions/computing-unit-actions.service.spec.ts @@ -81,7 +81,9 @@ describe("ComputingUnitActionsService", () => { "4G", "1", "1G", - "64M" + "64M", + // No curated image chosen, so the unit runs the deployment's own. + undefined ); expect(computingUnitService.createLocalComputingUnit).not.toHaveBeenCalled(); }); diff --git a/frontend/src/app/common/service/computing-unit/computing-unit-actions/computing-unit-actions.service.ts b/frontend/src/app/common/service/computing-unit/computing-unit-actions/computing-unit-actions.service.ts index 6ac8051dab..2b67a8b2ba 100644 --- a/frontend/src/app/common/service/computing-unit/computing-unit-actions/computing-unit-actions.service.ts +++ b/frontend/src/app/common/service/computing-unit/computing-unit-actions/computing-unit-actions.service.ts @@ -37,6 +37,8 @@ export interface StartComputingUnitRequest { jvmMemorySize: string; shmSize: string; localUri: string; + /** A curated image to start from. Absent means the deployment's own image. */ + imageId?: number; } @Injectable({ @@ -74,7 +76,8 @@ export class ComputingUnitActionsService { request.memory, request.gpu, request.jvmMemorySize, - request.shmSize + request.shmSize, + request.imageId ); } diff --git a/frontend/src/app/common/service/computing-unit/workflow-computing-unit/workflow-computing-unit-managing.service.ts b/frontend/src/app/common/service/computing-unit/workflow-computing-unit/workflow-computing-unit-managing.service.ts index 58768bb346..8c683ff440 100644 --- a/frontend/src/app/common/service/computing-unit/workflow-computing-unit/workflow-computing-unit-managing.service.ts +++ b/frontend/src/app/common/service/computing-unit/workflow-computing-unit/workflow-computing-unit-managing.service.ts @@ -84,9 +84,12 @@ export class WorkflowComputingUnitManagingService { jvmMemorySize: string, shmSize: string, uri: string, - unitType: "kubernetes" | "local" + unitType: "kubernetes" | "local", + iid?: number ): Observable<DashboardWorkflowComputingUnit> { - const body = { name, cpuLimit, memoryLimit, gpuLimit, jvmMemorySize, shmSize, uri, unitType }; + // iid is left out when no curated image was chosen, so the unit runs the deployment's + // own image exactly as before. + const body = { name, cpuLimit, memoryLimit, gpuLimit, jvmMemorySize, shmSize, uri, unitType, iid }; return this.http .post<DashboardWorkflowComputingUnit>(`${AppSettings.getApiEndpoint()}/${COMPUTING_UNIT_CREATE_URL}`, body) @@ -110,9 +113,20 @@ export class WorkflowComputingUnitManagingService { memoryLimit: string, gpuLimit: string, jvmMemorySize: string, - shmSize: string + shmSize: string, + iid?: number ): Observable<DashboardWorkflowComputingUnit> { - return this.createComputingUnit(name, cpuLimit, memoryLimit, gpuLimit, jvmMemorySize, shmSize, "", "kubernetes"); + return this.createComputingUnit( + name, + cpuLimit, + memoryLimit, + gpuLimit, + jvmMemorySize, + shmSize, + "", + "kubernetes", + iid + ); } /** diff --git a/frontend/src/app/dashboard/service/admin/cu-image/cu-image.service.ts b/frontend/src/app/dashboard/service/admin/cu-image/cu-image.service.ts index 07b42708f9..cc4758328d 100644 --- a/frontend/src/app/dashboard/service/admin/cu-image/cu-image.service.ts +++ b/frontend/src/app/dashboard/service/admin/cu-image/cu-image.service.ts @@ -51,6 +51,11 @@ export interface CuImageValidationLog { log: string; } +/** Only a READY image can back a computing unit. */ +export function isStartable(image: CuImage): boolean { + return image.status === "READY"; +} + /** Whether the list is worth polling. */ export function isInProgress(image: CuImage): boolean { return image.status === "PENDING" || image.status === "VALIDATING";
