"""Main application.""" import re from typing import Any, Dict, List import sentry_sdk from datadog_api_client.v2.model.state import State from lambdacommon.common_config import logger from packaging import version from sentry_sdk.integrations.aws_lambda import AwsLambdaIntegration import config from common.datadog_utils import ( list_frontends_with_details, list_services_with_details, update_scorecard_non_service_outcomes, update_scorecard_service_outcomes_batch, ) if config.SENTRY_DSN: sentry_sdk.init( dsn=config.SENTRY_DSN, environment=config.ENVIRONMENT, integrations=[AwsLambdaIntegration(timeout_warning=True)], ) def handler(event: Dict[str, Any], context: Any) -> None: """Lambda handler.""" if not config.DD_API_KEY or not config.DD_APP_KEY: logger.error("Missing DD_API_KEY or DD_APP_KEY") raise ValueError("Missing DD_API_KEY or DD_APP_KEY") logger.info("Fetching services from catalog...") services = list_services_with_details(config.DD_API_KEY, config.DD_APP_KEY) logger.info(f"Found {len(services)} services.") logger.info("Fetching frontends from catalog...") frontends = list_frontends_with_details(config.DD_API_KEY, config.DD_APP_KEY) logger.info(f"Found {len(frontends)} frontends.") service_outcomes: List[Dict[str, Any]] = [] non_service_outcomes: List[Dict[str, Any]] = [] for entity in services + frontends: entity_name = entity["name"] entity_kind = entity["kind"] is_service = entity_kind == "service" exclusion_reason = _find_exclusion(entity_name) if exclusion_reason: _record_outcome( service_outcomes, non_service_outcomes, is_service, entity_name, entity_kind, config.SCORECARD_RULE_ID, State.SKIP, exclusion_reason, ) continue """ This should always be a list with a single language but handle multiple just in case """ entity_langs = [language.lower() for language in entity.get("languages", []) if language] entity_tags = entity.get("tags", []) # Find if entity uses a supported language matched_lang = None for lang in entity_langs: if lang in config.MINIMUM_SUPPORTED_VERSIONS: matched_lang = lang break if not matched_lang: _record_outcome( service_outcomes, non_service_outcomes, is_service, entity_name, entity_kind, config.SCORECARD_RULE_ID, State.FAIL, "Service not one of the checked languages: " f"{', '.join(config.MINIMUM_SUPPORTED_VERSIONS.keys())}", ) continue language_version = None for tag in entity_tags: if tag.startswith("language_version:"): language_version = tag.split(":", 1)[1] break if not language_version: _record_outcome( service_outcomes, non_service_outcomes, is_service, entity_name, entity_kind, config.SCORECARD_RULE_ID, State.FAIL, "Missing 'language_version' tag", ) continue # Compare Version min_version_str = config.MINIMUM_SUPPORTED_VERSIONS[matched_lang] try: clean_version = language_version.lstrip("v").lstrip("V") parsed_version = version.parse(clean_version) parsed_required_version = version.parse(min_version_str) if parsed_version >= parsed_required_version: _record_outcome( service_outcomes, non_service_outcomes, is_service, entity_name, entity_kind, config.SCORECARD_RULE_ID, State.PASS, f"{matched_lang} version {language_version} >= {min_version_str}", # noqa: E501 ) else: _record_outcome( service_outcomes, non_service_outcomes, is_service, entity_name, entity_kind, config.SCORECARD_RULE_ID, State.FAIL, f"{matched_lang} version {language_version} < {min_version_str}", # noqa: E501 ) except Exception as e: logger.error(f"Error parsing version for {entity_name}: {e}") _record_outcome( service_outcomes, non_service_outcomes, is_service, entity_name, entity_kind, config.SCORECARD_RULE_ID, State.FAIL, f"Invalid version format: {language_version}", ) logger.info( f"Updating scorecard outcomes: {len(service_outcomes)} services, " f"{len(non_service_outcomes)} non-service entities..." ) update_scorecard_service_outcomes_batch(config.DD_API_KEY, config.DD_APP_KEY, service_outcomes) update_scorecard_non_service_outcomes(config.DD_API_KEY, config.DD_APP_KEY, non_service_outcomes) logger.info( f"Scorecard outcomes updated for {len(service_outcomes)} services " f"and {len(non_service_outcomes)} non-service entities." ) def _find_exclusion(service_name: str) -> str | None: """Return exclusion reason if service is excluded, else None.""" for entry in config.EXCLUDED_SERVICES: if re.fullmatch(entry["pattern"], service_name): return entry["reason"] return None def _record_outcome( service_outcomes: List[Dict[str, Any]], non_service_outcomes: List[Dict[str, Any]], is_service: bool, entity_name: str, entity_kind: str, rule_id: str, state: State, remarks: str, ) -> None: """Append an outcome to the appropriate list based on entity kind.""" outcome = {"rule_id": rule_id, "state": state, "remarks": remarks} if is_service: service_outcomes.append({**outcome, "service_name": entity_name}) else: entity_ref = f"{entity_kind}:{entity_name}" non_service_outcomes.append({**outcome, "entity_reference": entity_ref}) if __name__ == "__main__": handler({}, {})