From 05ae9e1e7091a7a467cf03e12bfe4a6ca2b89b1e Mon Sep 17 00:00:00 2001 From: Flora Thiebaut Date: Tue, 4 Nov 2025 11:13:01 +0100 Subject: [PATCH 1/5] feat: add support for building linux/arm64 images from code --- client/scripts/update_api_spec.js | 2 +- .../api/sessionLaunchersV2.generated-api.ts | 4 + .../api/sessionLaunchersV2.openapi.json | 19 +++ .../SessionForm/BuilderAdvancedSettings.tsx | 130 ++++++++++++++++++ .../SessionForm/BuilderEnvironmentFields.tsx | 2 + .../SessionModals/NewSessionLauncherModal.tsx | 1 + .../features/sessionsV2/session.constants.tsx | 15 ++ .../src/features/sessionsV2/session.utils.ts | 8 ++ .../features/sessionsV2/sessionsV2.types.ts | 5 +- 9 files changed, 184 insertions(+), 2 deletions(-) create mode 100644 client/src/features/sessionsV2/components/SessionForm/BuilderAdvancedSettings.tsx diff --git a/client/scripts/update_api_spec.js b/client/scripts/update_api_spec.js index 5c4786f02a..5561c10f82 100644 --- a/client/scripts/update_api_spec.js +++ b/client/scripts/update_api_spec.js @@ -24,7 +24,7 @@ import { parseDocument } from "yaml"; const GH_BASE_URL = "https://raw.githubusercontent.com"; const DATA_SERVICES_REPO = "SwissDataScienceCenter/renku-data-services"; -const DATA_SERVICES_RELEASE = "main"; +const DATA_SERVICES_RELEASE = "leafty/configure-arm-builds"; async function main() { argv.forEach((arg) => { diff --git a/client/src/features/sessionsV2/api/sessionLaunchersV2.generated-api.ts b/client/src/features/sessionsV2/api/sessionLaunchersV2.generated-api.ts index 0a34981ae6..efe337c240 100644 --- a/client/src/features/sessionsV2/api/sessionLaunchersV2.generated-api.ts +++ b/client/src/features/sessionsV2/api/sessionLaunchersV2.generated-api.ts @@ -315,12 +315,15 @@ export type EnvironmentWithImageGet = Environment & { environment_kind: EnvironmentKind; }; export type Repository = string; +export type BuildPlatform = string; +export type BuildPlatforms = BuildPlatform[]; export type BuilderVariant = string; export type FrontendVariant = string; export type RepositoryRevision = string; export type BuildContextDir = string; export type BuildParameters = { repository: Repository; + platforms?: BuildPlatforms; builder_variant: BuilderVariant; frontend_variant: FrontendVariant; repository_revision?: RepositoryRevision; @@ -385,6 +388,7 @@ export type RepositoryRevisionPatch = string; export type BuildContextDirPatch = string; export type BuildParametersPatch = { repository?: Repository; + platforms?: BuildPlatforms; builder_variant?: BuilderVariant; frontend_variant?: FrontendVariant; repository_revision?: RepositoryRevisionPatch; diff --git a/client/src/features/sessionsV2/api/sessionLaunchersV2.openapi.json b/client/src/features/sessionsV2/api/sessionLaunchersV2.openapi.json index da13f94ff9..8b75015a69 100644 --- a/client/src/features/sessionsV2/api/sessionLaunchersV2.openapi.json +++ b/client/src/features/sessionsV2/api/sessionLaunchersV2.openapi.json @@ -1006,6 +1006,19 @@ "maxLength": 99, "example": "My Renku Session :)" }, + "BuildPlatforms": { + "description": "The target runtime platforms of a session environment.", + "type": "array", + "items": { + "$ref": "#/components/schemas/BuildPlatform" + } + }, + "BuildPlatform": { + "description": "A runtime platform, e.g. \"linux/amd64\".", + "type": "string", + "minLength": 1, + "maxLength": 99 + }, "BuilderVariant": { "description": "Type of virtual environment manager when building custom environments.", "type": "string", @@ -1106,6 +1119,9 @@ "repository": { "$ref": "#/components/schemas/Repository" }, + "platforms": { + "$ref": "#/components/schemas/BuildPlatforms" + }, "builder_variant": { "$ref": "#/components/schemas/BuilderVariant" }, @@ -1144,6 +1160,9 @@ "repository": { "$ref": "#/components/schemas/Repository" }, + "platforms": { + "$ref": "#/components/schemas/BuildPlatforms" + }, "builder_variant": { "$ref": "#/components/schemas/BuilderVariant" }, diff --git a/client/src/features/sessionsV2/components/SessionForm/BuilderAdvancedSettings.tsx b/client/src/features/sessionsV2/components/SessionForm/BuilderAdvancedSettings.tsx new file mode 100644 index 0000000000..a2875ab4bf --- /dev/null +++ b/client/src/features/sessionsV2/components/SessionForm/BuilderAdvancedSettings.tsx @@ -0,0 +1,130 @@ +/*! + * Copyright 2025 - Swiss Data Science Center (SDSC) + * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and + * Eidgenössische Technische Hochschule Zürich (ETHZ). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import ChevronFlippedIcon from "~/components/icons/ChevronFlippedIcon"; +import cx from "classnames"; +import { useCallback, useMemo, useState } from "react"; +import { + Controller, + type Control, + type FieldValues, + type UseControllerProps, +} from "react-hook-form"; +import { Collapse, Label } from "reactstrap"; + +import { BUILDER_PLATFORMS } from "../../session.constants"; +import type { SessionLauncherForm } from "../../sessionsV2.types"; +import BuilderSelectorCommon from "./BuilderSelectorCommon"; + +interface BuilderAdvancedSettingsProps { + control: Control; +} + +export default function BuilderAdvancedSettings({ + control, +}: BuilderAdvancedSettingsProps) { + const [isOpen, setIsOpen] = useState(false); + const toggleIsOpen = useCallback( + () => setIsOpen((isAdvancedSettingOpen) => !isAdvancedSettingOpen), + [] + ); + return ( +
+ + +
+ +
+
+
+ ); +} + +interface BuilderPlatformSelectorProps + extends UseControllerProps {} + +function BuilderPlatformSelector({ + ...controllerProps +}: BuilderPlatformSelectorProps) { + const defaultValue = useMemo( + () => + controllerProps.defaultValue + ? controllerProps.defaultValue + : BUILDER_PLATFORMS[0], + [controllerProps.defaultValue] + ); + + return ( +
+ + ( + <> +
+ +
+
+ {error?.message ? ( + <>{error.message} + ) : ( + <>Please select a valid platform. + )} +
+ + )} + rules={ + controllerProps.rules ?? { + required: "Please select a platform.", + } + } + /> +
+ ); +} diff --git a/client/src/features/sessionsV2/components/SessionForm/BuilderEnvironmentFields.tsx b/client/src/features/sessionsV2/components/SessionForm/BuilderEnvironmentFields.tsx index b7dc4d95fa..0dd6ea7f1c 100644 --- a/client/src/features/sessionsV2/components/SessionForm/BuilderEnvironmentFields.tsx +++ b/client/src/features/sessionsV2/components/SessionForm/BuilderEnvironmentFields.tsx @@ -29,6 +29,7 @@ import { DEFAULT_APP_PARAMS } from "../../../../utils/context/appParams.constant import { useProject } from "../../../ProjectPageV2/ProjectPageContainer/ProjectPageContainer"; import { useGetRepositoriesProbesQuery } from "../../../repositories/repositories.api"; import type { SessionLauncherForm } from "../../sessionsV2.types"; +import BuilderAdvancedSettings from "./BuilderAdvancedSettings"; import BuilderFrontendSelector from "./BuilderFrontendSelector"; import BuilderTypeSelector from "./BuilderTypeSelector"; import CodeRepositoryAdvancedSettings from "./CodeRepositoryAdvancedSettings"; @@ -104,6 +105,7 @@ export default function BuilderEnvironmentFields({ + ); diff --git a/client/src/features/sessionsV2/components/SessionModals/NewSessionLauncherModal.tsx b/client/src/features/sessionsV2/components/SessionModals/NewSessionLauncherModal.tsx index 3e4479942c..24594271ed 100644 --- a/client/src/features/sessionsV2/components/SessionModals/NewSessionLauncherModal.tsx +++ b/client/src/features/sessionsV2/components/SessionModals/NewSessionLauncherModal.tsx @@ -72,6 +72,7 @@ export default function NewSessionLauncherModal({ default_url: DEFAULT_URL, port: DEFAULT_PORT, repository: "", + platform: "", }, }); const { diff --git a/client/src/features/sessionsV2/session.constants.tsx b/client/src/features/sessionsV2/session.constants.tsx index 1621292ddc..678a2f1341 100644 --- a/client/src/features/sessionsV2/session.constants.tsx +++ b/client/src/features/sessionsV2/session.constants.tsx @@ -136,6 +136,21 @@ export const BUILDER_FRONTENDS = [ }, ] as readonly BuilderSelectorOption[]; +export const BUILDER_PLATFORMS = [ + { + value: "linux/amd64", + label: "linux/amd64", + description: + "The default runtime platform. Select this option unless you know your session will run on a different platform.", + }, + { + value: "linux/arm64", + label: "linux/arm64", + description: + "Select this option if your session will run on ARM64 compute resources.", + }, +] as readonly BuilderSelectorOption[]; + export const IMAGE_BUILD_DOCS = "https://renku.notion.site/How-to-create-a-custom-environment-from-a-code-repository-1960df2efafc801b88f6da59a0aa8234"; diff --git a/client/src/features/sessionsV2/session.utils.ts b/client/src/features/sessionsV2/session.utils.ts index 989ecba0be..a3206b30c7 100644 --- a/client/src/features/sessionsV2/session.utils.ts +++ b/client/src/features/sessionsV2/session.utils.ts @@ -103,6 +103,7 @@ export function getFormattedEnvironmentValues(data: SessionLauncherForm): { gid, mount_directory, name, + platform, port, repository_revision: repository_revision_, repository, @@ -125,6 +126,7 @@ export function getFormattedEnvironmentValues(data: SessionLauncherForm): { builder_variant, frontend_variant, repository, + platforms: [platform], ...(context_dir ? { context_dir } : {}), ...(repository_revision ? { repository_revision } : {}), }, @@ -203,6 +205,7 @@ export function getFormattedEnvironmentValuesForEdit( builder_variant, context_dir, frontend_variant, + platform, repository_revision, repository, } = data; @@ -218,6 +221,7 @@ export function getFormattedEnvironmentValuesForEdit( repository, repository_revision: repository_revision ?? "", context_dir: context_dir ?? "", + platforms: [platform], }, }, }; @@ -277,6 +281,10 @@ export function getLauncherDefaultValues( launcher.environment.environment_image_source === "build" ? launcher.environment.build_parameters.context_dir ?? "" : "", + platform: + launcher.environment.environment_image_source === "build" + ? launcher.environment.build_parameters.platforms?.at(0) ?? "" + : "", }; } diff --git a/client/src/features/sessionsV2/sessionsV2.types.ts b/client/src/features/sessionsV2/sessionsV2.types.ts index 0d1bafa637..d9d94a9004 100644 --- a/client/src/features/sessionsV2/sessionsV2.types.ts +++ b/client/src/features/sessionsV2/sessionsV2.types.ts @@ -103,7 +103,7 @@ export interface SessionLauncherForm // For "global" environments environmentId: EnvironmentId; - // For "custom" + "image" environments + // For "custom + image" environments default_url: DefaultUrl; uid: EnvironmentUid; gid: EnvironmentGid; @@ -112,6 +112,9 @@ export interface SessionLauncherForm args: string; command: string; strip_path_prefix: boolean; + + // For "custom + build" environments + platform: string; } export interface SessionResources { From 93d30c46ce476c8d33223c2d58843570d23fb61d Mon Sep 17 00:00:00 2001 From: Flora Thiebaut Date: Thu, 27 Nov 2025 09:11:06 +0100 Subject: [PATCH 2/5] update --- client/scripts/update_api_spec.js | 2 +- .../sessionsV2/api/sessionLaunchersV2.generated-api.ts | 2 +- .../features/sessionsV2/api/sessionLaunchersV2.openapi.json | 3 +-- .../components/SessionForm/BuilderAdvancedSettings.tsx | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/client/scripts/update_api_spec.js b/client/scripts/update_api_spec.js index 5561c10f82..ae036591de 100644 --- a/client/scripts/update_api_spec.js +++ b/client/scripts/update_api_spec.js @@ -24,7 +24,7 @@ import { parseDocument } from "yaml"; const GH_BASE_URL = "https://raw.githubusercontent.com"; const DATA_SERVICES_REPO = "SwissDataScienceCenter/renku-data-services"; -const DATA_SERVICES_RELEASE = "leafty/configure-arm-builds"; +const DATA_SERVICES_RELEASE = "leafty/feat-resource-class-platforms"; async function main() { argv.forEach((arg) => { diff --git a/client/src/features/sessionsV2/api/sessionLaunchersV2.generated-api.ts b/client/src/features/sessionsV2/api/sessionLaunchersV2.generated-api.ts index efe337c240..2f44cceecb 100644 --- a/client/src/features/sessionsV2/api/sessionLaunchersV2.generated-api.ts +++ b/client/src/features/sessionsV2/api/sessionLaunchersV2.generated-api.ts @@ -315,7 +315,7 @@ export type EnvironmentWithImageGet = Environment & { environment_kind: EnvironmentKind; }; export type Repository = string; -export type BuildPlatform = string; +export type BuildPlatform = "linux/amd64" | "linux/arm64"; export type BuildPlatforms = BuildPlatform[]; export type BuilderVariant = string; export type FrontendVariant = string; diff --git a/client/src/features/sessionsV2/api/sessionLaunchersV2.openapi.json b/client/src/features/sessionsV2/api/sessionLaunchersV2.openapi.json index 8b75015a69..9ea19812e5 100644 --- a/client/src/features/sessionsV2/api/sessionLaunchersV2.openapi.json +++ b/client/src/features/sessionsV2/api/sessionLaunchersV2.openapi.json @@ -1016,8 +1016,7 @@ "BuildPlatform": { "description": "A runtime platform, e.g. \"linux/amd64\".", "type": "string", - "minLength": 1, - "maxLength": 99 + "enum": ["linux/amd64", "linux/arm64"] }, "BuilderVariant": { "description": "Type of virtual environment manager when building custom environments.", diff --git a/client/src/features/sessionsV2/components/SessionForm/BuilderAdvancedSettings.tsx b/client/src/features/sessionsV2/components/SessionForm/BuilderAdvancedSettings.tsx index a2875ab4bf..330997ac6c 100644 --- a/client/src/features/sessionsV2/components/SessionForm/BuilderAdvancedSettings.tsx +++ b/client/src/features/sessionsV2/components/SessionForm/BuilderAdvancedSettings.tsx @@ -16,7 +16,6 @@ * limitations under the License. */ -import ChevronFlippedIcon from "~/components/icons/ChevronFlippedIcon"; import cx from "classnames"; import { useCallback, useMemo, useState } from "react"; import { @@ -27,6 +26,7 @@ import { } from "react-hook-form"; import { Collapse, Label } from "reactstrap"; +import ChevronFlippedIcon from "~/components/icons/ChevronFlippedIcon"; import { BUILDER_PLATFORMS } from "../../session.constants"; import type { SessionLauncherForm } from "../../sessionsV2.types"; import BuilderSelectorCommon from "./BuilderSelectorCommon"; From 40b8081e4079618eeb790b3a771953163c807c0e Mon Sep 17 00:00:00 2001 From: Flora Thiebaut Date: Thu, 27 Nov 2025 09:19:13 +0100 Subject: [PATCH 3/5] fixes --- .../src/features/sessionsV2/session.constants.tsx | 6 +++--- client/src/features/sessionsV2/session.utils.ts | 15 ++++++++++++--- .../src/features/sessionsV2/sessionsV2.types.ts | 6 +++--- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/client/src/features/sessionsV2/session.constants.tsx b/client/src/features/sessionsV2/session.constants.tsx index 678a2f1341..69ed4c3471 100644 --- a/client/src/features/sessionsV2/session.constants.tsx +++ b/client/src/features/sessionsV2/session.constants.tsx @@ -138,18 +138,18 @@ export const BUILDER_FRONTENDS = [ export const BUILDER_PLATFORMS = [ { - value: "linux/amd64", + value: "linux/amd64" as const, label: "linux/amd64", description: "The default runtime platform. Select this option unless you know your session will run on a different platform.", }, { - value: "linux/arm64", + value: "linux/arm64" as const, label: "linux/arm64", description: "Select this option if your session will run on ARM64 compute resources.", }, -] as readonly BuilderSelectorOption[]; +] as readonly BuilderSelectorOption<"linux/amd64" | "linux/arm64">[]; export const IMAGE_BUILD_DOCS = "https://renku.notion.site/How-to-create-a-custom-environment-from-a-code-repository-1960df2efafc801b88f6da59a0aa8234"; diff --git a/client/src/features/sessionsV2/session.utils.ts b/client/src/features/sessionsV2/session.utils.ts index a3206b30c7..00ea9d3130 100644 --- a/client/src/features/sessionsV2/session.utils.ts +++ b/client/src/features/sessionsV2/session.utils.ts @@ -25,10 +25,11 @@ import type { SessionLauncherEnvironmentPatchParams, } from "./api/sessionLaunchersV2.api"; import { + BUILDER_PLATFORMS, DEFAULT_URL, ENV_VARIABLES_RESERVED_PREFIX, } from "./session.constants"; -import { SessionLauncherForm } from "./sessionsV2.types"; +import type { SessionLauncherForm } from "./sessionsV2.types"; export function getSessionFavicon( sessionState?: SessionStatusState, @@ -103,7 +104,7 @@ export function getFormattedEnvironmentValues(data: SessionLauncherForm): { gid, mount_directory, name, - platform, + platform: platform_, port, repository_revision: repository_revision_, repository, @@ -119,6 +120,10 @@ export function getFormattedEnvironmentValues(data: SessionLauncherForm): { if (environmentSelect === "custom + build") { const context_dir = context_dir_?.trim(); const repository_revision = repository_revision_?.trim(); + const platform = + BUILDER_PLATFORMS.map(({ value }) => value).find( + (value) => value === platform_ + ) ?? BUILDER_PLATFORMS[0].value; return { success: true, data: { @@ -205,10 +210,14 @@ export function getFormattedEnvironmentValuesForEdit( builder_variant, context_dir, frontend_variant, - platform, + platform: platform_, repository_revision, repository, } = data; + const platform = + BUILDER_PLATFORMS.map(({ value }) => value).find( + (value) => value === platform_ + ) ?? BUILDER_PLATFORMS[0].value; return { success: true, diff --git a/client/src/features/sessionsV2/sessionsV2.types.ts b/client/src/features/sessionsV2/sessionsV2.types.ts index d9d94a9004..b2eb027cf5 100644 --- a/client/src/features/sessionsV2/sessionsV2.types.ts +++ b/client/src/features/sessionsV2/sessionsV2.types.ts @@ -19,7 +19,7 @@ import type { ReactNode } from "react"; import type { CloudStorageDetailsOptions } from "../project/components/cloudStorage/projectCloudStorage.types"; -import type { ResourceClassWithId } from "./api/computeResources.generated-api"; +import type { ResourceClassWithId } from "./api/computeResources.api"; import type { BuildParametersPost, DefaultUrl, @@ -185,9 +185,9 @@ export interface DockerImage { error?: unknown; } -export interface BuilderSelectorOption { +export interface BuilderSelectorOption { label: string; - value: string; + value: T; description?: ReactNode; } From be46fd0d9cd3339769470b0b3eea17f612ab299b Mon Sep 17 00:00:00 2001 From: Flora Thiebaut Date: Thu, 27 Nov 2025 14:04:50 +0100 Subject: [PATCH 4/5] open collapse if current value is not default platform --- .../components/SessionForm/BuilderAdvancedSettings.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/src/features/sessionsV2/components/SessionForm/BuilderAdvancedSettings.tsx b/client/src/features/sessionsV2/components/SessionForm/BuilderAdvancedSettings.tsx index 330997ac6c..ed94913137 100644 --- a/client/src/features/sessionsV2/components/SessionForm/BuilderAdvancedSettings.tsx +++ b/client/src/features/sessionsV2/components/SessionForm/BuilderAdvancedSettings.tsx @@ -20,6 +20,7 @@ import cx from "classnames"; import { useCallback, useMemo, useState } from "react"; import { Controller, + useWatch, type Control, type FieldValues, type UseControllerProps, @@ -38,7 +39,9 @@ interface BuilderAdvancedSettingsProps { export default function BuilderAdvancedSettings({ control, }: BuilderAdvancedSettingsProps) { - const [isOpen, setIsOpen] = useState(false); + const watchPlatform = useWatch({ control, name: "platform" }); + const isDefaultPlatform = watchPlatform === BUILDER_PLATFORMS[0].value; + const [isOpen, setIsOpen] = useState(!isDefaultPlatform); const toggleIsOpen = useCallback( () => setIsOpen((isAdvancedSettingOpen) => !isAdvancedSettingOpen), [] From aa3443f0e03de92d62bb24d6aa39cb3b463374ee Mon Sep 17 00:00:00 2001 From: Flora Thiebaut Date: Thu, 27 Nov 2025 14:21:35 +0100 Subject: [PATCH 5/5] use useController instead --- .../SessionForm/BuilderAdvancedSettings.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/client/src/features/sessionsV2/components/SessionForm/BuilderAdvancedSettings.tsx b/client/src/features/sessionsV2/components/SessionForm/BuilderAdvancedSettings.tsx index ed94913137..e0299328e3 100644 --- a/client/src/features/sessionsV2/components/SessionForm/BuilderAdvancedSettings.tsx +++ b/client/src/features/sessionsV2/components/SessionForm/BuilderAdvancedSettings.tsx @@ -20,7 +20,7 @@ import cx from "classnames"; import { useCallback, useMemo, useState } from "react"; import { Controller, - useWatch, + useController, type Control, type FieldValues, type UseControllerProps, @@ -39,8 +39,13 @@ interface BuilderAdvancedSettingsProps { export default function BuilderAdvancedSettings({ control, }: BuilderAdvancedSettingsProps) { - const watchPlatform = useWatch({ control, name: "platform" }); - const isDefaultPlatform = watchPlatform === BUILDER_PLATFORMS[0].value; + const { + formState: { defaultValues }, + } = useController({ control, name: "platform" }); + defaultValues?.platform; + const isDefaultPlatform = + defaultValues?.platform == null || + defaultValues.platform === BUILDER_PLATFORMS[0].value; const [isOpen, setIsOpen] = useState(!isDefaultPlatform); const toggleIsOpen = useCallback( () => setIsOpen((isAdvancedSettingOpen) => !isAdvancedSettingOpen),