Skip to content

Commit 6f2efc1

Browse files
authored
feat: add support for building linux/arm64 images from code (#3899)
Add advanced setting to build session environments to run on the `linux/arm64` platform. This feature is required to build sessions which will run on arm64 nodes (e.g. `daint` at CSCS).
1 parent d128a09 commit 6f2efc1

File tree

9 files changed

+204
-6
lines changed

9 files changed

+204
-6
lines changed

client/scripts/update_api_spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import { parseDocument } from "yaml";
2424

2525
const GH_BASE_URL = "https://raw.githubusercontent.com";
2626
const DATA_SERVICES_REPO = "SwissDataScienceCenter/renku-data-services";
27-
const DATA_SERVICES_RELEASE = "main";
27+
const DATA_SERVICES_RELEASE = "leafty/feat-resource-class-platforms";
2828

2929
async function main() {
3030
argv.forEach((arg) => {

client/src/features/sessionsV2/api/sessionLaunchersV2.generated-api.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,12 +315,15 @@ export type EnvironmentWithImageGet = Environment & {
315315
environment_kind: EnvironmentKind;
316316
};
317317
export type Repository = string;
318+
export type BuildPlatform = "linux/amd64" | "linux/arm64";
319+
export type BuildPlatforms = BuildPlatform[];
318320
export type BuilderVariant = string;
319321
export type FrontendVariant = string;
320322
export type RepositoryRevision = string;
321323
export type BuildContextDir = string;
322324
export type BuildParameters = {
323325
repository: Repository;
326+
platforms?: BuildPlatforms;
324327
builder_variant: BuilderVariant;
325328
frontend_variant: FrontendVariant;
326329
repository_revision?: RepositoryRevision;
@@ -385,6 +388,7 @@ export type RepositoryRevisionPatch = string;
385388
export type BuildContextDirPatch = string;
386389
export type BuildParametersPatch = {
387390
repository?: Repository;
391+
platforms?: BuildPlatforms;
388392
builder_variant?: BuilderVariant;
389393
frontend_variant?: FrontendVariant;
390394
repository_revision?: RepositoryRevisionPatch;

client/src/features/sessionsV2/api/sessionLaunchersV2.openapi.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1006,6 +1006,18 @@
10061006
"maxLength": 99,
10071007
"example": "My Renku Session :)"
10081008
},
1009+
"BuildPlatforms": {
1010+
"description": "The target runtime platforms of a session environment.",
1011+
"type": "array",
1012+
"items": {
1013+
"$ref": "#/components/schemas/BuildPlatform"
1014+
}
1015+
},
1016+
"BuildPlatform": {
1017+
"description": "A runtime platform, e.g. \"linux/amd64\".",
1018+
"type": "string",
1019+
"enum": ["linux/amd64", "linux/arm64"]
1020+
},
10091021
"BuilderVariant": {
10101022
"description": "Type of virtual environment manager when building custom environments.",
10111023
"type": "string",
@@ -1106,6 +1118,9 @@
11061118
"repository": {
11071119
"$ref": "#/components/schemas/Repository"
11081120
},
1121+
"platforms": {
1122+
"$ref": "#/components/schemas/BuildPlatforms"
1123+
},
11091124
"builder_variant": {
11101125
"$ref": "#/components/schemas/BuilderVariant"
11111126
},
@@ -1144,6 +1159,9 @@
11441159
"repository": {
11451160
"$ref": "#/components/schemas/Repository"
11461161
},
1162+
"platforms": {
1163+
"$ref": "#/components/schemas/BuildPlatforms"
1164+
},
11471165
"builder_variant": {
11481166
"$ref": "#/components/schemas/BuilderVariant"
11491167
},
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
/*!
2+
* Copyright 2025 - Swiss Data Science Center (SDSC)
3+
* A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and
4+
* Eidgenössische Technische Hochschule Zürich (ETHZ).
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
import cx from "classnames";
20+
import { useCallback, useMemo, useState } from "react";
21+
import {
22+
Controller,
23+
useController,
24+
type Control,
25+
type FieldValues,
26+
type UseControllerProps,
27+
} from "react-hook-form";
28+
import { Collapse, Label } from "reactstrap";
29+
30+
import ChevronFlippedIcon from "~/components/icons/ChevronFlippedIcon";
31+
import { BUILDER_PLATFORMS } from "../../session.constants";
32+
import type { SessionLauncherForm } from "../../sessionsV2.types";
33+
import BuilderSelectorCommon from "./BuilderSelectorCommon";
34+
35+
interface BuilderAdvancedSettingsProps {
36+
control: Control<SessionLauncherForm>;
37+
}
38+
39+
export default function BuilderAdvancedSettings({
40+
control,
41+
}: BuilderAdvancedSettingsProps) {
42+
const {
43+
formState: { defaultValues },
44+
} = useController({ control, name: "platform" });
45+
defaultValues?.platform;
46+
const isDefaultPlatform =
47+
defaultValues?.platform == null ||
48+
defaultValues.platform === BUILDER_PLATFORMS[0].value;
49+
const [isOpen, setIsOpen] = useState(!isDefaultPlatform);
50+
const toggleIsOpen = useCallback(
51+
() => setIsOpen((isAdvancedSettingOpen) => !isAdvancedSettingOpen),
52+
[]
53+
);
54+
return (
55+
<div className={cx("d-flex", "flex-column", "gap-1")}>
56+
<button
57+
className={cx(
58+
"d-flex",
59+
"align-items-center",
60+
"w-100",
61+
"bg-transparent",
62+
"border-0",
63+
"p-0",
64+
"h4"
65+
)}
66+
type="button"
67+
onClick={toggleIsOpen}
68+
>
69+
Advanced settings
70+
<ChevronFlippedIcon className="ms-1" flipped={isOpen} />
71+
</button>
72+
<Collapse isOpen={isOpen}>
73+
<div className={cx("d-flex", "flex-column", "gap-3")}>
74+
<BuilderPlatformSelector name="platform" control={control} />
75+
</div>
76+
</Collapse>
77+
</div>
78+
);
79+
}
80+
81+
interface BuilderPlatformSelectorProps<T extends FieldValues>
82+
extends UseControllerProps<T> {}
83+
84+
function BuilderPlatformSelector<T extends FieldValues>({
85+
...controllerProps
86+
}: BuilderPlatformSelectorProps<T>) {
87+
const defaultValue = useMemo(
88+
() =>
89+
controllerProps.defaultValue
90+
? controllerProps.defaultValue
91+
: BUILDER_PLATFORMS[0],
92+
[controllerProps.defaultValue]
93+
);
94+
95+
return (
96+
<div>
97+
<Label for="builder-environment-platform-select-input">Platform</Label>
98+
<Controller
99+
{...controllerProps}
100+
render={({
101+
field: { onBlur, onChange, value, disabled },
102+
fieldState: { error },
103+
}) => (
104+
<>
105+
<div
106+
className={cx(error && "is-invalid")}
107+
data-cy="environment-platform-select"
108+
>
109+
<BuilderSelectorCommon
110+
defaultValue={defaultValue}
111+
disabled={disabled}
112+
id="builder-environment-platform-select"
113+
inputId="builder-environment-platform-select-input"
114+
name={controllerProps.name}
115+
onBlur={onBlur}
116+
onChange={onChange}
117+
options={BUILDER_PLATFORMS}
118+
value={value ?? ""}
119+
/>
120+
</div>
121+
<div className="invalid-feedback">
122+
{error?.message ? (
123+
<>{error.message}</>
124+
) : (
125+
<>Please select a valid platform.</>
126+
)}
127+
</div>
128+
</>
129+
)}
130+
rules={
131+
controllerProps.rules ?? {
132+
required: "Please select a platform.",
133+
}
134+
}
135+
/>
136+
</div>
137+
);
138+
}

client/src/features/sessionsV2/components/SessionForm/BuilderEnvironmentFields.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { DEFAULT_APP_PARAMS } from "../../../../utils/context/appParams.constant
2929
import { useProject } from "../../../ProjectPageV2/ProjectPageContainer/ProjectPageContainer";
3030
import { useGetRepositoriesProbesQuery } from "../../../repositories/repositories.api";
3131
import type { SessionLauncherForm } from "../../sessionsV2.types";
32+
import BuilderAdvancedSettings from "./BuilderAdvancedSettings";
3233
import BuilderFrontendSelector from "./BuilderFrontendSelector";
3334
import BuilderTypeSelector from "./BuilderTypeSelector";
3435
import CodeRepositoryAdvancedSettings from "./CodeRepositoryAdvancedSettings";
@@ -104,6 +105,7 @@ export default function BuilderEnvironmentFields({
104105
</div>
105106
<BuilderTypeSelector name="builder_variant" control={control} />
106107
<BuilderFrontendSelector name="frontend_variant" control={control} />
108+
<BuilderAdvancedSettings control={control} />
107109
</div>
108110
);
109111

client/src/features/sessionsV2/components/SessionModals/NewSessionLauncherModal.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ export default function NewSessionLauncherModal({
7272
default_url: DEFAULT_URL,
7373
port: DEFAULT_PORT,
7474
repository: "",
75+
platform: "",
7576
},
7677
});
7778
const {

client/src/features/sessionsV2/session.constants.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,21 @@ export const BUILDER_FRONTENDS = [
136136
},
137137
] as readonly BuilderSelectorOption[];
138138

139+
export const BUILDER_PLATFORMS = [
140+
{
141+
value: "linux/amd64" as const,
142+
label: "linux/amd64",
143+
description:
144+
"The default runtime platform. Select this option unless you know your session will run on a different platform.",
145+
},
146+
{
147+
value: "linux/arm64" as const,
148+
label: "linux/arm64",
149+
description:
150+
"Select this option if your session will run on ARM64 compute resources.",
151+
},
152+
] as readonly BuilderSelectorOption<"linux/amd64" | "linux/arm64">[];
153+
139154
export const IMAGE_BUILD_DOCS =
140155
"https://renku.notion.site/How-to-create-a-custom-environment-from-a-code-repository-1960df2efafc801b88f6da59a0aa8234";
141156

client/src/features/sessionsV2/session.utils.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,11 @@ import type {
2525
SessionLauncherEnvironmentPatchParams,
2626
} from "./api/sessionLaunchersV2.api";
2727
import {
28+
BUILDER_PLATFORMS,
2829
DEFAULT_URL,
2930
ENV_VARIABLES_RESERVED_PREFIX,
3031
} from "./session.constants";
31-
import { SessionLauncherForm } from "./sessionsV2.types";
32+
import type { SessionLauncherForm } from "./sessionsV2.types";
3233

3334
export function getSessionFavicon(
3435
sessionState?: SessionStatusState,
@@ -103,6 +104,7 @@ export function getFormattedEnvironmentValues(data: SessionLauncherForm): {
103104
gid,
104105
mount_directory,
105106
name,
107+
platform: platform_,
106108
port,
107109
repository_revision: repository_revision_,
108110
repository,
@@ -118,13 +120,18 @@ export function getFormattedEnvironmentValues(data: SessionLauncherForm): {
118120
if (environmentSelect === "custom + build") {
119121
const context_dir = context_dir_?.trim();
120122
const repository_revision = repository_revision_?.trim();
123+
const platform =
124+
BUILDER_PLATFORMS.map(({ value }) => value).find(
125+
(value) => value === platform_
126+
) ?? BUILDER_PLATFORMS[0].value;
121127
return {
122128
success: true,
123129
data: {
124130
environment_image_source: "build",
125131
builder_variant,
126132
frontend_variant,
127133
repository,
134+
platforms: [platform],
128135
...(context_dir ? { context_dir } : {}),
129136
...(repository_revision ? { repository_revision } : {}),
130137
},
@@ -203,9 +210,14 @@ export function getFormattedEnvironmentValuesForEdit(
203210
builder_variant,
204211
context_dir,
205212
frontend_variant,
213+
platform: platform_,
206214
repository_revision,
207215
repository,
208216
} = data;
217+
const platform =
218+
BUILDER_PLATFORMS.map(({ value }) => value).find(
219+
(value) => value === platform_
220+
) ?? BUILDER_PLATFORMS[0].value;
209221

210222
return {
211223
success: true,
@@ -218,6 +230,7 @@ export function getFormattedEnvironmentValuesForEdit(
218230
repository,
219231
repository_revision: repository_revision ?? "",
220232
context_dir: context_dir ?? "",
233+
platforms: [platform],
221234
},
222235
},
223236
};
@@ -277,6 +290,10 @@ export function getLauncherDefaultValues(
277290
launcher.environment.environment_image_source === "build"
278291
? launcher.environment.build_parameters.context_dir ?? ""
279292
: "",
293+
platform:
294+
launcher.environment.environment_image_source === "build"
295+
? launcher.environment.build_parameters.platforms?.at(0) ?? ""
296+
: "",
280297
};
281298
}
282299

client/src/features/sessionsV2/sessionsV2.types.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import type { ReactNode } from "react";
2020

2121
import type { CloudStorageDetailsOptions } from "../project/components/cloudStorage/projectCloudStorage.types";
22-
import type { ResourceClassWithId } from "./api/computeResources.generated-api";
22+
import type { ResourceClassWithId } from "./api/computeResources.api";
2323
import type {
2424
BuildParametersPost,
2525
DefaultUrl,
@@ -103,7 +103,7 @@ export interface SessionLauncherForm
103103
// For "global" environments
104104
environmentId: EnvironmentId;
105105

106-
// For "custom" + "image" environments
106+
// For "custom + image" environments
107107
default_url: DefaultUrl;
108108
uid: EnvironmentUid;
109109
gid: EnvironmentGid;
@@ -112,6 +112,9 @@ export interface SessionLauncherForm
112112
args: string;
113113
command: string;
114114
strip_path_prefix: boolean;
115+
116+
// For "custom + build" environments
117+
platform: string;
115118
}
116119

117120
export interface SessionResources {
@@ -182,9 +185,9 @@ export interface DockerImage {
182185
error?: unknown;
183186
}
184187

185-
export interface BuilderSelectorOption {
188+
export interface BuilderSelectorOption<T extends string = string> {
186189
label: string;
187-
value: string;
190+
value: T;
188191
description?: ReactNode;
189192
}
190193

0 commit comments

Comments
 (0)