logstash/.buildkite/scripts/jdk-matrix-tests/generate-steps.py
Dimitrios Liappis c0c213d17e
Split java/ruby unit test steps on Windows (#15888)
As a follow up to #15861 this commit splits the current unit tests step
for the Windows JDK matrix pipeline to two that run
Java and Ruby unit tests separately.

Closes https://github.com/elastic/logstash/issues/15566
2024-03-11 09:27:11 +02:00

405 lines
13 KiB
Python

import abc
import copy
from dataclasses import dataclass, field
import os
import sys
import typing
from ruamel.yaml import YAML
from ruamel.yaml.scalarstring import LiteralScalarString
ENABLED_RETRIES = {"automatic": [{"limit": 3}]}
@dataclass
class JobRetValues:
step_label: str
command: str
step_key: str
depends: str
agent: typing.Dict[typing.Any, typing.Any]
retry: typing.Optional[typing.Dict[typing.Any, typing.Any]] = None
artifact_paths: list = field(default_factory=list)
class GCPAgent:
def __init__(self, image: str, machineType: str, diskSizeGb: int = 200, diskType: str = "pd-ssd") -> None:
self.provider = "gcp"
self.imageProject = "elastic-images-prod"
self.image = image
self.machineType = machineType
self.diskSizeGb = diskSizeGb
self.diskType = diskType
def to_dict(self):
return {
"provider": self.provider,
"imageProject": self.imageProject,
"image": self.image,
"machineType": self.machineType,
"diskSizeGb": self.diskSizeGb,
"diskType": self.diskType,
}
class AWSAgent:
def __init__(self, imagePrefix: str, instanceType: str, diskSizeGb: int):
self.provider = "aws"
self.imagePrefix = imagePrefix
self.instanceType = instanceType
self.diskSizeGb = diskSizeGb
def to_dict(self):
return {
"provider": self.provider,
"imagePrefix": self.imagePrefix,
"instanceType": self.instanceType,
"diskSizeGb": self.diskSizeGb,
}
class DefaultAgent:
"""
Represents an empty agent definition which makes Buildkite use the default agent i.e. a container
"""
def __init__(self) -> None:
pass
def to_json(self) -> typing.Dict:
return {}
@dataclass
class BuildkiteEmojis:
running: str = ":bk-status-running:"
success: str = ":bk-status-passed:"
failed: str = ":bk-status-failed:"
def slugify_bk_key(key: str) -> str:
"""
Convert and return key to an acceptable format for Buildkite's key: field
Only alphanumerics, dashes and underscores are allowed.
"""
mapping_table = str.maketrans({'.': '_', ' ': '_', '/': '_'})
return key.translate(mapping_table)
def get_bk_metadata(key: str) -> typing.List[str]:
try:
return os.environ[key].split()
except KeyError:
print(f"Missing environment variable [{key}]. This should be set before calling this script using buildkite-agent meta-data get. Exiting.")
exit(1)
def bk_annotate(body: str, context: str, mode = "") -> str:
cmd = f"""buildkite-agent annotate --style=info --context={context} """
if mode:
cmd += f"--{mode} "
cmd += f"\"{body}\n\""
return cmd
class Jobs(abc.ABC):
def __init__(self, os: str, jdk: str, group_key: str, agent: typing.Union[GCPAgent, AWSAgent]):
self.os = os
self.jdk = jdk
self.group_key = group_key
self.init_annotation_key = f"{os}-{jdk}-initialize-annotation"
self.agent = agent
def init_annotation(self) -> JobRetValues:
"""
Command for creating the header of a new annotation for a group step
"""
body = f"### Group: {self.os} / {self.jdk}\n| **Status** | **Test** |\n| --- | ----|"
return JobRetValues(
step_label="Initialize annotation",
command=LiteralScalarString(bk_annotate(body=body, context=self.group_key)),
step_key=self.init_annotation_key,
depends="",
agent=DefaultAgent().to_json(),
)
@abc.abstractmethod
def all_jobs(self) -> list[typing.Callable[[], typing.Tuple[str, str]]]:
pass
class WindowsJobs(Jobs):
def __init__(self, os: str, jdk: str, group_key: str, agent: typing.Union[GCPAgent, AWSAgent]):
super().__init__(os=os, jdk=jdk, group_key=group_key, agent=agent)
def all_jobs(self) -> list[typing.Callable[[], JobRetValues]]:
return [
self.init_annotation,
self.java_unit_test,
self.ruby_unit_test,
]
def java_unit_test(self) -> JobRetValues:
step_name_human = "Java Unit Test"
step_key = f"{self.group_key}-java-unit-test"
test_command = rf'''.\\.buildkite\\scripts\\jdk-matrix-tests\\launch-command.ps1 -JDK "{self.jdk}" -StepNameHuman "{step_name_human}" -AnnotateContext "{self.group_key}" -CIScript ".\\ci\\unit_tests.ps1 java" -Annotate
'''
return JobRetValues(
step_label=step_name_human,
command=LiteralScalarString(test_command),
step_key=step_key,
depends=self.init_annotation_key,
artifact_paths=["build_reports.zip"],
agent=self.agent.to_dict(),
retry=copy.deepcopy(ENABLED_RETRIES),
)
def ruby_unit_test(self) -> JobRetValues:
step_name_human = "Ruby Unit Test"
step_key = f"{self.group_key}-ruby-unit-test"
test_command = rf'''.\\.buildkite\\scripts\\jdk-matrix-tests\\launch-command.ps1 -JDK "{self.jdk}" -StepNameHuman "{step_name_human}" -AnnotateContext "{self.group_key}" -CIScript ".\\ci\\unit_tests.ps1 ruby" -Annotate
'''
return JobRetValues(
step_label=step_name_human,
command=LiteralScalarString(test_command),
step_key=step_key,
depends=self.init_annotation_key,
artifact_paths=["build_reports.zip"],
agent=self.agent.to_dict(),
retry=copy.deepcopy(ENABLED_RETRIES),
)
class LinuxJobs(Jobs):
def __init__(self, os: str, jdk: str, group_key: str, agent: typing.Union[GCPAgent, AWSAgent]):
super().__init__(os=os, jdk=jdk, group_key=group_key, agent=agent)
def all_jobs(self) -> list[typing.Callable[[], JobRetValues]]:
return [
self.init_annotation,
self.java_unit_test,
self.ruby_unit_test,
self.integration_tests_part_1,
self.integration_tests_part_2,
self.pq_integration_tests_part_1,
self.pq_integration_tests_part_2,
self.x_pack_unit_tests,
self.x_pack_integration,
]
def prepare_shell(self) -> str:
jdk_dir = f"/opt/buildkite-agent/.java/{self.jdk}"
return f"""#!/usr/bin/env bash
set -euo pipefail
# unset generic JAVA_HOME
unset JAVA_HOME
# LS env vars for JDK matrix tests
export BUILD_JAVA_HOME={jdk_dir}
export RUNTIME_JAVA_HOME={jdk_dir}
export LS_JAVA_HOME={jdk_dir}
export PATH="/opt/buildkite-agent/.rbenv/bin:/opt/buildkite-agent/.pyenv/bin:$PATH"
eval "$(rbenv init -)"
"""
def failed_step_annotation(self, step_name_human) -> str:
return bk_annotate(body=f"| {BuildkiteEmojis.failed} | {step_name_human} |", context=self.group_key, mode="append")
def succeeded_step_annotation(self, step_name_human) -> str:
return bk_annotate(body=f"| {BuildkiteEmojis.success} | {step_name_human} |", context=self.group_key, mode="append")
def emit_command(self, step_name_human, test_command: str) -> str:
return LiteralScalarString(f"""
{self.prepare_shell()}
# temporarily disable immediate failure on errors, so that we can update the BK annotation
set +eo pipefail
{test_command}
if [[ $$? -ne 0 ]]; then
{self.failed_step_annotation(step_name_human)}
exit 1
else
{self.succeeded_step_annotation(step_name_human)}
fi
""")
def java_unit_test(self) -> JobRetValues:
step_name_human = "Java Unit Test"
step_key = f"{self.group_key}-java-unit-test"
test_command = '''
export ENABLE_SONARQUBE="false"
ci/unit_tests.sh java
'''
return JobRetValues(
step_label=step_name_human,
command=self.emit_command(step_name_human, test_command),
step_key=step_key,
depends=self.init_annotation_key,
agent=self.agent.to_dict(),
retry=copy.deepcopy(ENABLED_RETRIES),
)
def ruby_unit_test(self) -> JobRetValues:
step_name_human = "Ruby Unit Test"
step_key = f"{self.group_key}-ruby-unit-test"
test_command = """
ci/unit_tests.sh ruby
"""
return JobRetValues(
step_label=step_name_human,
command=self.emit_command(step_name_human, test_command),
step_key=step_key,
depends=self.init_annotation_key,
agent=self.agent.to_dict(),
retry=copy.deepcopy(ENABLED_RETRIES),
)
def integration_tests_part_1(self) -> JobRetValues:
return self.integration_tests(part=1)
def integration_tests_part_2(self) -> JobRetValues:
return self.integration_tests(part=2)
def integration_tests(self, part: int) -> JobRetValues:
step_name_human = f"Integration Tests - {part}"
step_key = f"{self.group_key}-integration-tests-{part}"
test_command = f"""
ci/integration_tests.sh split {part-1}
"""
return JobRetValues(
step_label=step_name_human,
command=self.emit_command(step_name_human, test_command),
step_key=step_key,
depends=self.init_annotation_key,
agent=self.agent.to_dict(),
retry=copy.deepcopy(ENABLED_RETRIES),
)
def pq_integration_tests_part_1(self) -> JobRetValues:
return self.pq_integration_tests(part=1)
def pq_integration_tests_part_2(self) -> JobRetValues:
return self.pq_integration_tests(part=2)
def pq_integration_tests(self, part: int) -> JobRetValues:
step_name_human = f"IT Persistent Queues - {part}"
step_key = f"{self.group_key}-it-persistent-queues-{part}"
test_command = f"""
export FEATURE_FLAG=persistent_queues
ci/integration_tests.sh split {part-1}
"""
return JobRetValues(
step_label=step_name_human,
command=self.emit_command(step_name_human, test_command),
step_key=step_key,
depends=self.init_annotation_key,
agent=self.agent.to_dict(),
retry=copy.deepcopy(ENABLED_RETRIES),
)
def x_pack_unit_tests(self) -> JobRetValues:
step_name_human = "x-pack unit tests"
step_key = f"{self.group_key}-x-pack-unit-test"
test_command = """
x-pack/ci/unit_tests.sh
"""
return JobRetValues(
step_label=step_name_human,
command=self.emit_command(step_name_human, test_command),
step_key=step_key,
depends=self.init_annotation_key,
agent=self.agent.to_dict(),
retry=copy.deepcopy(ENABLED_RETRIES),
)
def x_pack_integration(self) -> JobRetValues:
step_name_human = "x-pack integration"
step_key = f"{self.group_key}-x-pack-integration"
test_command = """
x-pack/ci/integration_tests.sh
"""
return JobRetValues(
step_label=step_name_human,
command=self.emit_command(step_name_human, test_command),
step_key=step_key,
depends=self.init_annotation_key,
agent=self.agent.to_dict(),
retry=copy.deepcopy(ENABLED_RETRIES),
)
if __name__ == "__main__":
matrix_oses = get_bk_metadata(key="MATRIX_OSES")
matrix_jdkes = get_bk_metadata(key="MATRIX_JDKS")
pipeline_name = os.environ.get("BUILDKITE_PIPELINE_NAME", "").lower()
structure = {"steps": []}
for matrix_os in matrix_oses:
gcpAgent = GCPAgent(
image=f"family/platform-ingest-logstash-multi-jdk-{matrix_os}",
machineType="n2-standard-4",
diskSizeGb=200,
diskType="pd-ssd",
)
awsAgent = AWSAgent(
imagePrefix=f"platform-ingest-logstash-multi-jdk-{matrix_os}",
instanceType="m5.2xlarge",
diskSizeGb=200,
)
for matrix_jdk in matrix_jdkes:
group_name = f"{matrix_os}/{matrix_jdk}"
group_key = slugify_bk_key(group_name)
agent = awsAgent if "amazon" in matrix_os else gcpAgent
if "windows" in pipeline_name:
jobs = WindowsJobs(os=matrix_os, jdk=matrix_jdk, group_key=group_key, agent=agent)
else:
jobs = LinuxJobs(os=matrix_os, jdk=matrix_jdk, group_key=group_key, agent=agent)
group_steps = []
for job in jobs.all_jobs():
job_values = job()
step = {
"label": f"{job_values.step_label}",
"key": job_values.step_key,
}
if job_values.depends:
step["depends_on"] = job_values.depends
if job_values.agent:
step["agents"] = job_values.agent
if job_values.artifact_paths:
step["artifact_paths"] = job_values.artifact_paths
if job_values.retry:
step["retry"] = job_values.retry
step["command"] = job_values.command
group_steps.append(step)
structure["steps"].append({
"group": group_name,
"key": slugify_bk_key(group_name),
"steps": group_steps})
print('# yaml-language-server: $schema=https://raw.githubusercontent.com/buildkite/pipeline-schema/main/schema.json')
YAML().dump(structure, sys.stdout)