Skip to content

Resource cleanup

Resource cleanup scope

The "resource cleanup" described below covers removing images around execution environments, custom models, custom apps, and custom jobs in DataRobot. Images are the only resource type supported.

This section describes the administrative operations that clean up DataRobot resources that are no longer required.

These cleanup operations follow a two-step pattern—scan and cleanup—which adds flexibility in how the end-cleanup will be performed.

The scan step identifies resources that match one of the cleanup rules (for example, images whose linked entity has been deleted), making them eligible to be removed. DataRobot queries various data sources to find them, and the identified resources are then compiled into a report.

The cleanup step loops through the report produced by scan and cleans the resources according to the type of cleanup being performed.

Cleanup options are:

  1. Cleanup with human supervision: The scan produces a report that can be manually analyzed before triggering the cleanup.

  2. Fully automated: scan and cleanup are called programmatically, one after the other.

In addition to adding flexibility, the scan/cleanup pattern also adds robustness and traceability to critical cleanup operations. Because the scan step only identifies cleanup opportunities, it also acts as a safeguard—admins cannot call the cleanup action if the scan did not finish successfully. This means if there are some accidental deletions, the report linked to the scan will allow for quick troubleshooting to help understand the resources removed by this endpoint.

Access requirements

These cleanup operations are exposed via a public API that requires administrative rights due to the nature of perma-deletion operations.

The resource cleanup endpoints are available when the ENABLE_RESOURCE_IMAGE_CLEANUP configuration variable is set in core.config_env_vars, which is the default in the DataRobot Helm chart. In addition, the administrator calling the API must have the Enable Resource Cleanup for Custom Models/Apps/Environments permission (ENABLE_RESOURCE_IMAGE_CLEANUP) enabled on their user. The cleanup cronjob authenticates as a system user and does not need this permission.

The following five endpoints are available per cleanup type:

Method Endpoint Description
GET /api/v2/<cleanup-type>Cleanups/ List the available scans.
POST /api/v2/<cleanup-type>Cleanups/ Trigger a scan job to identify resources that could be cleaned up for the given cleanup type. This endpoint triggers an async job to perform the actual scan, and returns immediately with the <scan-id>, whose status can be polled via the GET endpoint.
GET /api/v2/<cleanup-type>Cleanups/<scan-id> Get the status of a current scan, useful for polling scan and cleanup statuses.
GET /api/v2/<cleanup-type>Cleanups/<scan-id>/report Retrieve the report produced by the scan, which lists the identified resources. If the cleanup was performed, the report will also contain information related to the performed cleanups.
POST /api/v2/<cleanup-type>Cleanups/<scan-id> Trigger a cleanup job to clean up resources as described in the report. This triggers an async job that will run the actual cleanup, and then immediately returns the <run-id>, which helps identify the run and follows its status. To retrieve the status, use the GET endpoint, similar to how the scan's status is polled.

There are two types of resource cleanup supported:

  1. image: To help remove images that are no longer used by DataRobot.
  2. cve: To help remediate CVE images by identifying which DataRobot entities use the CVE image, and then cleanup those images if possible.

The report

Every successfully executed scan produces a report that can be retrieved via /api/v2/<cleanup-type>Cleanups/<scan-id>/report. The report shares a common schema across all cleanup types, however, some nested structures will depend on the type of cleanup. The report is updated after each cleanup run, including runs that fail; resources that could not be removed are marked cleanupFailed with the error message.

{
    "schema_version": "1.0",
    "scanDetails": {  // Contains meta-information around a single scan.
        "type": "image_cleanup_scan",
        "scanId": "d9923b9a-837b-4917-93d5-0e7bf30ecb80",
        "scanTimestamp": "2025-09-29T07:49:37.228794+00:00",
        "summary": {}, // Summary of the scan, which will give a sense of the entities processed. Its content depends on the cleanup type.
        "parameters": {}  // The parameters configured for the cleanup scan. Its content depends on the cleanup type.
    },
    "identifiedResources": [],  // List of resources identified during scan. Its content depends on the cleanup type.
    "cleanupRunsHistory": []  // Record of the "cleanups" performed. Its content depends on the cleanup type.
}

Resource cleanup client

While there is no officially supported client for resource cleanup, the following code provides a reference implementation for interacting with the resource cleanup endpoints:

import requests
import os
import time
import logging
from typing import Dict, List, Optional

API_BASE_URL = f"https://app.datarobot.com/api/v2"
API_KEY = os.environ["DR_API_KEY"]


class ResourceCleanupClient:
    """Client for the DataRobot resource cleanup API."""

    CLEANUP_TYPE_TO_ENDPOINT = {
        "image": "imageCleanups/",
        "cve": "cveCleanups/",  
    }

    def __init__(self, api_base_url: str, api_key: str, cleanup_type: str):
        self.api_base_url = api_base_url
        self.api_key = api_key
        self.cleanup_type = cleanup_type
        self.endpoint = f"{self.api_base_url}/{self.CLEANUP_TYPE_TO_ENDPOINT[cleanup_type]}"
        self.headers = {"Authorization": f"Bearer {self.api_key}"}
        self.timeout = 30
        self.logger = logging.getLogger(f"{__name__}.{self.__class__.__name__}")

    def list_scans(self) -> List[Dict]:
        """List all existing cleanup scans."""
        self.logger.info(f"Listing {self.cleanup_type} cleanup scans...")
        response = requests.get(
            self.endpoint, headers=self.headers, timeout=self.timeout
        )
        response.raise_for_status()
        scans = response.json()
        self.logger.info(f"Found {scans.get('totalCount', 0)} existing scans")
        return scans

    def trigger_scan(self, scan_params: Dict) -> str:
        """Trigger a new cleanup scan.

        Returns:
            scan_id: The ID of the triggered scan
        """
        self.logger.info(f"Triggering {self.cleanup_type} cleanup scan...")
        self.logger.debug(f"Scan parameters: {scan_params}")

        response = requests.post(
            self.endpoint, 
            headers=self.headers, 
            json=scan_params,
            timeout=self.timeout
        )
        response.raise_for_status()

        scan_data = response.json()
        scan_id = scan_data["scanId"]
        self.logger.info(f"Scan triggered successfully. Scan ID: {scan_id}")
        return scan_id

    def get_scan_info(self, scan_id: str) -> Dict:
        """Get the current status of a scan."""
        response = requests.get(
            f"{self.endpoint}{scan_id}",
            headers=self.headers,
            timeout=self.timeout
        )
        response.raise_for_status()
        return response.json()

    def poll_completion(self, scan_id: str, mode: str, max_wait_time: int = 1800, poll_interval: int = 5) -> Dict:
        """Poll scan status until completion.

        Args:
            scan_id: The scan ID to poll
            mode: The mode to poll for, either "scan" or "cleanup"
            max_wait_time: Maximum time to wait in seconds (default: 30 minutes)
            poll_interval: Time between polls in seconds (default: 5 seconds)

        Returns:
            Final scan status
        """
        self.logger.info(f"Polling {mode} status for scan {scan_id}...")
        start_time = time.time()

        while time.time() - start_time < max_wait_time:
            scan_info = self.get_scan_info(scan_id)
            if mode == "scan":
                status = scan_info["scanStatus"]
            elif mode == "cleanup":
                status = scan_info["cleanupStatus"]
            else:
                raise ValueError(f"Invalid mode: {mode}")
            self.logger.debug(f"{mode.capitalize()} status: {status}")

            if status == "completed":
                self.logger.info(f"{mode.capitalize()} completed successfully!")
                return scan_info
            elif status == "error":
                error_msg = scan_info.get('errorMessage', 'Unknown error')
                self.logger.error(f"{mode.capitalize()} failed with error: {error_msg}")
                raise Exception(f"{mode.capitalize()} failed with error: {error_msg}")

            time.sleep(poll_interval)

        self.logger.error(f"{mode.capitalize()} did not complete within {max_wait_time} seconds")
        raise Exception(f"{mode.capitalize()} did not complete within {max_wait_time} seconds")

    def get_scan_report(self, scan_id: str) -> Dict:
        """Retrieve the scan report."""
        self.logger.info(f"Retrieving scan report for scan {scan_id}...")
        response = requests.get(
            f"{self.endpoint}{scan_id}/report",
            headers=self.headers,
            timeout=self.timeout
        )
        response.raise_for_status()
        return response.json()

    def trigger_cleanup(self, scan_id: str) -> str:
        """Trigger cleanup for a completed scan.

        Args:
            scan_id: The scan ID to trigger cleanup for

        Returns:
            run_id: The ID of the cleanup run
        """
        self.logger.info(f"Triggering cleanup for scan {scan_id}...")
        response = requests.post(
            f"{self.endpoint}{scan_id}/cleanup",
            headers=self.headers,
            timeout=self.timeout
        )
        response.raise_for_status()

        cleanup_data = response.json()
        run_id = cleanup_data["cleanupRunId"]
        self.logger.info(f"Cleanup triggered successfully. Run ID: {run_id}")
        return run_id

    def run_resource_cleanup(self, scan_params: Dict, trigger_cleanup: bool = False) -> Dict:
        """Run the complete resource cleanup workflow as documented.

        Args:
            scan_params: Parameters for the scan
            trigger_cleanup: Whether to trigger cleanup after scan

        Returns:
            Final report with scan and cleanup results
        """

        # Step 1: Trigger scan
        scan_id = self.trigger_scan(scan_params)

        # Step 2: Poll scan completion
        self.poll_completion(scan_id, mode="scan")

        # Step 3: Get and validate scan report
        scan_report = self.get_scan_report(scan_id)

        identified_resources = scan_report["identifiedResources"]
        eligible_resources = [r for r in identified_resources if r["status"] == "eligibleForCleanup"]

        if not eligible_resources:
            self.logger.info("No resources eligible for cleanup found.")
            return scan_report

        if not trigger_cleanup:
            self.logger.info(f"Found {len(eligible_resources)} resources eligible for cleanup, but cleanup not triggered (trigger_cleanup=False)")
            return scan_report

        # Step 4: Trigger cleanup
        self.trigger_cleanup(scan_id)

        # Step 5: Poll cleanup completion
        self.poll_completion(scan_id, mode="cleanup")

        # Step 6: Get final report and analyze cleanup results
        final_report = self.get_scan_report(scan_id)
        self.logger.info("Resource cleanup workflow completed successfully")

        return final_report

See below for examples on how to use this client for each cleanup type:

Image cleanup (/api/v2/imageCleanups)

This cleanup type removes images produced by DataRobot that are no longer needed.

Automatic cleanup (cronjob configuration)

Because the custom-models-image-cleanup cronjob calls the /api/v2/imageCleanups endpoint daily, users are not required to run it manually. The cronjob removes images that are no longer needed.

The automatic cleanup cronjob can be configured through the Helm values.yaml file. Below are the available configuration options:

Enable or disable the cronjob

The job is disabled by default. To enable it:

custom-models:
  jobs:
    customModelsImageCleanup:
      enabled: true

Set enabled: false to disable it again.

Schedule configuration

The cronjob runs on a configurable schedule using standard cron syntax. To change the frequency:

custom-models:
  jobs:
    customModelsImageCleanup:
      schedule: "0 0 * * *"  # Daily at midnight (default)
      # schedule: "0 2 * * 0"  # Weekly on Sunday at 2 AM
      # schedule: "0 2 1 * *"  # Monthly on the 1st at 2 AM

Cleanup parameters configuration

You can customize which resources are cleaned up by configuring the cleanup parameters:

custom-models:
  jobs:
    customModelsImageCleanup:
      # Cleanup rules to apply (empty list = use API defaults)
      cleanupRules: 
        - "entityIsDeleted"
        - "imageNotInDataRobotDB"
        - "lifecycleExpired"

      # Entity types to scan (empty list = scan all entity types)
      entityTypes:
        - "environment"
        - "customModel"
        - "customApp"
        - "customJob"

      # Image repositories to scan (empty list = scan all repositories)
      imageRepositories:
        - "custom-models/managed-image"
        - "base-image"
        - "custom-apps/managed-image"

Removals per run

Each cleanup run removes at most RESOURCE_CLEANUP_IMAGE_MAX_N_REMOVALS_CLEANUP images (100 by default in the Helm chart). Images identified by the scan but not processed within that limit are marked cleanupSkippedMaxNRemovals in the report and are picked up by the next run. Raise the limit through the config_env_vars of the core chart when a large backlog needs to drain faster:

core:
  config_env_vars:
    RESOURCE_CLEANUP_IMAGE_MAX_N_REMOVALS_CLEANUP: "1000"

Ad-hoc cleanup

In particular scenarios, it might be beneficial to run this cleanup manually—either because the automatic cleanup has been disabled or because the user wants to use the advanced parameters of the endpoint.

These parameters can be used to help limit the cleanup scope of the scan:

  • entity_types: e.g., only custom models or only custom apps.
  • repositories: Help mitigate unwanted effects of image removals in certain repositories.
  • cleanup_rules: Limit which type of heuristics can be used to delete images. Not all entities support all cleanup rules. The supported cleanup rules are the following:

    • entityIsDeleted: If the entity(s) linked to this image have been soft-deleted. In this case, the image can be hard-deleted.
    • imageNotInDataRobotDB: If the image is in a repository that only DataRobot writes to, but it cannot be mapped to any DataRobot entity. Such an image was created by DataRobot and is no longer used.

      Understanding imageNotInDataRobotDB

      This is a special case where the internal logic is confident that this image, even though not mapped to a DataRobot entity, should be deleted. This happens when the image is known by the image building service but not by the main DataRobot app, which means there was some inconsistency in the application. Since the image is in the building service and not in the app, the scan endpoint knows that this image was once created by DataRobot and is not used anymore.

    • lifecycleExpired: For some repositories, it is possible to configure a lifecycle policy to remove images after a certain number of days. The number of days can be configured by RESOURCE_CLEANUP_IMAGE_LIFECYCLE_DAYS which defaults to 20 days. If this logic needs to be disabled, including via the automatic cleanup, set RESOURCE_CLEANUP_IMAGE_ENABLE_LIFECYCLE_REMOVAL to false. If an admin then calls the cleanup logic with cleanup_rule=lifecycleExpired, that configuration will be ignored.

Repository scope for lifecycle expiration

Lifecycle expiration only applies to the repository configured at IMAGE_BUILDER_CUSTOM_JOB_EXECUTION_REGISTRY_REPO (custom jobs). This is because custom jobs typically have shorter lifecycles and generate temporary execution images that can safely be removed after a defined period. If the job needs to be rerun, DataRobot will rebuild the image.

The image cleanup removes images for the following supported entities:

Entity type Image repositories
Execution environments IMAGE_BUILDER_CUSTOM_MODELS_ENVIRONMENT_REGISTRY_REPO
Custom models IMAGE_BUILDER_CUSTOM_MODELS_REGISTRY_REPO (runanble images)
IMAGE_BUILDER_CUSTOM_MODELS_ENVIRONMENT_REGISTRY_REPO (dependency images)
Custom apps IMAGE_BUILDER_CUSTOM_APPLICATION_REGISTRY_REPO (runnable images)
IMAGE_BUILDER_CUSTOM_MODELS_ENVIRONMENT_REGISTRY_REPO (dependency images)
Custom jobs IMAGE_BUILDER_CUSTOM_JOB_EXECUTION_REGISTRY_REPO (runnable images)

Code example

import requests
import os
import time
import logging
from typing import Dict, List, Optional

API_BASE_URL = "https://app.datarobot.com/api/v2"
API_KEY = os.environ["DR_API_KEY"]  # Key with admin rights!
CLEANUP_TYPE = "image"


class ResourceCleanupClient:
    # The client is defined at the top of this documentation.
    ...


def create_image_cleanup_params(
    entity_types: Optional[List[str]] = None,
    cleanup_rules: Optional[List[str]] = None,
    tenant_ids: Optional[List[str]] = None,
    image_repositories: Optional[List[str]] = None
) -> Dict:
    """Create parameters for image cleanup scan."""
    return {
        "entityTypes": entity_types or ["environment", "customModel", "customApp", "customJob"],
        "cleanupRules": cleanup_rules or ["entityIsDeleted", "imageNotInDataRobotDB", "lifecycleExpired"],
        "tenantIds": tenant_ids or [],
        "imageRepositories": image_repositories or []
    }


def setup_logging(log_level: str = "INFO") -> None:
    """Configure logging for the application."""
    logging.basicConfig(
        level=getattr(logging, log_level.upper()),
        format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
        datefmt='%Y-%m-%d %H:%M:%S'
    )


if __name__ == "__main__":
    setup_logging("INFO")
    logger = logging.getLogger(__name__)

    try:
        logger.info("Running image cleanup workflow...")
        client = ResourceCleanupClient(API_BASE_URL, API_KEY, "image")

        # List existing scans
        scans = client.list_scans()
        logger.info(f"Found {len(scans)} existing scans")

        # Run image cleanup workflow
        scan_params = create_image_cleanup_params(
            entity_types=[],
            cleanup_rules=[]
        )
        final_report = client.run_resource_cleanup(scan_params, trigger_cleanup=True)
        logger.info(f"Image cleanup completed. Found {len(final_report.get('identifiedResources', []))} resources in report.")
    except Exception as e:
        logger.error(f"Resource cleanup failed: {e}", exc_info=True)
        raise

Report example

{
    "schema_version": "1.0",
    "scanDetails": {
        "type": "image_cleanup_scan",
        "scanId": "387c738a-2c10-4766-821c-a8a761ba2d24",
        "scanTimestamp": "2025-09-29T10:08:06.952657+00:00",
        "summary": {
            "totalImagesScanned": 58,  // Number of images scanned across all repositories involved.
            "identifiedForCleanup": 24,  // Number of images that can be safely removed.
            "imagesPerRepository": {
                "base-image": 13,  // The typical repository for execution environment images and for images that are used as a dependency image. Dependency images are images that are not deployed but might be used as a layer to build runnable images.
                "custom-apps/managed-image": 4,  // These are typically runnable images.
                "custom-models/managed-image": 7  // These are typically runnable images.
            }
        },
        "parameters": {  
            "tenantIds": [
                "d7864135-579c-4310-acdc-f0db93e88da0",  // can filter only the images for a given tenant.
            ],
            "imageRepositories": [],  // can filter the images only for specified repositories.
            "cleanupRules": [  // by heuristic rule
                "entityIsDeleted"  
            ],
            "entityTypes": [  // by entity
                "customModel",
                "customApp",
                "environment"
            ]
        }
    },
    "identifiedResources": [
        {
            "details": {
                "imageTag": "68b557b6b1446ca314081d71",
                "imageRepository": "base-image",
                "cleanupReason": "entityIsDeleted",  // The rule used to identify this image as eligible for cleanup.
                "entityType": "customApp",  // The entity type linked to this image.
                "entityId": "68b5578db1446ca314081d5c",  // The entity id. For customApps it is the application source ID.
                "entityVersionIds": [  // The versions that depend of this image. For customApps it is the custom application source ID. 
                    "68b5578db1446ca314081d5e",
                    "68b55ac3d02246dc6860e85d",
                    "68b55ecfac2191907b2290d5"
                ],
                "ibsBuildId": "6889f5f13e1723d83a3edad8",
                "lrs": null,  // If null, this application might have been developed, but never deployed.
                "userId": "6842de77104a752e1cf2a10e",  // The user that created the entity.
                "tenantId": "d7864135-579c-4310-acdc-f0db93e88da0"  // The tenant it belongs to.
            },
            "status": "eligibleForCleanup"  // The options are: notEligibleForCleanup which means that the /cleanup endpoint will not act. eligibleForCleanup, which means the /cleanup endpoint will act. cleanupSuccess, that the /cleanup endpoint acted and successfully cleaned up the image. cleanupFailed, that the /cleanup endpoint ran, but failed to clean up the image. In this case, an error message will also be provided.
        },
        {
            "details": {
                "imageTag": "68b158c3c84e8bc86d70c4e4",
                "imageRepository": "custom-apps/managed-image",
                "cleanupReason": "entityIsDeleted",
                "entityType": "customApp",
                "entityId": "68b157d8c84e8bc86d70c4b7",
                "entityVersionIds": [
                    "68b158bec84e8bc86d70c4de"
                ],
                "ibsBuildId": null,
                "lrs": [  // In this case, the application was deployed, so it is available.
                    {
                        "lrs_id": [
                            "68b158c3c84e8bc86d70c4df"
                        ],
                        "lrs_status": [
                            "stopped"  // It is very unlikely that the status will not be `running` or `starting`, this is because this endpoint should not select images that are currently being used.
                        ]
                    }
                ],
                "userId": "6842de77104a752e1cf2a10e",
                "tenantId": "d7864135-579c-4310-acdc-f0db93e88da0"
            },
            "status": "eligibleForCleanup"
        },
        {
            "details": {
                "imageTag": "6889f5f13e1723d83a3edad7",
                "imageRepository": "base-image",  // In this case, this is a dependency image.
                "cleanupReason": "entityIsDeleted",
                "entityType": "customModel",  // In this case, this is a customModel. 
                "entityId": "6888c9bb3e1723d83a3eda8d",  // Its entity is the custom model ID
                "entityVersionIds": [
                    "6889f5423e1723d83a3edad5",  // The version is the custom model version ID. 
                    "6889fa403e1723d83a3edaea"
                ],
                "ibsBuildId": "68a882777d22b3a51a638dc3",
                "lrs": null,  // Always null because it is a dependency image, this one should never be deployed.
                "userId": "6842de77104a752e1cf2a10e",
                "tenantId": "d7864135-579c-4310-acdc-f0db93e88da0"
            },
            "status": "eligibleForCleanup"
        },
        {
            "details": {
                "imageTag": "68a875b72c1f9d255b080fc4",
                "imageRepository": "managed-image",  // In this case, this is likely a runnable image.
                "cleanupReason": "entityIsDeleted",
                "entityType": "customModel",
                "entityId": "686d1b576c02265c891d1ee5",
                "entityVersionIds": [
                    "68a875b12c1f9d255b080fc0"
                ],
                "ibsBuildId": "6842de77104a752e1cf2a11e",
                "lrs": [
                    {
                        "lrs_id": "68a875b72c1f9d255b080fc3",
                        "lrs_status": "stopped"  // It was once deployed, but now it is stopped.
                    }
                ],
                "userId": "6842de77104a752e1cf2a10e",
                "tenantId": "d7864135-579c-4310-acdc-f0db93e88da0"
            },
            "status": "eligibleForCleanup"
        },
        {
            "details": {
                "imageTag": "68a882777d22b3a51a638dc7",
                "imageRepository": "base-image",
                "cleanupReason": "entityIsDeleted",
                "entityType": "environment",  // Execution environment image.
                "entityId": "68a882777d22b3a51a638dc2",  // environment ID
                "entityVersionIds": [
                    "68a882777d22b3a51a638dc6"  // environment version ID
                ],
                "ibsBuildId": null,
                "lrs": null,
                "userId": "6842de77104a752e1cf2a10e",
                "tenantId": "d7864135-579c-4310-acdc-f0db93e88da0"
            },
            "status": "eligibleForCleanup"
        }
        ...  // this might be a long list of images
    ],
    "cleanupRunsHistory": []
}

CVE cleanup (/api/v2/cveCleanups)

CVE cleanup endpoint limitations

This endpoint does not perform a CVE scan. Instead, it supports CVE remediation by mapping a list of images with DataRobot entities. This is useful because if a CVE scan outside of DataRobot is performed on the DataRobot image repositories, the scan might yield a list of CVE images—which is when the endpoint becomes useful.

This cleanup type supports CVE remediation by helping DataRobot system administrators and/or Support map a given list of images with their directly linked DataRobot entities. By doing so, the administrator will be able to trace back which entities are relying on such images and will be able to proceed with their removal.

See below for a few common scenarios you might encounter when cleaning CVE images:

  1. The CVE image is used by DataRobot entities that are deployed. In this case, the API will indicate which images are deployed to the cluster, and by which entities. To remediate this case, the user will need to shut down all the running entities, and then remove the entities that are affected.

  2. The CVE image is not deployed but the linked entities are not removed. In this case, only the affected entities will need to be removed.

  3. The CVE image is not needed anymore. This case is the easiest one to handle because the image can be removed immediately.

Code example

import requests
import os
import time
import logging
from typing import Dict, List, Optional

API_BASE_URL = "https://app.datarobot.com/api/v2"
API_KEY = os.environ["DR_API_KEY"]  # Key with admin rights!


class ResourceCleanupClient:
    # The client is defined at the top of this documentation.
    ...


def create_cve_cleanup_params(
    cve_images: List[str],
    entity_types: Optional[List[str]] = None,
    tenant_ids: Optional[List[str]] = None,
    image_repositories: Optional[List[str]] = None
) -> Dict:
    """Create parameters for CVE cleanup scan."""
    return {
        "cveImages": cve_images,
        "entityTypes": entity_types or ["customModel", "customApp", "environment", "customJob"],
        "tenantIds": tenant_ids or [],
        "imageRepositories": image_repositories or []
    }


def setup_logging(log_level: str = "INFO") -> None:
    """Configure logging for the application."""
    logging.basicConfig(
        level=getattr(logging, log_level.upper()),
        format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
        datefmt='%Y-%m-%d %H:%M:%S'
    )


if __name__ == "__main__":
    setup_logging("INFO")
    logger = logging.getLogger(__name__)

    try:
        logger.info("Running CVE cleanup workflow...")
        client = ResourceCleanupClient(API_BASE_URL, API_KEY, "cve")

        # List existing scans
        scans = client.list_scans()
        logger.info(f"Found {len(scans)} existing scans")
        scan_params = create_cve_cleanup_params(
            cve_images=[
                "localhost:5000/managed-image:68a86e112c1f9d255b080f8f",
                "localhost:5000/test-image:vulnerable-tag"
            ]
        )
        final_report = client.run_resource_cleanup(scan_params, trigger_cleanup=True)
        logger.info(f"CVE cleanup completed. Found {len(final_report.get('identifiedResources', []))} resources in report")
    except Exception as e:
        logger.error(f"Resource cleanup failed: {e}", exc_info=True)
        raise

Report example

{
    "schema_version": "1.0",
    "scanDetails": {
        "type": "image_cleanup_scan",
        "scanId": "54e4d4da-75eb-4bbf-80e0-a51a49fc455a",
        "scanTimestamp": "2025-09-29T08:13:59.180201+00:00",
        "summary": {
            "totalImagesScanned": 1,  // This count depends on the parameters, the scan scope will be limited by the parameters: `imageRepositories` and `entityTypes`.
            "identifiedForCleanup": 1,  // This count returns the number of images that were succesfully mapped with at least one DataRobot entity.
        },
        "parameters": {
            "tenantIds": [],
            "imageRepositories": [],  // If empty will scan all the repositories linked to the configured `entityTypes`. Otherwise, it will limit the scope of the scan to only those repositories.
            "entityTypes": [],  // If empty, will used scan for all the supported entities. The supported entities are: customApp, customModel, environment, customJob.
            "cveImages": [
                "localhost:5000/does-not-exist:68b040f8a917c60d3053de9a",
                "localhost:5000/custom-apps/managed-image:68af0dde05706cc75498e105",
            ]
        }
    },
    "identifiedResources": [
        {
            "details": {
                "imageUri": "localhost:5000/custom-apps/managed-image:68af0dde05706cc75498e105",
                "imageTag": "68af0dde05706cc75498e105",
                "imageRepository": "custom-apps/managed-image",
                "entityType": "customApp",
                "entityId": "68af0daf05706cc75498e0ec",
                "entityVersions": [  // Some images might be used by multiple versions linked to the same entity.
                    "68af0daf05706cc75498e0ee"
                ],
                "imageStatus": "deployed",  // the options are (1)`deployed` which indicates that the image is currently deployed and being used, its removal would have immediate consequences; (2)`active` which means the image is not deployed, but the entity is still active; (3)`inactive` the image will likely never be used again; (4)`unknown` the image could not be mapped to a DataRobot entity. In this case, it might be that the image is actually used by DataRobot, but the repository/entity is not yet supported by the CVE endpoint.
                "isDeleted": false,  // If true, all entities linked to this image have been deleted.
                "ibsBuildId": null,  // If empty, it means that the image is not managed by DataRobot, as long as the image repository is within the list of supported entities. So it can likely be safely deleted without impacting DataRobot. 
                "lrs": [  // LRS is an internal DataRobot system to deploy long running services. This is exposed to the admin user in case they need support with shutting down the service for image deletion; the `lrs_id` would simplify that task.
                    {
                        "lrsId": "68af0dde05706cc75498e0fc",
                        "lrsStatus": "running"
                    }
                ],
                "userId": "6842de77104a752e1cf2a10e",
                "tenantId": "d7864135-579c-4310-acdc-f0db93e88da0"
            },
            "status": "notEligibleForCleanup"  // The options are: notEligibleForCleanup which means that the /cleanup endpoint will not act. eligibleForCleanup, which means the /cleanup endpoint will act. cleanupSuccess, that the /cleanup endpoint acted and successfully cleaned up the image. cleanupFailed, that the /cleanup endpoint ran, but failed to clean up the image. In this case, an error message will also be provided.
        },
        {
            "details": {
                "imageUri": "localhost:5000/does-not-exist:68b040f8a917c60d3053de9a",
                "imageTag": "68b040f8a917c60d3053de9a",
                "imageRepository": "does-not-exist",  // This is an example to show the response for an image that could not be mapped with DataRobot.
                "entityType": null,
                "entityId": null,
                "entityVersions": null,
                "imageStatus": "unknown",  // most fields are null, but the status is unknown. 
                "isDeleted": null,
                "ibsBuildId": null,
                "lrs": null,
                "userId": null,
                "tenantId": null
            },
            "status": "notEligibleForCleanup"  // DataRobot cannot make a decision on whether this should be deleted, because this image is not managed by DataRobot. Marking it as `notEligibleForCleanup` means that the /cleanup endpoint will skip this entry. However, it is returned in the report for completeness.
        }
    ],
    "cleanupRunsHistory": []
}