Skip to content

Commit 12348ce

Browse files
authored
[release] Init step for custom BYOD image build (ray-project#55398)
- Add `custom_byod_build_init` to scan the tests and list out all custom images to build, then create a yaml file (that will be included when rayci runs) to launch the build jobs. - Add `custom_byod_build` as a python script that the jobs can call to build & push the images This needs to be merged after ray-project#55397 --------- Signed-off-by: kevin <[email protected]>
1 parent 2cbf1e2 commit 12348ce

File tree

6 files changed

+289
-0
lines changed

6 files changed

+289
-0
lines changed

ci/ray_ci/oss_config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ release_byod:
44
ray_ml_cr_repo: ray-ml
55
ray_llm_cr_repo: ray-llm
66
byod_ecr: 029272617770.dkr.ecr.us-west-2.amazonaws.com
7+
byod_ecr_region: us-west-2
78
aws_cr: 029272617770.dkr.ecr.us-west-2.amazonaws.com
89
gcp_cr: us-west1-docker.pkg.dev/anyscale-oss-ci
910
aws2gce_credentials: release/aws2gce_iam.json

release/BUILD.bazel

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,24 @@ py_test(
456456
],
457457
)
458458

459+
py_test(
460+
name = "test_custom_byod_build_init_helper",
461+
size = "small",
462+
srcs = ["ray_release/tests/test_custom_byod_build_init_helper.py"],
463+
data = [
464+
"ray_release/configs/oss_config.yaml",
465+
],
466+
exec_compatible_with = ["//:hermetic_python"],
467+
tags = [
468+
"release_unit",
469+
"team:ci",
470+
],
471+
deps = [
472+
":ray_release",
473+
bk_require("pytest"),
474+
],
475+
)
476+
459477
py_test(
460478
name = "test_cluster_manager",
461479
size = "small",
@@ -729,3 +747,12 @@ py_binary(
729747
":ray_release",
730748
],
731749
)
750+
751+
py_binary(
752+
name = "custom_byod_build_init",
753+
srcs = ["ray_release/scripts/custom_byod_build_init.py"],
754+
exec_compatible_with = ["//:hermetic_python"],
755+
deps = [
756+
":ray_release",
757+
],
758+
)

release/ray_release/configs/global_config.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ class GlobalConfig(TypedDict):
1111
byod_ray_ml_cr_repo: str
1212
byod_ray_llm_cr_repo: str
1313
byod_ecr: str
14+
byod_ecr_region: str
1415
byod_aws_cr: str
1516
byod_gcp_cr: str
1617
state_machine_pr_aws_bucket: str
@@ -67,6 +68,10 @@ def _init_global_config(config_file: str):
6768
config_content.get("byod", {}).get("byod_ecr")
6869
or config_content.get("release_byod", {}).get("byod_ecr")
6970
),
71+
byod_ecr_region=(
72+
config_content.get("byod", {}).get("byod_ecr_region")
73+
or config_content.get("release_byod", {}).get("byod_ecr_region")
74+
),
7075
byod_aws_cr=(
7176
config_content.get("byod", {}).get("aws_cr")
7277
or config_content.get("release_byod", {}).get("aws_cr")
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
from typing import List, Tuple
2+
import yaml
3+
from ray_release.configs.global_config import get_global_config
4+
from ray_release.logger import logger
5+
from ray_release.test import Test
6+
7+
8+
def _generate_custom_build_step_key(image: str) -> str:
9+
# Buildkite step key cannot contain special characters, so they need to be replaced.
10+
# Buildkite also limits step key length to 80 characters.
11+
return (
12+
"custom_build_"
13+
+ image.replace("/", "_")
14+
.replace(":", "_")
15+
.replace(".", "_")
16+
.replace("-", "_")[-40:]
17+
)
18+
19+
20+
def get_images_from_tests(tests: List[Test]) -> List[Tuple[str, str, str]]:
21+
"""Get a list of custom BYOD images to build from a list of tests."""
22+
custom_byod_images = set()
23+
for test in tests:
24+
if not test.require_custom_byod_image():
25+
continue
26+
custom_byod_image_build = (
27+
test.get_anyscale_byod_image(),
28+
test.get_anyscale_base_byod_image(),
29+
test.get_byod_post_build_script(),
30+
)
31+
logger.info(f"To be built: {custom_byod_image_build[0]}")
32+
custom_byod_images.add(custom_byod_image_build)
33+
return list(custom_byod_images)
34+
35+
36+
def create_custom_build_yaml(destination_file: str, tests: List[Test]) -> None:
37+
config = get_global_config()
38+
if not config or not config.get("byod_ecr_region") or not config.get("byod_ecr"):
39+
raise ValueError("byod_ecr_region and byod_ecr must be set in the config")
40+
"""Create a yaml file for building custom BYOD images"""
41+
custom_byod_images = get_images_from_tests(tests)
42+
if not custom_byod_images:
43+
return
44+
build_config = {"group": "Custom images build", "steps": []}
45+
46+
for image, base_image, post_build_script in custom_byod_images:
47+
if not post_build_script:
48+
continue
49+
step = {
50+
"label": f":tapioca: build custom: {image}",
51+
"key": _generate_custom_build_step_key(image),
52+
"instance_type": "release-medium",
53+
"commands": [
54+
f"aws ecr get-login-password --region {config['byod_ecr_region']} | docker login --username AWS --password-stdin {config['byod_ecr']}",
55+
f"bazelisk run //release:custom_byod_build -- --image-name {image} --base-image {base_image} --post-build-script {post_build_script}",
56+
],
57+
}
58+
if "ray-ml" in image:
59+
step["depends_on"] = "anyscalemlbuild"
60+
elif "ray-llm" in image:
61+
step["depends_on"] = "anyscalellmbuild"
62+
else:
63+
step["depends_on"] = "anyscalebuild"
64+
build_config["steps"].append(step)
65+
66+
with open(destination_file, "w") as f:
67+
yaml.dump(build_config, f, default_flow_style=False, sort_keys=False)
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import os
2+
from typing import Tuple
3+
from pathlib import Path
4+
import sys
5+
6+
import click
7+
8+
from ray_release.buildkite.filter import filter_tests
9+
from ray_release.buildkite.settings import get_pipeline_settings
10+
from ray_release.config import (
11+
read_and_validate_release_test_collection,
12+
RELEASE_TEST_CONFIG_FILES,
13+
)
14+
from ray_release.configs.global_config import init_global_config
15+
from ray_release.exception import ReleaseTestConfigError, ReleaseTestCLIError
16+
from ray_release.logger import logger
17+
from ray_release.custom_byod_build_init_helper import create_custom_build_yaml
18+
19+
20+
@click.command(
21+
help="Create a rayci yaml file for building custom BYOD images based on tests."
22+
)
23+
@click.option(
24+
"--test-collection-file",
25+
type=str,
26+
multiple=True,
27+
help="Test collection file, relative path to ray repo.",
28+
)
29+
@click.option(
30+
"--run-jailed-tests",
31+
is_flag=True,
32+
show_default=True,
33+
default=False,
34+
help=("Will run jailed tests."),
35+
)
36+
@click.option(
37+
"--run-unstable-tests",
38+
is_flag=True,
39+
show_default=True,
40+
default=False,
41+
help=("Will run unstable tests."),
42+
)
43+
@click.option(
44+
"--global-config",
45+
default="oss_config.yaml",
46+
type=click.Choice(
47+
[x.name for x in (Path(__file__).parent.parent / "configs").glob("*.yaml")]
48+
),
49+
help="Global config to use for test execution.",
50+
)
51+
@click.option(
52+
"--frequency",
53+
default=None,
54+
type=click.Choice(["manual", "nightly", "nightly-3x", "weekly"]),
55+
help="Run frequency of the test",
56+
)
57+
@click.option(
58+
"--test-filters",
59+
default=None,
60+
type=str,
61+
help="Test filters by prefix/regex.",
62+
)
63+
def main(
64+
test_collection_file: Tuple[str],
65+
run_jailed_tests: bool = False,
66+
run_unstable_tests: bool = False,
67+
global_config: str = "oss_config.yaml",
68+
frequency: str = None,
69+
test_filters: str = None,
70+
):
71+
global_config_file = os.path.join(
72+
os.path.dirname(__file__), "..", "configs", global_config
73+
)
74+
init_global_config(global_config_file)
75+
settings = get_pipeline_settings()
76+
77+
frequency = frequency or settings["frequency"]
78+
prefer_smoke_tests = settings["prefer_smoke_tests"]
79+
test_filters = test_filters or settings["test_filters"]
80+
81+
try:
82+
test_collection = read_and_validate_release_test_collection(
83+
test_collection_file or RELEASE_TEST_CONFIG_FILES
84+
)
85+
except ReleaseTestConfigError as e:
86+
raise ReleaseTestConfigError(
87+
"Cannot load test yaml file.\nHINT: If you're kicking off tests for a "
88+
"specific commit on Buildkite to test Ray wheels, after clicking "
89+
"'New build', leave the commit at HEAD, and only specify the commit "
90+
"in the dialog that asks for the Ray wheels."
91+
) from e
92+
93+
filtered_tests = filter_tests(
94+
test_collection,
95+
frequency=frequency,
96+
test_filters=test_filters,
97+
prefer_smoke_tests=prefer_smoke_tests,
98+
run_jailed_tests=run_jailed_tests,
99+
run_unstable_tests=run_unstable_tests,
100+
)
101+
logger.info(f"Found {len(filtered_tests)} tests to run.")
102+
if len(filtered_tests) == 0:
103+
raise ReleaseTestCLIError(
104+
"Empty test collection. The selected frequency or filter did "
105+
"not return any tests to run. Adjust your filters."
106+
)
107+
tests = [test for test, _ in filtered_tests]
108+
create_custom_build_yaml(".buildkite/release/custom_byod_build.rayci.yml", tests)
109+
110+
111+
if __name__ == "__main__":
112+
sys.exit(main())
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import os
2+
import tempfile
3+
import sys
4+
import pytest
5+
from unittest import mock
6+
import yaml
7+
8+
from ray_release.custom_byod_build_init_helper import create_custom_build_yaml
9+
from ray_release.configs.global_config import init_global_config
10+
from ray_release.bazel import bazel_runfile
11+
from ray_release.test import Test
12+
from ray_release.configs.global_config import get_global_config
13+
14+
15+
init_global_config(bazel_runfile("release/ray_release/configs/oss_config.yaml"))
16+
17+
18+
@mock.patch("ray_release.custom_byod_build_init_helper.get_images_from_tests")
19+
def test_create_custom_build_yaml(mock_get_images_from_tests):
20+
config = get_global_config()
21+
custom_byod_images = [
22+
(
23+
"ray-project/ray-ml:abc123-custom",
24+
"ray-project/ray-ml:abc123-base",
25+
"custom_script.sh",
26+
),
27+
("ray-project/ray-ml:abc123-custom", "ray-project/ray-ml:abc123-base", ""),
28+
(
29+
"ray-project/ray-ml:nightly-py37-cpu-custom-abcdef123456789abc123456789",
30+
"ray-project/ray-ml:nightly-py37-cpu-base",
31+
"custom_script.sh",
32+
), # longer than 40 chars
33+
]
34+
mock_get_images_from_tests.return_value = custom_byod_images
35+
36+
# List of dummy tests
37+
tests = [
38+
Test(
39+
name="test_1",
40+
frequency="manual",
41+
group="test_group",
42+
team="test_team",
43+
working_dir="test_working_dir",
44+
),
45+
Test(
46+
name="test_2",
47+
frequency="manual",
48+
group="test_group",
49+
team="test_team",
50+
working_dir="test_working_dir",
51+
),
52+
]
53+
with tempfile.TemporaryDirectory() as tmpdir:
54+
create_custom_build_yaml(
55+
os.path.join(tmpdir, "custom_byod_build.rayci.yml"), tests
56+
)
57+
with open(os.path.join(tmpdir, "custom_byod_build.rayci.yml"), "r") as f:
58+
content = yaml.safe_load(f)
59+
assert content["group"] == "Custom images build"
60+
assert len(content["steps"]) == 2
61+
assert (
62+
f"--region {config['byod_ecr_region']}"
63+
in content["steps"][0]["commands"][0]
64+
)
65+
assert f"{config['byod_ecr']}" in content["steps"][0]["commands"][0]
66+
assert (
67+
f"--image-name {custom_byod_images[0][0]}"
68+
in content["steps"][0]["commands"][1]
69+
)
70+
assert (
71+
f"--image-name {custom_byod_images[2][0]}"
72+
in content["steps"][1]["commands"][1]
73+
)
74+
75+
76+
if __name__ == "__main__":
77+
sys.exit(pytest.main(["-v", __file__]))

0 commit comments

Comments
 (0)