diff --git a/.gitlab/ssi_gitlab-ci.yml b/.gitlab/ssi_gitlab-ci.yml index 0c66ced2557..e454766889d 100644 --- a/.gitlab/ssi_gitlab-ci.yml +++ b/.gitlab/ssi_gitlab-ci.yml @@ -252,6 +252,20 @@ ssi_tests: # Run the tests and capture stdout+stderr so the after_script can # re-print the run summary (pipefail keeps run.sh's real exit code). set -o pipefail + # Catalog entries are stored as ssi/... or ssi-dev/.... Put the registry back. + # Any other value is a custom image reference and must be used as given. + expand_k8s_ref() { + case "$1" in + ssi/*|ssi-dev/*) printf '%s/%s' "${PRIVATE_DOCKER_REGISTRY}" "$1" ;; + *) printf '%s' "$1" ;; + esac + } + if [ -z "${K8S_WEBLOG_IMG:-}" ]; then + K8S_WEBLOG_IMG="${PRIVATE_DOCKER_REGISTRY}/system-tests/${K8S_WEBLOG}" + fi + K8S_LIB_INIT_IMG=$(expand_k8s_ref "${K8S_LIB_INIT_IMG}") + K8S_INJECTOR_IMG=$(expand_k8s_ref "${K8S_INJECTOR_IMG:-}") + K8S_CLUSTER_IMG=$(expand_k8s_ref "${K8S_CLUSTER_IMG:-}") ./run.sh ${K8S_SCENARIO} --k8s-library ${TEST_LIBRARY} --k8s-weblog ${K8S_WEBLOG} --k8s-weblog-img ${K8S_WEBLOG_IMG}:${TAG_NAME} --k8s-lib-init-img ${K8S_LIB_INIT_IMG} --k8s-injector-img ${K8S_INJECTOR_IMG:-None} --k8s-cluster-img ${K8S_CLUSTER_IMG:-None} --report-run-url $CI_JOB_URL --report-environment ${REPORT_ENVIRONMENT} 2>&1 | tee "${CI_PROJECT_DIR}/run_output.log" after_script: | diff --git a/docs/understand/scenarios/k8s_lib_injection.md b/docs/understand/scenarios/k8s_lib_injection.md index 131e8b0498a..9632e991243 100644 --- a/docs/understand/scenarios/k8s_lib_injection.md +++ b/docs/understand/scenarios/k8s_lib_injection.md @@ -417,7 +417,7 @@ export K8S_LIB_INIT_IMG="my-registry/my-lib-init:my-tag" ### Implementation Details - **Parser**: [`utils/k8s/k8s_components_parser.py`](../../utils/k8s/k8s_components_parser.py) - Singleton that reads `k8s_components.json` -- **CI Matrix Builder**: [`utils/scripts/ci_orchestrators/gitlab_exporter.py`](../../utils/scripts/ci_orchestrators/gitlab_exporter.py) - Generates test matrix for GitLab CI +- **CI Matrix Builder**: [`utils/scripts/ci_orchestrators/gitlab_exporter.py`](../../utils/scripts/ci_orchestrators/gitlab_exporter.py) - Generates test matrix for GitLab CI. Image values in the job name are the path without the registry host (`ssi/cluster-agent:7.84.3`). The job prepends `PRIVATE_DOCKER_REGISTRY` before `./run.sh`. - **Scenario Configuration**: [`utils/_context/_scenarios/k8s_lib_injection.py`](../../utils/_context/_scenarios/k8s_lib_injection.py) - Uses parser to load default versions diff --git a/tests/test_the_test/test_k8s_ci_matrix_labels.py b/tests/test_the_test/test_k8s_ci_matrix_labels.py new file mode 100644 index 00000000000..ca819dc691c --- /dev/null +++ b/tests/test_the_test/test_k8s_ci_matrix_labels.py @@ -0,0 +1,64 @@ +"""GitLab K8s matrix values omit the ECR host. The job puts the registry back.""" + +from utils import pytest, scenarios +from utils._context._scenarios import get_all_scenarios +from utils.k8s.k8s_components_parser import K8sComponentsParser +from utils.scripts.ci_orchestrators.gitlab_exporter import ( + _short_image_ref, + get_k8s_scenario_components, + print_k8s_gitlab_pipeline, +) + +_IMAGE_ENV = ("K8S_INJECTOR_IMG", "K8S_LIB_INIT_IMG", "K8S_WEBLOG_IMG") + + +def _clear_image_env(monkeypatch: pytest.MonkeyPatch) -> None: + for name in _IMAGE_ENV: + monkeypatch.delenv(name, raising=False) + + +@scenarios.test_the_test +def test_short_image_ref_drops_only_the_ecr_host(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.delenv("PRIVATE_DOCKER_REGISTRY", raising=False) + host = "235494822917.dkr.ecr.us-east-1.amazonaws.com" + assert _short_image_ref(f"{host}/ssi/cluster-agent:7.84.3") == "ssi/cluster-agent:7.84.3" + assert _short_image_ref(f"{host}/ssi-dev/cluster-agent-dev:master") == "ssi-dev/cluster-agent-dev:master" + assert _short_image_ref("3.257.1") == "3.257.1" + custom = "my-registry/my-lib-init:my-tag" + assert _short_image_ref(custom) == custom + other_account = "486234852809.dkr.ecr.us-east-1.amazonaws.com/ssi/apm-inject:abc" + assert _short_image_ref(other_account) == other_account + other_path = f"{host}/team/custom:tag" + assert _short_image_ref(other_path) == other_path + + +@scenarios.test_the_test +def test_matrix_job_name_uses_the_image_path(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("CI_PROJECT_NAME", "system-tests") + monkeypatch.setenv("CI_PIPELINE_SOURCE", "push") + monkeypatch.delenv("PRIVATE_DOCKER_REGISTRY", raising=False) + _clear_image_env(monkeypatch) + + matrix = get_k8s_scenario_components("K8S_LIB_INJECTION_PROFILING_ENABLED", get_all_scenarios(), "php") + parser = K8sComponentsParser() + cluster = parser.get_default_component_version("cluster_agent") + helm = parser.get_default_component_version("helm_chart") + + assert matrix["K8S_CLUSTER_IMG"] == cluster.split("/", 1)[1] + assert matrix["K8S_HELM_CHART"] == helm + assert matrix["K8S_INJECTOR_IMG"] == [ + item.split("/", 1)[1] for item in parser.get_all_component_versions("injector") + ] + assert "235494822917" not in str(matrix) + assert "${PRIVATE_DOCKER_REGISTRY}" not in str(matrix) + + pipeline: dict = {"stages": []} + print_k8s_gitlab_pipeline( + "php", + {"K8S_LIB_INJECTION_PROFILING_ENABLED": ["dd-lib-php-init-test-app"]}, + "dev", + pipeline, + ) + entry = pipeline["K8S_LIB_INJECTION_PROFILING_ENABLED"]["parallel"]["matrix"][0] + assert entry["K8S_WEBLOG"] == "dd-lib-php-init-test-app" + assert "K8S_WEBLOG_IMG" not in entry diff --git a/utils/scripts/ci_orchestrators/gitlab_exporter.py b/utils/scripts/ci_orchestrators/gitlab_exporter.py index 273cf14cef7..df2c75ad72f 100644 --- a/utils/scripts/ci_orchestrators/gitlab_exporter.py +++ b/utils/scripts/ci_orchestrators/gitlab_exporter.py @@ -126,6 +126,25 @@ def print_ssi_gitlab_pipeline(language: str, matrix_data: dict[str, dict], ci_en print("Pipeline file generated: ", output_file) +def _short_image_ref(value: str) -> str: + """Drop our registry host from catalog images so the job name keeps the path. + + Only ``ssi/`` and ``ssi-dev/`` images on ``PRIVATE_DOCKER_REGISTRY`` are shortened. + The job adds that same host back. A custom image on any other host stays intact. + """ + host, slash, path = value.partition("/") + registry = os.getenv("PRIVATE_DOCKER_REGISTRY", "235494822917.dkr.ecr.us-east-1.amazonaws.com") + if slash and host == registry and path.startswith(("ssi/", "ssi-dev/")): + return path + return value + + +def _as_job_name(value: str | list[str]) -> str | list[str]: + if isinstance(value, list): + return [_short_image_ref(item) for item in value] + return _short_image_ref(value) + + def get_k8s_scenario_components( scenario: str, all_system_tests_scenarios: list[Scenario], language: str ) -> dict[str, str | list[str]]: @@ -153,7 +172,6 @@ def get_k8s_scenario_components( - Tests ALL available versions (for injector and lib init) - Tests only the pinned versions for the rest of the components - Component Versions Returned: ============================ - K8S_HELM_CHART_OPERATOR: Datadog operator helm chart version (if with_datadog_operator) @@ -162,44 +180,40 @@ def get_k8s_scenario_components( - K8S_HELM_CHART: Datadog helm chart version (if cluster agent without operator) - K8S_LIB_INIT_IMG: Library init container image (always included) + Catalog images on PRIVATE_DOCKER_REGISTRY under ssi/ or ssi-dev/ are returned + without the registry host (for example ``ssi/cluster-agent:7.84.3``). The GitLab + job adds the host back before ./run.sh. Custom images on another host are unchanged. + Args: scenario: Scenario name (must exist in utils/_context/_scenarios/__init__.py) all_system_tests_scenarios: List of all available scenarios language: Programming language (java, python, nodejs, etc.) Returns: - Dictionary with component version assignments for GitLab CI matrix + Dictionary with component version assignments for the GitLab CI matrix """ - # Load all valid scenarios from the scenarios module - all_valid_scenarios = {s.name for s in all_system_tests_scenarios} - + all_valid_scenarios = {item.name for item in all_system_tests_scenarios} if scenario not in all_valid_scenarios: raise ValueError( f"Scenario '{scenario}' not found in utils/_context/_scenarios/__init__.py. " - f"Please ensure the scenario is properly defined." + "Please ensure the scenario is properly defined." ) - # Extract environment variables for CI context ci_project_name = os.getenv("CI_PROJECT_NAME") ci_pipeline_source = os.getenv("CI_PIPELINE_SOURCE") k8s_injector_img = os.getenv("K8S_INJECTOR_IMG") k8s_lib_init_img = os.getenv("K8S_LIB_INIT_IMG") - - # Determine CI context is_system_tests = ci_project_name == "system-tests" is_system_tests_scheduled = is_system_tests and ci_pipeline_source == "schedule" - # Get scenario configuration - scenario_obj = next((s for s in all_system_tests_scenarios if s.name == scenario), None) - with_datadog_operator = getattr(scenario_obj, "with_datadog_operator", False) - with_cluster_agent = getattr(scenario_obj, "with_cluster_agent", False) + scenario_obj = next(item for item in all_system_tests_scenarios if item.name == scenario) + with_datadog_operator = bool(getattr(scenario_obj, "with_datadog_operator", False)) + with_cluster_agent = bool(getattr(scenario_obj, "with_cluster_agent", False)) - # Initialize component parser and result parser = K8sComponentsParser() - components = {} + components: dict[str, str | list[str]] = {} - # Datadog Operator scenario: requires operator helm chart if with_datadog_operator: components["K8S_HELM_CHART_OPERATOR"] = ( parser.get_all_component_versions("helm_chart_operator") @@ -207,21 +221,18 @@ def get_k8s_scenario_components( else parser.get_default_component_version("helm_chart_operator") ) - # Cluster Agent scenarios: require cluster agent and injector if with_cluster_agent: components["K8S_CLUSTER_IMG"] = ( parser.get_all_component_versions("cluster_agent") if is_system_tests_scheduled else parser.get_default_component_version("cluster_agent") ) - components["K8S_INJECTOR_IMG"] = k8s_injector_img or ( parser.get_all_component_versions("injector") if is_system_tests else parser.get_default_component_version("injector") ) - # Non-operator cluster scenarios: require standard helm chart if with_cluster_agent and not with_datadog_operator: components["K8S_HELM_CHART"] = ( parser.get_all_component_versions("helm_chart") @@ -229,44 +240,37 @@ def get_k8s_scenario_components( else parser.get_default_component_version("helm_chart") ) - # Library init: required for all scenarios components["K8S_LIB_INIT_IMG"] = k8s_lib_init_img or ( parser.get_all_component_versions("lib_init", language) if is_system_tests else parser.get_default_component_version("lib_init", language) ) - return components + return {key: _as_job_name(value) for key, value in components.items()} def print_k8s_gitlab_pipeline( - language: str, k8s_matrix: dict[str, dict], ci_environment: str, result_pipeline: dict + language: str, k8s_matrix: dict[str, list[str]], ci_environment: str, result_pipeline: dict ) -> None: result_pipeline["stages"].append("K8S_LIB_INJECTION") - all_system_tests_scenarios = get_all_scenarios() - # Create the jobs by scenario. + scenarios = get_all_scenarios() + # Same image for every weblog when the caller sets it. Kept off the matrix so the + # registry host is not part of the job name. Otherwise the job builds it from K8S_WEBLOG. + weblog_image = os.getenv("K8S_WEBLOG_IMG") for scenario, weblogs in k8s_matrix.items(): - job = scenario - result_pipeline[job] = {} - result_pipeline[job]["extends"] = ".k8s_lib_injection_base" - # Job variables - result_pipeline[job]["variables"] = {} - result_pipeline[job]["variables"]["TEST_LIBRARY"] = language - result_pipeline[job]["variables"]["K8S_SCENARIO"] = scenario - result_pipeline[job]["variables"]["REPORT_ENVIRONMENT"] = ci_environment - - result_pipeline[job]["parallel"] = {"matrix": []} - for weblog_name in weblogs: - k8s_weblog_img = os.getenv("K8S_WEBLOG_IMG", "${PRIVATE_DOCKER_REGISTRY}" + f"/system-tests/{weblog_name}") - - # Build matrix entry - only include components that have values - matrix_entry = { - "K8S_WEBLOG": weblog_name, - "K8S_WEBLOG_IMG": k8s_weblog_img, - } - matrix_entry.update(get_k8s_scenario_components(scenario, all_system_tests_scenarios, language)) - - result_pipeline[job]["parallel"]["matrix"].append(matrix_entry) + variables = { + "TEST_LIBRARY": language, + "K8S_SCENARIO": scenario, + "REPORT_ENVIRONMENT": ci_environment, + } + if weblog_image: + variables["K8S_WEBLOG_IMG"] = weblog_image + components = get_k8s_scenario_components(scenario, scenarios, language) + result_pipeline[scenario] = { + "extends": ".k8s_lib_injection_base", + "variables": variables, + "parallel": {"matrix": [{"K8S_WEBLOG": weblog, **components} for weblog in weblogs]}, + } def print_docker_ssi_gitlab_pipeline(