Repository: google/cloud-forensics-utils

Issue:
Add support for specifying resources by ID as well as name
Add support for accessing resources by ID as well as name, for example instance ID for GetInstance.

Discussion:
codecov-commenter: # [Codecov](https://codecov.io/gh/google/cloud-forensics-utils/pull/444?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google) Report
> :exclamation: No coverage uploaded for pull request base (`main@4a2f66c`). [Click here to learn what that means](https://docs.codecov.io/docs/error-reference?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google#section-missing-base-commit).
> The diff coverage is `n/a`.

```diff
@@           Coverage Diff           @@
##             main     #444   +/-   ##
=======================================
  Coverage        ?   59.14%           
=======================================
  Files           ?       49           
  Lines           ?     4031           
  Branches        ?        0           
=======================================
  Hits            ?     2384           
  Misses          ?     1647           
  Partials        ?        0           
```

| Flag | Coverage Δ | |
|---|---|---|
| nosetests | `59.14% <0.00%> (?)` | |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google#carryforward-flags-in-the-pull-request-comment) to find out more.


------

[Continue to review full report at Codecov](https://codecov.io/gh/google/cloud-forensics-utils/pull/444?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/google/cloud-forensics-utils/pull/444?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google). Last update [4a2f66c...7f037c4](https://codecov.io/gh/google/cloud-forensics-utils/pull/444?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google).


Patch:
diff --git a/libcloudforensics/providers/gcp/internal/compute.py b/libcloudforensics/providers/gcp/internal/compute.py
index 5103574..0c16d10 100644
--- a/libcloudforensics/providers/gcp/internal/compute.py
+++ b/libcloudforensics/providers/gcp/internal/compute.py
@@ -46,6 +46,10 @@ E2_STANDARD_CPU_CORES = [2, 4, 8, 16, 32]
 # Numerical policy_level value for non-hierarchical FW rules
 NON_HIERARCHICAL_FW_POLICY_LEVEL = 999
 
+# Will only matches IDs as names can't start with a number
+# https://cloud.google.com/compute/docs/naming-resources#resource-name-format
+RESOURCE_ID_REGEX = r'^\d{19}$'
+
 
 class GoogleCloudCompute(common.GoogleCloudComputeClient):
   """Class representing all Google Cloud Compute objects in a project.
@@ -140,11 +144,13 @@ class GoogleCloudCompute(common.GoogleCloudComputeClient):
           for instance in response['items'][zone]['instances']:
             _, zone = instance['zone'].rsplit('/', 1)
             name = instance['name']
+            resource_id = instance['id']
             deletion_protection = instance.get('deletionProtection', False)
             instances[name] = GoogleComputeInstance(
                 self.project_id,
                 zone,
                 name,
+                resource_id=resource_id,
                 labels=instance.get('labels'),
                 deletion_protection=deletion_protection)
         except KeyError:
@@ -324,7 +330,8 @@ class GoogleCloudCompute(common.GoogleCloudComputeClient):
     """Get instance from project.
 
     Args:
-      instance_name (str): The instance name.
+      instance_name (str): The instance identifier, can be either an instance
+        name or ID.
 
     Returns:
       GoogleComputeInstance: A Google Compute Instance object.
@@ -334,7 +341,14 @@ class GoogleCloudCompute(common.GoogleCloudComputeClient):
     """
 
     instances = self.Instances()
-    instance = instances.get(instance_name)
+
+    if re.match(RESOURCE_ID_REGEX, instance_name):
+      id_match = [instance for instance in instances.values()
+                  if instance.resource_id == instance_name]
+      instance = id_match.pop() if id_match else None
+    else:
+      instance = instances.get(instance_name)
+
     if not instance:
       raise errors.ResourceNotFoundError(
           'Instance {0:s} was not found in project {1:s}'.format(
diff --git a/libcloudforensics/providers/gcp/internal/compute_base_resource.py b/libcloudforensics/providers/gcp/internal/compute_base_resource.py
index 4780961..a206b5f 100644
--- a/libcloudforensics/providers/gcp/internal/compute_base_resource.py
+++ b/libcloudforensics/providers/gcp/internal/compute_base_resource.py
@@ -29,6 +29,7 @@ class GoogleComputeBaseResource(common.GoogleCloudComputeClient):
     project_id (str): Google Cloud project ID.
     zone (str): What zone the resource is in.
     name (str): Name of the resource.
+    resource_id (str): The ID number of the resource.
     labels (Dict): Dictionary of labels for the resource, if existing.
     deletion_protection (bool): True if the resource has deletionProtection
         enabled.
@@ -38,6 +39,7 @@ class GoogleComputeBaseResource(common.GoogleCloudComputeClient):
                project_id: str,
                zone: str,
                name: str,
+               resource_id: Optional[str] = None,
                labels: Optional[Dict[str, Any]] = None,
                deletion_protection: bool = False,
                region: Optional[str] = None) -> None:
@@ -47,6 +49,7 @@ class GoogleComputeBaseResource(common.GoogleCloudComputeClient):
       project_id: Google Cloud project ID.
       zone: What zone the resource is in.
       name: Name of the resource.
+      resource_id: The ID number of the resource.
       labels: Dictionary of labels for the resource, if existing.
       deletion_protection: True if the resource has deletionProtection
           enabled.
@@ -56,6 +59,7 @@ class GoogleComputeBaseResource(common.GoogleCloudComputeClient):
     self.deletion_protection = deletion_protection
     self.zone = zone
     self.name = name
+    self.resource_id = resource_id
     self.labels = labels
     self._data = {}  # type: Dict[str, Any]
     self.project_id = project_id  # type: str


Test patch:
diff --git a/tests/providers/gcp/gcp_mocks.py b/tests/providers/gcp/gcp_mocks.py
index 056f016..912c5fb 100644
--- a/tests/providers/gcp/gcp_mocks.py
+++ b/tests/providers/gcp/gcp_mocks.py
@@ -44,7 +44,8 @@ FAKE_SOURCE_PROJECT = gcp_project.GoogleCloudProject(
     'fake-source-project', 'fake-zone')
 
 FAKE_INSTANCE = compute.GoogleComputeInstance(
-    FAKE_SOURCE_PROJECT.project_id, 'fake-zone', 'fake-instance')
+    FAKE_SOURCE_PROJECT.project_id, 'fake-zone', 'fake-instance',
+    resource_id='0123456789012345678')
 
 FAKE_DISK = compute.GoogleComputeDisk(
     FAKE_SOURCE_PROJECT.project_id, 'fake-zone', 'fake-disk')
@@ -101,7 +102,8 @@ MOCK_INSTANCES_AGGREGATED = {
         0: {
             'instances': [{
                 'name': FAKE_INSTANCE.name,
-                'zone': '/' + FAKE_INSTANCE.zone
+                'zone': '/' + FAKE_INSTANCE.zone,
+                'id': FAKE_INSTANCE.resource_id
             }]
         }
     }
diff --git a/tests/providers/gcp/internal/test_compute.py b/tests/providers/gcp/internal/test_compute.py
index 0841f49..b9dbd82 100644
--- a/tests/providers/gcp/internal/test_compute.py
+++ b/tests/providers/gcp/internal/test_compute.py
@@ -91,6 +91,8 @@ class GoogleCloudComputeTest(unittest.TestCase):
     # pylint: enable=protected-access
     with self.assertRaises(errors.ResourceNotFoundError):
       gcp_mocks.FAKE_SOURCE_PROJECT.compute.GetInstance('non-existent-instance')
+    id_found_instance = gcp_mocks.FAKE_SOURCE_PROJECT.compute.GetInstance(gcp_mocks.FAKE_INSTANCE.resource_id)
+    self.assertEqual(id_found_instance, gcp_mocks.FAKE_INSTANCE)
 
   @typing.no_type_check
   @mock.patch('libcloudforensics.providers.gcp.internal.compute.GoogleCloudCompute.ListDisks')