import json from typing import Annotated, Optional import typer from ddtrace import tracer from rich import print from datadog_tools import config from datadog_tools.actions.software_catalog import SoftwareCatalog from datadog_tools.connectors.aws import AWS from datadog_tools.connectors.datadog import Datadog from datadog_tools.connectors.sentry import Sentry from datadog_tools.connectors.sonar import Sonar from datadog_tools.connectors.swagger import Swagger from datadog_tools.models.software_catalog import KindEnum from datadog_tools.utils.files import get_entity_kind_from_entity_definition_file, get_full_entity_definition_file_path app: typer.Typer = typer.Typer() @app.command() @tracer.wrap(service="datadog-tools", resource="software_catalog.create_service") def create_service( repository_url: Annotated[str, typer.Option(help="The URL of the service's GitHub repository")], repository_path: Annotated[ str, typer.Option(help="The path to the service's GitHub repository") ] = "/var/app/repository", service_definition_file_path: Annotated[ Optional[str], typer.Option( help="The path to the service definition file within the repository. \ For directories containing more than one service definition file" ), ] = None, service_path: Annotated[str, typer.Option(help="The path to the service code within the repository")] = "", pipeline_url: Annotated[ Optional[str], typer.Option(help="The URL of the CI pipeline for the service, if applicable."), ] = None, dry_run: Annotated[bool, typer.Option(help="Set this to skip the actual update of the Software Catalog")] = False, fetch_sentry_metadata: Annotated[ bool, typer.Option(help="Whether or not to attempt to fetch Sentry metadata for the service") ] = True, fetch_sonar_metadata: Annotated[ bool, typer.Option(help="Whether or not to attempt to fetch Sonar metadata for the service") ] = True, fetch_swagger_metadata: Annotated[ bool, typer.Option(help="Whether or not to attempt to fetch Swagger metadata for the service") ] = True, fetch_aws_metadata: Annotated[ bool, typer.Option(help="Whether or not to attempt to fetch AWS metadata for the service") ] = True, ): """Create or update a service in the software catalog.""" print(f'Creating or updating service using repository path "{repository_path}" and service path "{service_path}"') sentry = ( Sentry(api_token=config.SENTRY_AUTH_TOKEN, organization=config.SENTRY_ORGANIZATION) if fetch_sentry_metadata else None ) sonar = Sonar(api_token=config.SONAR_TOKEN, base_url=config.SONAR_BASE_URL) if fetch_sonar_metadata else None swagger = Swagger(base_url=config.SWAGGER_URL) if fetch_swagger_metadata else None aws = ( AWS(resource_explorer_view_arn=config.RESOURCE_EXPLORER_VIEW_ARN, region=config.AWS_REGION) if fetch_aws_metadata else None ) with Datadog(api_key=config.DD_API_KEY, app_key=config.DD_APP_KEY) as datadog: software_catalog = SoftwareCatalog(datadog=datadog, sentry=sentry, sonar=sonar, swagger=swagger, aws=aws) return software_catalog.upsert_service( repository_url=repository_url, repository_path=repository_path, service_definition_file_path=service_definition_file_path, service_path=service_path, pipeline_url=pipeline_url, dry_run=dry_run, ) @app.command() @tracer.wrap(service="datadog-tools", resource="software_catalog.create_library") def create_library( repository_url: Annotated[str, typer.Option(help="The URL of the library's GitHub repository")], repository_path: Annotated[ str, typer.Option(help="The path to the library's GitHub repository") ] = "/var/app/repository", library_definition_file_path: Annotated[ Optional[str], typer.Option( help="The path to the library definition file within the repository. \ For directories containing more than one library definition file" ), ] = None, library_path: Annotated[str, typer.Option(help="The path to the library code within the repository")] = "", pipeline_url: Annotated[ Optional[str], typer.Option(help="The URL of the CI pipeline for the service, if applicable."), ] = None, dry_run: Annotated[bool, typer.Option(help="Set this to skip the actual update of the Software Catalog")] = False, ): """Create or update a library in the software catalog.""" print(f'Creating or updating library using repository path "{repository_path}" and library path "{library_path}"') with Datadog(api_key=config.DD_API_KEY, app_key=config.DD_APP_KEY) as datadog: software_catalog = SoftwareCatalog(datadog) return software_catalog.upsert_library( repository_url=repository_url, repository_path=repository_path, library_definition_file_path=library_definition_file_path, library_path=library_path, pipeline_url=pipeline_url, dry_run=dry_run, ) @app.command() @tracer.wrap(service="datadog-tools", resource="software_catalog.validate_service") def validate_service( repository_path: Annotated[ str, typer.Option(help="The path to the service's GitHub repository") ] = "/var/app/repository", service_definition_file_path: Annotated[ Optional[str], typer.Option( help="The path to the service definition file within the repository. \ For directories containing more than one service definition file" ), ] = None, service_path: Annotated[str, typer.Option(help="The path to the service code within the repository")] = "", ): """Validate a service definition in the software catalog.""" print(f'Validating service definition at repository path "{repository_path}" and service path "{service_path}"') with Datadog(api_key=config.DD_API_KEY, app_key=config.DD_APP_KEY) as datadog: software_catalog = SoftwareCatalog(datadog) software_catalog.validate_service( repository_path=repository_path, service_definition_file_path=service_definition_file_path, service_path=service_path, ) @app.command() @tracer.wrap(service="datadog-tools", resource="software_catalog.validate_library") def validate_library( repository_path: Annotated[ str, typer.Option(help="The path to the library's GitHub repository") ] = "/var/app/repository", library_definition_file_path: Annotated[ Optional[str], typer.Option( help="The path to the library definition file within the repository. \ For directories containing more than one library definition file" ), ] = None, library_path: Annotated[str, typer.Option(help="The path to the library code within the repository")] = "", ): """Validate a library definition in the software catalog.""" print(f'Validating library definition at repository path "{repository_path}" and library path "{library_path}"') with Datadog(api_key=config.DD_API_KEY, app_key=config.DD_APP_KEY) as datadog: software_catalog = SoftwareCatalog(datadog) software_catalog.validate_library( repository_path=repository_path, library_definition_file_path=library_definition_file_path, library_path=library_path, ) @app.command() @tracer.wrap(service="datadog-tools", resource="software_catalog.create_frontend") def create_frontend( repository_url: Annotated[str, typer.Option(help="The URL of the frontend's GitHub repository")], repository_path: Annotated[ str, typer.Option(help="The path to the frontend's GitHub repository") ] = "/var/app/repository", frontend_definition_file_path: Annotated[ Optional[str], typer.Option( help="The path to the frontend definition file within the repository. \ For directories containing more than one frontend definition file" ), ] = None, frontend_path: Annotated[str, typer.Option(help="The path to the frontend code within the repository")] = "", pipeline_url: Annotated[ Optional[str], typer.Option(help="The URL of the CI pipeline for the frontend, if applicable."), ] = None, dry_run: Annotated[bool, typer.Option(help="Set this to skip the actual update of the Software Catalog")] = False, ): """Create or update a frontend in the software catalog.""" print(f'Creating/updating frontend at repository "{repository_path}" with frontend path "{frontend_path}"') with Datadog(api_key=config.DD_API_KEY, app_key=config.DD_APP_KEY) as datadog: software_catalog = SoftwareCatalog(datadog) return software_catalog.upsert_frontend( repository_url=repository_url, repository_path=repository_path, frontend_definition_file_path=frontend_definition_file_path, frontend_path=frontend_path, pipeline_url=pipeline_url, dry_run=dry_run, ) @app.command() @tracer.wrap(service="datadog-tools", resource="software_catalog.validate_frontend") def validate_frontend( repository_path: Annotated[ str, typer.Option(help="The path to the frontend's GitHub repository") ] = "/var/app/repository", frontend_definition_file_path: Annotated[ Optional[str], typer.Option( help="The path to the frontend definition file within the repository. \ For directories containing more than one frontend definition file" ), ] = None, frontend_path: Annotated[str, typer.Option(help="The path to the frontend code within the repository")] = "", ): """Validate a frontend definition in the software catalog.""" print(f'Validating frontend definition at repository path "{repository_path}" and frontend path "{frontend_path}"') with Datadog(api_key=config.DD_API_KEY, app_key=config.DD_APP_KEY) as datadog: software_catalog = SoftwareCatalog(datadog) software_catalog.validate_frontend( repository_path=repository_path, frontend_definition_file_path=frontend_definition_file_path, frontend_path=frontend_path, ) @app.command() @tracer.wrap(service="datadog-tools", resource="software_catalog.list_services") def list_services( raw: Annotated[ bool, typer.Option("--raw", help="Do not filter by the extension 'sonymusic-pde.com/metadata-source'") ] = False, raw_json: Annotated[bool, typer.Option("--raw-json", help="Do not prettify the json output")] = False, ): """ List SoftwareCatalog services and prints them as a json. By default only returns services which have extension 'sonymusic-pde.com/metadata-source'. """ with Datadog(api_key=config.DD_API_KEY, app_key=config.DD_APP_KEY) as datadog: software_catalog = SoftwareCatalog(datadog) services = software_catalog.list_entities(kind=KindEnum.service, raw=raw) if raw_json: print(json.dumps(services)) else: print(json.dumps(services, indent=4)) return services @app.command() @tracer.wrap(service="datadog-tools", resource="software_catalog.list_libraries") def list_libraries( raw: Annotated[ bool, typer.Option("--raw", help="Do not filter by the extension 'sonymusic-pde.com/metadata-source'") ] = False, raw_json: Annotated[bool, typer.Option("--raw-json", help="Do not prettify the json output")] = False, ): """ List SoftwareCatalog libraries and prints them as a json to stdout. By default only returns libraries which have extension 'sonymusic-pde.com/metadata-source'. """ with Datadog(api_key=config.DD_API_KEY, app_key=config.DD_APP_KEY) as datadog: software_catalog = SoftwareCatalog(datadog) libraries = software_catalog.list_entities(kind=KindEnum.library, raw=raw) if raw_json: print(json.dumps(libraries)) else: print(json.dumps(libraries, indent=4)) return libraries @app.command() @tracer.wrap(service="datadog-tools", resource="software_catalog.list_frontends") def list_frontends( raw: Annotated[ bool, typer.Option("--raw", help="Do not filter by the extension 'sonymusic-pde.com/metadata-source'") ] = False, raw_json: Annotated[bool, typer.Option("--raw-json", help="Do not prettify the json output")] = False, ): """ List SoftwareCatalog frontends and prints them as a json to stdout. By default only returns frontends which have extension 'sonymusic-pde.com/metadata-source'. """ with Datadog(api_key=config.DD_API_KEY, app_key=config.DD_APP_KEY) as datadog: software_catalog = SoftwareCatalog(datadog) frontends = software_catalog.list_entities(kind=KindEnum.frontend, raw=raw) if raw_json: print(json.dumps(frontends)) else: print(json.dumps(frontends, indent=4)) return frontends @app.command() @tracer.wrap(service="datadog-tools", resource="software_catalog.list_systems") def list_systems( raw_json: Annotated[bool, typer.Option("--raw-json", help="Do not prettify the json output")] = False, ): """ List SoftwareCatalog systems and prints them as a json to stdout. """ with Datadog(api_key=config.DD_API_KEY, app_key=config.DD_APP_KEY) as datadog: software_catalog = SoftwareCatalog(datadog) # systems don't have metadata extension, so always set raw to true systems = software_catalog.list_entities(kind=KindEnum.system, raw=True) if raw_json: print(json.dumps(systems)) else: print(json.dumps(systems, indent=4)) return systems @app.command() @tracer.wrap(service="datadog-tools", resource="software_catalog.get_service") def get_service( name: Annotated[str, typer.Option(help="The name of the service in software catalog.")], raw_json: Annotated[bool, typer.Option("--raw-json", help="Do not prettify the json output")] = False, ): """Get the schema of the SoftwareCatalog service and print it as a json to stdout.""" with Datadog(api_key=config.DD_API_KEY, app_key=config.DD_APP_KEY) as datadog: software_catalog = SoftwareCatalog(datadog) schema = software_catalog.get_entity_schema(name=name, kind=KindEnum.service) if raw_json: print(json.dumps(schema)) else: print(json.dumps(schema, indent=4)) return schema @app.command() @tracer.wrap(service="datadog-tools", resource="software_catalog.get_library") def get_library( name: Annotated[str, typer.Option(help="The name of the library in software catalog.")], raw_json: Annotated[bool, typer.Option("--raw-json", help="Do not prettify the json output")] = False, ): """Get the schema of the SoftwareCatalog library and print it as a json to stdout.""" with Datadog(api_key=config.DD_API_KEY, app_key=config.DD_APP_KEY) as datadog: software_catalog = SoftwareCatalog(datadog) schema = software_catalog.get_entity_schema(name=name, kind=KindEnum.library) if raw_json: print(json.dumps(schema)) else: print(json.dumps(schema, indent=4)) return schema @app.command() @tracer.wrap(service="datadog-tools", resource="software_catalog.get_frontend") def get_frontend( name: Annotated[str, typer.Option(help="The name of the frontend in software catalog.")], raw_json: Annotated[bool, typer.Option("--raw-json", help="Do not prettify the json output")] = False, ): """Get the schema of the SoftwareCatalog frontend and print it as a json to stdout.""" with Datadog(api_key=config.DD_API_KEY, app_key=config.DD_APP_KEY) as datadog: software_catalog = SoftwareCatalog(datadog) schema = software_catalog.get_entity_schema(name=name, kind=KindEnum.frontend) if raw_json: print(json.dumps(schema)) else: print(json.dumps(schema, indent=4)) return schema @app.command() @tracer.wrap(service="datadog-tools", resource="software_catalog.get_system") def get_system( name: Annotated[str, typer.Option(help="The name of the system in software catalog.")], raw_json: Annotated[bool, typer.Option("--raw-json", help="Do not prettify the json output")] = False, ): """Get the schema of the SoftwareCatalog system and print it as a json to stdout.""" with Datadog(api_key=config.DD_API_KEY, app_key=config.DD_APP_KEY) as datadog: software_catalog = SoftwareCatalog(datadog) schema = software_catalog.get_entity_schema(name=name, kind=KindEnum.system) if raw_json: print(json.dumps(schema)) else: print(json.dumps(schema, indent=4)) return schema @app.command() @tracer.wrap(service="datadog-tools", resource="software_catalog.create_entity") def create_entity( repository_url: Annotated[str, typer.Option(help="The URL of the entity's GitHub repository")], repository_path: Annotated[ str, typer.Option(help="The path to the entity's GitHub repository") ] = "/var/app/repository", entity_definition_file_path: Annotated[ Optional[str], typer.Option( help="The path to the entity definition file within the repository. \ For directories containing more than one entity definition file" ), ] = None, entity_path: Annotated[str, typer.Option(help="The path to the entity code within the repository")] = "", pipeline_url: Annotated[ Optional[str], typer.Option(help="The URL of the CI pipeline for the entity, if applicable."), ] = None, dry_run: Annotated[bool, typer.Option(help="Set this to skip the actual update of the Software Catalog")] = False, fetch_sentry_metadata: Annotated[ bool, typer.Option(help="Whether or not to attempt to fetch Sentry metadata for the entity") ] = True, fetch_sonar_metadata: Annotated[ bool, typer.Option(help="Whether or not to attempt to fetch Sonar metadata for the entity") ] = True, fetch_swagger_metadata: Annotated[ bool, typer.Option(help="Whether or not to attempt to fetch Swagger metadata for the entity") ] = True, fetch_aws_metadata: Annotated[ bool, typer.Option(help="Whether or not to attempt to fetch AWS metadata for the entity") ] = True, ): """ Create or update an entity in the software catalog. """ full_entity_definition_path = get_full_entity_definition_file_path( repository_path, entity_path, entity_definition_file_path ) entity_kind = get_entity_kind_from_entity_definition_file(full_entity_definition_path) if not entity_kind: raise ValueError(f"Could not determine entity kind from definition file at {full_entity_definition_path}") if entity_kind == KindEnum.service.value: return create_service( repository_url=repository_url, repository_path=repository_path, service_definition_file_path=entity_definition_file_path, service_path=entity_path, pipeline_url=pipeline_url, dry_run=dry_run, fetch_sentry_metadata=fetch_sentry_metadata, fetch_sonar_metadata=fetch_sonar_metadata, fetch_swagger_metadata=fetch_swagger_metadata, fetch_aws_metadata=fetch_aws_metadata, ) elif entity_kind == KindEnum.library.value: return create_library( repository_url=repository_url, repository_path=repository_path, library_definition_file_path=entity_definition_file_path, library_path=entity_path, pipeline_url=pipeline_url, dry_run=dry_run, ) elif entity_kind == KindEnum.frontend.value: return create_frontend( repository_url=repository_url, repository_path=repository_path, frontend_definition_file_path=entity_definition_file_path, frontend_path=entity_path, pipeline_url=pipeline_url, dry_run=dry_run, ) else: raise ValueError(f"Unsupported entity kind: {entity_kind}") @app.command() @tracer.wrap(service="datadog-tools", resource="software_catalog.validate_entity") def validate_entity( repository_path: Annotated[ str, typer.Option(help="The path to the entity's GitHub repository") ] = "/var/app/repository", entity_definition_file_path: Annotated[ Optional[str], typer.Option( help="The path to the entity definition file within the repository. \ For directories containing more than one entity definition file" ), ] = None, entity_path: Annotated[str, typer.Option(help="The path to the entity code within the repository")] = "", ): """ Validate an entity definition in the software catalog. """ full_entity_definition_path = get_full_entity_definition_file_path( repository_path, entity_path, entity_definition_file_path ) entity_kind = get_entity_kind_from_entity_definition_file(full_entity_definition_path) if not entity_kind: raise ValueError(f"Could not determine entity kind from definition file at {full_entity_definition_path}") if entity_kind == KindEnum.service.value: return validate_service( repository_path=repository_path, service_definition_file_path=entity_definition_file_path, service_path=entity_path, ) elif entity_kind == KindEnum.library.value: return validate_library( repository_path=repository_path, library_definition_file_path=entity_definition_file_path, library_path=entity_path, ) elif entity_kind == KindEnum.frontend.value: return validate_frontend( repository_path=repository_path, frontend_definition_file_path=entity_definition_file_path, frontend_path=entity_path, ) else: raise ValueError(f"Unsupported entity kind: {entity_kind}")