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 create dialog with the Image
    
field](https://raw.githubusercontent.com/tanishqgandhi1908/texera/assets/cu-image-admin-page/screenshots/picker-1-dialog.png)
    
    ![The dropdown open, listing the ready
    
images](https://raw.githubusercontent.com/tanishqgandhi1908/texera/assets/cu-image-admin-page/screenshots/picker-2-options.png)
    
    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";

Reply via email to