Skip to content

Commit 6d185bb

Browse files
authored
[gcp][feat] Add firestore service collection (#2275)
1 parent 897e920 commit 6d185bb

File tree

10 files changed

+1190
-41
lines changed

10 files changed

+1190
-41
lines changed

plugins/gcp/fix_plugin_gcp/collector.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from fix_plugin_gcp.config import GcpConfig
66
from fix_plugin_gcp.gcp_client import GcpApiSpec
7-
from fix_plugin_gcp.resources import compute, container, billing, sqladmin, storage, aiplatform
7+
from fix_plugin_gcp.resources import compute, container, billing, sqladmin, storage, aiplatform, firestore
88
from fix_plugin_gcp.resources.base import GcpResource, GcpProject, ExecutorQueue, GraphBuilder, GcpRegion, GcpZone
99
from fix_plugin_gcp.utils import Credentials
1010
from fixlib.baseresources import Cloud
@@ -19,6 +19,7 @@
1919
+ sqladmin.resources
2020
+ storage.resources
2121
+ aiplatform.resources
22+
+ firestore.resources
2223
)
2324

2425

plugins/gcp/fix_plugin_gcp/resources/aiplatform.py

Lines changed: 0 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1886,19 +1886,6 @@ def connect_in_graph(self, builder: GraphBuilder, source: Json) -> None:
18861886
@define(eq=False, slots=False)
18871887
class GcpVertexAIArtifact:
18881888
kind: ClassVar[str] = "gcp_vertex_ai_artifact"
1889-
_kind_display = ""
1890-
_kind_service = ""
1891-
api_spec: ClassVar[GcpApiSpec] = GcpApiSpec(
1892-
service="aiplatform",
1893-
version="v1",
1894-
service_with_region_prefix=True,
1895-
accessors=["projects", "locations", "metadataStores", "artifacts"],
1896-
action="list",
1897-
request_parameter={"parent": "projects/{project}/locations/{region}"},
1898-
request_parameter_in={"project", "region"},
1899-
response_path="artifacts",
1900-
response_regional_sub_path=None,
1901-
)
19021889
mapping: ClassVar[Dict[str, Bender]] = {
19031890
"id": S("name").or_else(S("id")).or_else(S("selfLink")),
19041891
"tags": S("labels", default={}),
@@ -2519,19 +2506,6 @@ class GcpVertexAIPipelineTaskDetailPipelineTaskStatus:
25192506
@define(eq=False, slots=False)
25202507
class GcpVertexAIExecution:
25212508
kind: ClassVar[str] = "gcp_vertex_ai_execution"
2522-
_kind_display = ""
2523-
_kind_service = ""
2524-
api_spec: ClassVar[GcpApiSpec] = GcpApiSpec(
2525-
service="aiplatform",
2526-
version="v1",
2527-
service_with_region_prefix=True,
2528-
accessors=["projects", "locations", "metadataStores", "executions"],
2529-
action="list",
2530-
request_parameter={"parent": "projects/{project}/locations/{region}"},
2531-
request_parameter_in={"project", "location"},
2532-
response_path="executions",
2533-
response_regional_sub_path=None,
2534-
)
25352509
mapping: ClassVar[Dict[str, Bender]] = {
25362510
"id": S("name").or_else(S("id")).or_else(S("selfLink")),
25372511
"tags": S("labels", default={}),
@@ -2597,19 +2571,6 @@ class GcpVertexAIPipelineTaskDetail:
25972571
@define(eq=False, slots=False)
25982572
class GcpVertexAIContext:
25992573
kind: ClassVar[str] = "gcp_vertex_ai_context"
2600-
_kind_display = ""
2601-
_kind_service = ""
2602-
api_spec: ClassVar[GcpApiSpec] = GcpApiSpec(
2603-
service="aiplatform",
2604-
version="v1",
2605-
service_with_region_prefix=True,
2606-
accessors=["projects", "locations", "metadataStores", "contexts"],
2607-
action="list",
2608-
request_parameter={"parent": "projects/{project}/locations/{region}"},
2609-
request_parameter_in={"project", "location"},
2610-
response_path="contexts",
2611-
response_regional_sub_path=None,
2612-
)
26132574
mapping: ClassVar[Dict[str, Bender]] = {
26142575
"id": S("name").or_else(S("id")).or_else(S("selfLink")),
26152576
"tags": S("labels", default={}),
Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
from datetime import datetime
2+
import logging
3+
from typing import ClassVar, Dict, Optional, List, Any, Type
4+
5+
from attr import define, field
6+
7+
from fix_plugin_gcp.gcp_client import GcpApiSpec
8+
from fix_plugin_gcp.resources.base import GcpErrorHandler, GcpResource, GcpDeprecationStatus, GraphBuilder
9+
from fixlib.baseresources import BaseDatabase, ModelReference
10+
from fixlib.json_bender import Bender, S, Bend, MapDict
11+
from fixlib.types import Json
12+
13+
log = logging.getLogger("fix.plugins.gcp")
14+
15+
16+
# https://cloud.google.com/firestore/docs
17+
18+
service_name = "firestore"
19+
20+
21+
@define(eq=False, slots=False)
22+
class GcpFirestoreCmekConfig:
23+
kind: ClassVar[str] = "gcp_firestore_cmek_config"
24+
mapping: ClassVar[Dict[str, Bender]] = {
25+
"active_key_version": S("activeKeyVersion", default=[]),
26+
"kms_key_name": S("kmsKeyName"),
27+
}
28+
active_key_version: Optional[List[str]] = field(default=None)
29+
kms_key_name: Optional[str] = field(default=None)
30+
31+
32+
@define(eq=False, slots=False)
33+
class GcpFirestoreSourceInfo:
34+
kind: ClassVar[str] = "gcp_firestore_source_info"
35+
mapping: ClassVar[Dict[str, Bender]] = {"backup": S("backup", "backup"), "operation": S("operation")}
36+
backup: Optional[str] = field(default=None)
37+
operation: Optional[str] = field(default=None)
38+
39+
40+
@define(eq=False, slots=False)
41+
class GcpFirestoreDatabase(GcpResource, BaseDatabase):
42+
kind: ClassVar[str] = "gcp_firestore_database"
43+
_kind_display: ClassVar[str] = "GCP Firestore Database"
44+
_kind_description: ClassVar[str] = (
45+
"A Firestore Database in GCP, which is a scalable NoSQL cloud database to store and sync data for client- and server-side development."
46+
)
47+
_kind_service: ClassVar[Optional[str]] = service_name
48+
_metadata: ClassVar[Dict[str, Any]] = {"icon": "database", "group": "storage"}
49+
_reference_kinds: ClassVar[ModelReference] = {
50+
"successors": {
51+
"default": [
52+
"gcp_firestore_document",
53+
],
54+
},
55+
}
56+
api_spec: ClassVar[GcpApiSpec] = GcpApiSpec(
57+
service="firestore",
58+
version="v1",
59+
accessors=["projects", "databases"],
60+
action="list",
61+
request_parameter={"parent": "projects/{project}"},
62+
request_parameter_in={"project"},
63+
response_path="databases",
64+
response_regional_sub_path=None,
65+
)
66+
mapping: ClassVar[Dict[str, Bender]] = {
67+
"id": S("name").or_else(S("id")).or_else(S("selfLink")),
68+
"tags": S("labels", default={}),
69+
"name": S("name"),
70+
"ctime": S("creationTimestamp"),
71+
"description": S("description"),
72+
"link": S("selfLink"),
73+
"label_fingerprint": S("labelFingerprint"),
74+
"deprecation_status": S("deprecated", default={}) >> Bend(GcpDeprecationStatus.mapping),
75+
"app_engine_integration_mode": S("appEngineIntegrationMode"),
76+
"cmek_config": S("cmekConfig", default={}) >> Bend(GcpFirestoreCmekConfig.mapping),
77+
"concurrency_mode": S("concurrencyMode"),
78+
"create_time": S("createTime"),
79+
"delete_protection_state": S("deleteProtectionState"),
80+
"delete_time": S("deleteTime"),
81+
"earliest_version_time": S("earliestVersionTime"),
82+
"etag": S("etag"),
83+
"key_prefix": S("keyPrefix"),
84+
"location_id": S("locationId"),
85+
"point_in_time_recovery_enablement": S("pointInTimeRecoveryEnablement"),
86+
"previous_id": S("previousId"),
87+
"source_info": S("sourceInfo", default={}) >> Bend(GcpFirestoreSourceInfo.mapping),
88+
"type": S("type"),
89+
"uid": S("uid"),
90+
"update_time": S("updateTime"),
91+
"version_retention_period": S("versionRetentionPeriod"),
92+
}
93+
app_engine_integration_mode: Optional[str] = field(default=None)
94+
cmek_config: Optional[GcpFirestoreCmekConfig] = field(default=None)
95+
concurrency_mode: Optional[str] = field(default=None)
96+
create_time: Optional[datetime] = field(default=None)
97+
delete_protection_state: Optional[str] = field(default=None)
98+
delete_time: Optional[datetime] = field(default=None)
99+
earliest_version_time: Optional[datetime] = field(default=None)
100+
etag: Optional[str] = field(default=None)
101+
key_prefix: Optional[str] = field(default=None)
102+
location_id: Optional[str] = field(default=None)
103+
point_in_time_recovery_enablement: Optional[str] = field(default=None)
104+
previous_id: Optional[str] = field(default=None)
105+
source_info: Optional[GcpFirestoreSourceInfo] = field(default=None)
106+
type: Optional[str] = field(default=None)
107+
uid: Optional[str] = field(default=None)
108+
update_time: Optional[datetime] = field(default=None)
109+
version_retention_period: Optional[str] = field(default=None)
110+
111+
@classmethod
112+
def called_collect_apis(cls) -> List[GcpApiSpec]:
113+
return [
114+
cls.api_spec,
115+
GcpApiSpec(
116+
service="firestore",
117+
version="v1",
118+
accessors=["projects", "databases", "documents"],
119+
action="list",
120+
request_parameter={"parent": "projects/{project}/databases/{databaseId}/documents"},
121+
request_parameter_in={"project", "databaseId"},
122+
response_path="documents",
123+
response_regional_sub_path=None,
124+
),
125+
]
126+
127+
def post_process(self, graph_builder: GraphBuilder, source: Json) -> None:
128+
def collect_documents() -> None:
129+
spec = GcpApiSpec(
130+
service="firestore",
131+
version="v1",
132+
accessors=["projects", "databases", "documents"],
133+
action="list",
134+
request_parameter={"parent": f"{self.id}/documents"},
135+
request_parameter_in=set(),
136+
response_path="documents",
137+
response_regional_sub_path=None,
138+
)
139+
with GcpErrorHandler(
140+
spec.action,
141+
graph_builder.error_accumulator,
142+
spec.service,
143+
graph_builder.region.safe_name if graph_builder.region else None,
144+
set(),
145+
f" in {graph_builder.project.id} kind {GcpFirestoreDocument.kind}",
146+
):
147+
items = graph_builder.client.list(spec)
148+
documents = GcpFirestoreDocument.collect(items, graph_builder)
149+
for document in documents:
150+
graph_builder.add_edge(self, node=document)
151+
log.info(f"[GCP:{graph_builder.project.id}] finished collecting: {GcpFirestoreDocument.kind}")
152+
153+
graph_builder.submit_work(collect_documents)
154+
155+
156+
@define(eq=False, slots=False)
157+
class GcpArrayValue:
158+
kind: ClassVar[str] = "gcp_array_value"
159+
mapping: ClassVar[Dict[str, Bender]] = {"values": S("values", default=[])}
160+
values: Optional[List[Any]] = field(default=None)
161+
162+
163+
@define(eq=False, slots=False)
164+
class GcpLatLng:
165+
kind: ClassVar[str] = "gcp_lat_lng"
166+
mapping: ClassVar[Dict[str, Bender]] = {"latitude": S("latitude"), "longitude": S("longitude")}
167+
latitude: Optional[float] = field(default=None)
168+
longitude: Optional[float] = field(default=None)
169+
170+
171+
@define(eq=False, slots=False)
172+
class GcpMapValue:
173+
kind: ClassVar[str] = "gcp_map_value"
174+
mapping: ClassVar[Dict[str, Bender]] = {"fields": S("fields", default={})}
175+
fields: Optional[Dict[str, Any]] = field(default=None)
176+
177+
178+
@define(eq=False, slots=False)
179+
class GcpValue:
180+
kind: ClassVar[str] = "gcp_value"
181+
mapping: ClassVar[Dict[str, Bender]] = {
182+
"array_value": S("arrayValue", default={}) >> Bend(GcpArrayValue.mapping),
183+
"boolean_value": S("booleanValue"),
184+
"bytes_value": S("bytesValue"),
185+
"double_value": S("doubleValue"),
186+
"geo_point_value": S("geoPointValue", default={}) >> Bend(GcpLatLng.mapping),
187+
"integer_value": S("integerValue"),
188+
"map_value": S("mapValue", default={}) >> Bend(GcpMapValue.mapping),
189+
"null_value": S("nullValue"),
190+
"reference_value": S("referenceValue"),
191+
"string_value": S("stringValue"),
192+
"timestamp_value": S("timestampValue"),
193+
}
194+
array_value: Optional[GcpArrayValue] = field(default=None)
195+
boolean_value: Optional[bool] = field(default=None)
196+
bytes_value: Optional[str] = field(default=None)
197+
double_value: Optional[float] = field(default=None)
198+
geo_point_value: Optional[GcpLatLng] = field(default=None)
199+
integer_value: Optional[str] = field(default=None)
200+
map_value: Optional[GcpMapValue] = field(default=None)
201+
null_value: Optional[str] = field(default=None)
202+
reference_value: Optional[str] = field(default=None)
203+
string_value: Optional[str] = field(default=None)
204+
timestamp_value: Optional[datetime] = field(default=None)
205+
206+
207+
@define(eq=False, slots=False)
208+
class GcpFirestoreDocument(GcpResource):
209+
kind: ClassVar[str] = "gcp_firestore_document"
210+
_kind_display: ClassVar[str] = "GCP Firestore Document"
211+
_kind_description: ClassVar[str] = (
212+
"A Firestore Document in GCP, representing a single document in a Firestore database, which can contain fields and subcollections."
213+
)
214+
_kind_service: ClassVar[Optional[str]] = service_name
215+
_metadata: ClassVar[Dict[str, Any]] = {"icon": "database", "group": "storage"}
216+
# collected via GcpFirestoreDatabase()
217+
mapping: ClassVar[Dict[str, Bender]] = {
218+
"id": S("name").or_else(S("id")).or_else(S("selfLink")),
219+
"tags": S("labels", default={}),
220+
"name": S("name"),
221+
"ctime": S("creationTimestamp"),
222+
"description": S("description"),
223+
"link": S("selfLink"),
224+
"label_fingerprint": S("labelFingerprint"),
225+
"deprecation_status": S("deprecated", default={}) >> Bend(GcpDeprecationStatus.mapping),
226+
"create_time": S("createTime"),
227+
"fields": S("fields", default={}) >> MapDict(value_bender=Bend(GcpValue.mapping)),
228+
"update_time": S("updateTime"),
229+
}
230+
create_time: Optional[datetime] = field(default=None)
231+
fields: Optional[Dict[str, GcpValue]] = field(default=None)
232+
update_time: Optional[datetime] = field(default=None)
233+
234+
235+
@define(eq=False, slots=False)
236+
class GcpFirestoreStats:
237+
kind: ClassVar[str] = "gcp_firestore_stats"
238+
mapping: ClassVar[Dict[str, Bender]] = {
239+
"document_count": S("documentCount"),
240+
"index_count": S("indexCount"),
241+
"size_bytes": S("sizeBytes"),
242+
}
243+
document_count: Optional[str] = field(default=None)
244+
index_count: Optional[str] = field(default=None)
245+
size_bytes: Optional[str] = field(default=None)
246+
247+
248+
@define(eq=False, slots=False)
249+
class GcpFirestoreBackup(GcpResource):
250+
kind: ClassVar[str] = "gcp_firestore_backup"
251+
_kind_display: ClassVar[str] = "GCP Firestore Backup"
252+
_kind_description: ClassVar[str] = (
253+
"A Firestore Backup in GCP, which provides a way to back up and restore Firestore databases to protect against data loss."
254+
)
255+
_kind_service: ClassVar[Optional[str]] = service_name
256+
_metadata: ClassVar[Dict[str, Any]] = {"icon": "backup", "group": "storage"}
257+
api_spec: ClassVar[GcpApiSpec] = GcpApiSpec(
258+
service="firestore",
259+
version="v1",
260+
accessors=["projects", "locations", "backups"],
261+
action="list",
262+
request_parameter={"parent": "projects/{project}/locations/-"},
263+
request_parameter_in={"project"},
264+
response_path="backups",
265+
response_regional_sub_path=None,
266+
)
267+
mapping: ClassVar[Dict[str, Bender]] = {
268+
"id": S("name").or_else(S("id")).or_else(S("selfLink")),
269+
"tags": S("labels", default={}),
270+
"name": S("name"),
271+
"ctime": S("creationTimestamp"),
272+
"description": S("description"),
273+
"link": S("selfLink"),
274+
"label_fingerprint": S("labelFingerprint"),
275+
"deprecation_status": S("deprecated", default={}) >> Bend(GcpDeprecationStatus.mapping),
276+
"database_name": S("database"),
277+
"database_uid": S("databaseUid"),
278+
"expire_time": S("expireTime"),
279+
"snapshot_time": S("snapshotTime"),
280+
"state": S("state"),
281+
"backup_stats": S("stats", default={}) >> Bend(GcpFirestoreStats.mapping),
282+
}
283+
database_name: Optional[str] = field(default=None)
284+
database_uid: Optional[str] = field(default=None)
285+
expire_time: Optional[datetime] = field(default=None)
286+
snapshot_time: Optional[datetime] = field(default=None)
287+
state: Optional[str] = field(default=None)
288+
backup_stats: Optional[GcpFirestoreStats] = field(default=None)
289+
290+
291+
resources: List[Type[GcpResource]] = [GcpFirestoreDatabase, GcpFirestoreDocument, GcpFirestoreBackup]
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import json
2+
import os
3+
4+
from fix_plugin_gcp.resources.base import GraphBuilder
5+
from fix_plugin_gcp.resources.aiplatform import resources
6+
7+
8+
def test_gcp_aiplatform_resources(random_builder: GraphBuilder) -> None:
9+
file_path = os.path.join(os.path.dirname(__file__), "files", "aiplatform_resources.json")
10+
with open(file_path, "r") as f:
11+
data = json.load(f)
12+
13+
for resource, res_class in zip(data["resources"], resources):
14+
res_class.collect(raw=[resource], builder=random_builder)
15+
collected = random_builder.nodes(clazz=res_class)
16+
assert len(collected) == 1

0 commit comments

Comments
 (0)