diff --git a/client/scripts/update_api_spec.js b/client/scripts/update_api_spec.js index 5c4786f02a..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 = "main"; +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 0a34981ae6..2f44cceecb 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 = "linux/amd64" | "linux/arm64"; +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..9ea19812e5 100644 --- a/client/src/features/sessionsV2/api/sessionLaunchersV2.openapi.json +++ b/client/src/features/sessionsV2/api/sessionLaunchersV2.openapi.json @@ -1006,6 +1006,18 @@ "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", + "enum": ["linux/amd64", "linux/arm64"] + }, "BuilderVariant": { "description": "Type of virtual environment manager when building custom environments.", "type": "string", @@ -1106,6 +1118,9 @@ "repository": { "$ref": "#/components/schemas/Repository" }, + "platforms": { + "$ref": "#/components/schemas/BuildPlatforms" + }, "builder_variant": { "$ref": "#/components/schemas/BuilderVariant" }, @@ -1144,6 +1159,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..e0299328e3 --- /dev/null +++ b/client/src/features/sessionsV2/components/SessionForm/BuilderAdvancedSettings.tsx @@ -0,0 +1,138 @@ +/*! + * 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 cx from "classnames"; +import { useCallback, useMemo, useState } from "react"; +import { + Controller, + useController, + type Control, + type FieldValues, + type UseControllerProps, +} 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"; + +interface BuilderAdvancedSettingsProps { + control: Control; +} + +export default function BuilderAdvancedSettings({ + control, +}: BuilderAdvancedSettingsProps) { + 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), + [] + ); + 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..69ed4c3471 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" 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" as const, + label: "linux/arm64", + description: + "Select this option if your session will run on ARM64 compute resources.", + }, +] 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 989ecba0be..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,6 +104,7 @@ export function getFormattedEnvironmentValues(data: SessionLauncherForm): { gid, mount_directory, name, + platform: platform_, port, repository_revision: repository_revision_, repository, @@ -118,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: { @@ -125,6 +131,7 @@ export function getFormattedEnvironmentValues(data: SessionLauncherForm): { builder_variant, frontend_variant, repository, + platforms: [platform], ...(context_dir ? { context_dir } : {}), ...(repository_revision ? { repository_revision } : {}), }, @@ -203,9 +210,14 @@ export function getFormattedEnvironmentValuesForEdit( builder_variant, context_dir, frontend_variant, + platform: platform_, repository_revision, repository, } = data; + const platform = + BUILDER_PLATFORMS.map(({ value }) => value).find( + (value) => value === platform_ + ) ?? BUILDER_PLATFORMS[0].value; return { success: true, @@ -218,6 +230,7 @@ export function getFormattedEnvironmentValuesForEdit( repository, repository_revision: repository_revision ?? "", context_dir: context_dir ?? "", + platforms: [platform], }, }, }; @@ -277,6 +290,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..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, @@ -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 { @@ -182,9 +185,9 @@ export interface DockerImage { error?: unknown; } -export interface BuilderSelectorOption { +export interface BuilderSelectorOption { label: string; - value: string; + value: T; description?: ReactNode; }