diff --git a/Screenshot 2026-09-25 at 2.06.15 PM.png b/Screenshot 2026-09-25 at 2.06.15 PM.png deleted file mode 100644 index 044a7df..0000000 Binary files a/Screenshot 2026-09-25 at 2.06.15 PM.png and /dev/null differ diff --git a/aws-icons-svg/Analytics/Athena.svg b/aws-icons-svg/Analytics/Athena.svg new file mode 100644 index 0000000..fc175fe --- /dev/null +++ b/aws-icons-svg/Analytics/Athena.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-Athena_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Analytics/Clean-Rooms.svg b/aws-icons-svg/Analytics/Clean-Rooms.svg new file mode 100644 index 0000000..6df5a44 --- /dev/null +++ b/aws-icons-svg/Analytics/Clean-Rooms.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_AWS-Clean-Rooms_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Analytics/CloudSearch.svg b/aws-icons-svg/Analytics/CloudSearch.svg new file mode 100644 index 0000000..eb2f1b9 --- /dev/null +++ b/aws-icons-svg/Analytics/CloudSearch.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-CloudSearch_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Analytics/Data-Exchange.svg b/aws-icons-svg/Analytics/Data-Exchange.svg new file mode 100644 index 0000000..4c4bfc9 --- /dev/null +++ b/aws-icons-svg/Analytics/Data-Exchange.svg @@ -0,0 +1,16 @@ + + + Icon-Architecture/64/Arch_AWS-Data-Exchange_64 + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Analytics/Data-Pipeline.svg b/aws-icons-svg/Analytics/Data-Pipeline.svg new file mode 100644 index 0000000..f6dfb21 --- /dev/null +++ b/aws-icons-svg/Analytics/Data-Pipeline.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Data-Pipeline_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Analytics/DataZone.svg b/aws-icons-svg/Analytics/DataZone.svg new file mode 100644 index 0000000..2c09a93 --- /dev/null +++ b/aws-icons-svg/Analytics/DataZone.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_Amazon-DataZone_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Analytics/EMR.svg b/aws-icons-svg/Analytics/EMR.svg new file mode 100644 index 0000000..e1338a2 --- /dev/null +++ b/aws-icons-svg/Analytics/EMR.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-EMR_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Analytics/FinSpace.svg b/aws-icons-svg/Analytics/FinSpace.svg new file mode 100644 index 0000000..4217909 --- /dev/null +++ b/aws-icons-svg/Analytics/FinSpace.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_CN-Habañero_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Analytics/Glue-DataBrew.svg b/aws-icons-svg/Analytics/Glue-DataBrew.svg new file mode 100644 index 0000000..da6a546 --- /dev/null +++ b/aws-icons-svg/Analytics/Glue-DataBrew.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_AWS-Glue-DataBrew_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Analytics/Glue-Elastic-Views.svg b/aws-icons-svg/Analytics/Glue-Elastic-Views.svg new file mode 100644 index 0000000..bb8cd70 --- /dev/null +++ b/aws-icons-svg/Analytics/Glue-Elastic-Views.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_AWS-Glue-Elastic-Views_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Analytics/Glue.svg b/aws-icons-svg/Analytics/Glue.svg new file mode 100644 index 0000000..59100ad --- /dev/null +++ b/aws-icons-svg/Analytics/Glue.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Glue_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Analytics/Kinesis-Data-Analytics.svg b/aws-icons-svg/Analytics/Kinesis-Data-Analytics.svg new file mode 100644 index 0000000..71f439a --- /dev/null +++ b/aws-icons-svg/Analytics/Kinesis-Data-Analytics.svg @@ -0,0 +1,20 @@ + + + + Icon-Architecture/64/Arch_Amazon-Kinesis-Data-Analytics_64 + Created with Sketch. + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Analytics/Kinesis-Data-Streams.svg b/aws-icons-svg/Analytics/Kinesis-Data-Streams.svg new file mode 100644 index 0000000..23b121f --- /dev/null +++ b/aws-icons-svg/Analytics/Kinesis-Data-Streams.svg @@ -0,0 +1,20 @@ + + + + Icon-Architecture/64/Arch_Amazon-Kinesis-Data-Streams_64 + Created with Sketch. + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Analytics/Kinesis-Firehose.svg b/aws-icons-svg/Analytics/Kinesis-Firehose.svg new file mode 100644 index 0000000..0aaaca7 --- /dev/null +++ b/aws-icons-svg/Analytics/Kinesis-Firehose.svg @@ -0,0 +1,20 @@ + + + + Icon-Architecture/64/Arch_Amazon-Kinesis-Firehose_64 + Created with Sketch. + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Analytics/Kinesis-Video-Streams.svg b/aws-icons-svg/Analytics/Kinesis-Video-Streams.svg new file mode 100644 index 0000000..23a2101 --- /dev/null +++ b/aws-icons-svg/Analytics/Kinesis-Video-Streams.svg @@ -0,0 +1,20 @@ + + + + Icon-Architecture/64/Arch_Amazon-Kinesis-Video-Streams_64 + Created with Sketch. + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Analytics/Kinesis.svg b/aws-icons-svg/Analytics/Kinesis.svg new file mode 100644 index 0000000..622cae9 --- /dev/null +++ b/aws-icons-svg/Analytics/Kinesis.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-Kinesis_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Analytics/Lake-Formation.svg b/aws-icons-svg/Analytics/Lake-Formation.svg new file mode 100644 index 0000000..09778a6 --- /dev/null +++ b/aws-icons-svg/Analytics/Lake-Formation.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Lake-Formation_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Analytics/Managed-Streaming-for-Apache-Kafka.svg b/aws-icons-svg/Analytics/Managed-Streaming-for-Apache-Kafka.svg new file mode 100644 index 0000000..5993bd4 --- /dev/null +++ b/aws-icons-svg/Analytics/Managed-Streaming-for-Apache-Kafka.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-Managed-Streaming-for-Kafka_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Analytics/OpenSearch-Service.svg b/aws-icons-svg/Analytics/OpenSearch-Service.svg new file mode 100644 index 0000000..e83b00f --- /dev/null +++ b/aws-icons-svg/Analytics/OpenSearch-Service.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-Elasticsearch-Service_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Analytics/QuickSight.svg b/aws-icons-svg/Analytics/QuickSight.svg new file mode 100644 index 0000000..e8cc60e --- /dev/null +++ b/aws-icons-svg/Analytics/QuickSight.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-QuickSight_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Analytics/Redshift.svg b/aws-icons-svg/Analytics/Redshift.svg new file mode 100644 index 0000000..b789a59 --- /dev/null +++ b/aws-icons-svg/Analytics/Redshift.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-Redshiftct_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/App-Integration/API-Gateway.svg b/aws-icons-svg/App-Integration/API-Gateway.svg new file mode 100644 index 0000000..19247ff --- /dev/null +++ b/aws-icons-svg/App-Integration/API-Gateway.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_ Amazon-API-Gateway_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/App-Integration/AppFlow.svg b/aws-icons-svg/App-Integration/AppFlow.svg new file mode 100644 index 0000000..a9d42d5 --- /dev/null +++ b/aws-icons-svg/App-Integration/AppFlow.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_Amazon-AppFlow_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/App-Integration/AppSync.svg b/aws-icons-svg/App-Integration/AppSync.svg new file mode 100644 index 0000000..68731a7 --- /dev/null +++ b/aws-icons-svg/App-Integration/AppSync.svg @@ -0,0 +1,20 @@ + + + + Icon-Architecture/64/Arch_AWS-AppSync_64 + Created with Sketch. + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/App-Integration/Console-Mobile-Application.svg b/aws-icons-svg/App-Integration/Console-Mobile-Application.svg new file mode 100644 index 0000000..4116299 --- /dev/null +++ b/aws-icons-svg/App-Integration/Console-Mobile-Application.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_AWS-Console-Mobile-Application _64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/App-Integration/EventBridge.svg b/aws-icons-svg/App-Integration/EventBridge.svg new file mode 100644 index 0000000..469a4d3 --- /dev/null +++ b/aws-icons-svg/App-Integration/EventBridge.svg @@ -0,0 +1,16 @@ + + + Icon-Architecture/64/Arch_Amazon-EventBridge_64 + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/App-Integration/Express-Workflows.svg b/aws-icons-svg/App-Integration/Express-Workflows.svg new file mode 100644 index 0000000..de5fabc --- /dev/null +++ b/aws-icons-svg/App-Integration/Express-Workflows.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Express-Workflow_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/App-Integration/MQ.svg b/aws-icons-svg/App-Integration/MQ.svg new file mode 100644 index 0000000..e9d7f41 --- /dev/null +++ b/aws-icons-svg/App-Integration/MQ.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-MQ_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/App-Integration/Managed-Workflows-for-Apache-Airflow.svg b/aws-icons-svg/App-Integration/Managed-Workflows-for-Apache-Airflow.svg new file mode 100644 index 0000000..420ba07 --- /dev/null +++ b/aws-icons-svg/App-Integration/Managed-Workflows-for-Apache-Airflow.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_CN-Pinwheel_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/App-Integration/Simple-Notification-Service.svg b/aws-icons-svg/App-Integration/Simple-Notification-Service.svg new file mode 100644 index 0000000..6cb54ad --- /dev/null +++ b/aws-icons-svg/App-Integration/Simple-Notification-Service.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Simple-Notification-Service_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/App-Integration/Simple-Queue-Service.svg b/aws-icons-svg/App-Integration/Simple-Queue-Service.svg new file mode 100644 index 0000000..20726b5 --- /dev/null +++ b/aws-icons-svg/App-Integration/Simple-Queue-Service.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Simple-Queue-Service_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/App-Integration/Step-Functions.svg b/aws-icons-svg/App-Integration/Step-Functions.svg new file mode 100644 index 0000000..db87650 --- /dev/null +++ b/aws-icons-svg/App-Integration/Step-Functions.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Step-Functions_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Blockchain/Managed-Blockchain.svg b/aws-icons-svg/Blockchain/Managed-Blockchain.svg new file mode 100644 index 0000000..d3c2c89 --- /dev/null +++ b/aws-icons-svg/Blockchain/Managed-Blockchain.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-Managed-Blockchain_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Blockchain/Quantum-Ledger-Database.svg b/aws-icons-svg/Blockchain/Quantum-Ledger-Database.svg new file mode 100644 index 0000000..973946f --- /dev/null +++ b/aws-icons-svg/Blockchain/Quantum-Ledger-Database.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-Quantum-Ledger-Database_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Business-Applications/Alexa-For-Business.svg b/aws-icons-svg/Business-Applications/Alexa-For-Business.svg new file mode 100644 index 0000000..9235e0b --- /dev/null +++ b/aws-icons-svg/Business-Applications/Alexa-For-Business.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Alexa-For-Business_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Business-Applications/Chime-SDK.svg b/aws-icons-svg/Business-Applications/Chime-SDK.svg new file mode 100644 index 0000000..d96d8c1 --- /dev/null +++ b/aws-icons-svg/Business-Applications/Chime-SDK.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_Amazon-Chime-SDK_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Business-Applications/Chime-Voice-Connector.svg b/aws-icons-svg/Business-Applications/Chime-Voice-Connector.svg new file mode 100644 index 0000000..e3bb94b --- /dev/null +++ b/aws-icons-svg/Business-Applications/Chime-Voice-Connector.svg @@ -0,0 +1,16 @@ + + + Icon-Architecture/64/Arch_Amazon-Chime-Voice-Connector_64 + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Business-Applications/Chime.svg b/aws-icons-svg/Business-Applications/Chime.svg new file mode 100644 index 0000000..f1259c5 --- /dev/null +++ b/aws-icons-svg/Business-Applications/Chime.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-Chime_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Business-Applications/Connect.svg b/aws-icons-svg/Business-Applications/Connect.svg new file mode 100644 index 0000000..f037a10 --- /dev/null +++ b/aws-icons-svg/Business-Applications/Connect.svg @@ -0,0 +1,16 @@ + + + Icon-Architecture/64/Arch_Amazon-Connect_64 + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Business-Applications/Honeycode.svg b/aws-icons-svg/Business-Applications/Honeycode.svg new file mode 100644 index 0000000..02b9a3c --- /dev/null +++ b/aws-icons-svg/Business-Applications/Honeycode.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_Amazon-Honeycode_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Business-Applications/Pinpoint-APIs.svg b/aws-icons-svg/Business-Applications/Pinpoint-APIs.svg new file mode 100644 index 0000000..a622f19 --- /dev/null +++ b/aws-icons-svg/Business-Applications/Pinpoint-APIs.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_Amazon-Pinpoint-APIs_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Business-Applications/Pinpoint.svg b/aws-icons-svg/Business-Applications/Pinpoint.svg new file mode 100644 index 0000000..96a87a7 --- /dev/null +++ b/aws-icons-svg/Business-Applications/Pinpoint.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_Amazon-Pinpoint_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Business-Applications/Simple-Email-Service.svg b/aws-icons-svg/Business-Applications/Simple-Email-Service.svg new file mode 100644 index 0000000..c907bb3 --- /dev/null +++ b/aws-icons-svg/Business-Applications/Simple-Email-Service.svg @@ -0,0 +1,16 @@ + + + Icon-Architecture/64/Arch_Amazon-Simple-Email-Service_64 + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Business-Applications/Supply-Chain.svg b/aws-icons-svg/Business-Applications/Supply-Chain.svg new file mode 100644 index 0000000..4cc7ead --- /dev/null +++ b/aws-icons-svg/Business-Applications/Supply-Chain.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_AWS-Supply-Chain_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Business-Applications/Wickr.svg b/aws-icons-svg/Business-Applications/Wickr.svg new file mode 100644 index 0000000..85a4d01 --- /dev/null +++ b/aws-icons-svg/Business-Applications/Wickr.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_AWS-Wickr_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Business-Applications/WorkDocs-SDK.svg b/aws-icons-svg/Business-Applications/WorkDocs-SDK.svg new file mode 100644 index 0000000..8fec941 --- /dev/null +++ b/aws-icons-svg/Business-Applications/WorkDocs-SDK.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_Amazon-WorkDocs-SDK_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Business-Applications/WorkDocs.svg b/aws-icons-svg/Business-Applications/WorkDocs.svg new file mode 100644 index 0000000..b17dbb2 --- /dev/null +++ b/aws-icons-svg/Business-Applications/WorkDocs.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_Amazon-WorkDocs_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Business-Applications/WorkMail.svg b/aws-icons-svg/Business-Applications/WorkMail.svg new file mode 100644 index 0000000..7f79d90 --- /dev/null +++ b/aws-icons-svg/Business-Applications/WorkMail.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-WorkMail_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Cloud-Financial-Management/Application-Cost-Profiler.svg b/aws-icons-svg/Cloud-Financial-Management/Application-Cost-Profiler.svg new file mode 100644 index 0000000..96102f5 --- /dev/null +++ b/aws-icons-svg/Cloud-Financial-Management/Application-Cost-Profiler.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_AWS-Application-Cost-Profiler_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Cloud-Financial-Management/Billing-Conductor.svg b/aws-icons-svg/Cloud-Financial-Management/Billing-Conductor.svg new file mode 100644 index 0000000..5d55b27 --- /dev/null +++ b/aws-icons-svg/Cloud-Financial-Management/Billing-Conductor.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_AWS-Billing-Conductor_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Cloud-Financial-Management/Budgets.svg b/aws-icons-svg/Cloud-Financial-Management/Budgets.svg new file mode 100644 index 0000000..95722e2 --- /dev/null +++ b/aws-icons-svg/Cloud-Financial-Management/Budgets.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Budgets_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Cloud-Financial-Management/Cost-Explorer.svg b/aws-icons-svg/Cloud-Financial-Management/Cost-Explorer.svg new file mode 100644 index 0000000..a0ee6cd --- /dev/null +++ b/aws-icons-svg/Cloud-Financial-Management/Cost-Explorer.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Cost-Explorer_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Cloud-Financial-Management/Cost-and-Usage-Report.svg b/aws-icons-svg/Cloud-Financial-Management/Cost-and-Usage-Report.svg new file mode 100644 index 0000000..45cee69 --- /dev/null +++ b/aws-icons-svg/Cloud-Financial-Management/Cost-and-Usage-Report.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Cost-and-Usage-Report_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Cloud-Financial-Management/Reserved-Instance-Reporting.svg b/aws-icons-svg/Cloud-Financial-Management/Reserved-Instance-Reporting.svg new file mode 100644 index 0000000..0b318fc --- /dev/null +++ b/aws-icons-svg/Cloud-Financial-Management/Reserved-Instance-Reporting.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Reserved-Instance-Reporting_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Cloud-Financial-Management/Savings-Plans.svg b/aws-icons-svg/Cloud-Financial-Management/Savings-Plans.svg new file mode 100644 index 0000000..f94d038 --- /dev/null +++ b/aws-icons-svg/Cloud-Financial-Management/Savings-Plans.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_Savings-Plans_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Compute/App-Runner.svg b/aws-icons-svg/Compute/App-Runner.svg new file mode 100644 index 0000000..1ae3b43 --- /dev/null +++ b/aws-icons-svg/Compute/App-Runner.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_AWS-Fusion_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Compute/Application-Auto-Scaling.svg b/aws-icons-svg/Compute/Application-Auto-Scaling.svg new file mode 100644 index 0000000..dfd9285 --- /dev/null +++ b/aws-icons-svg/Compute/Application-Auto-Scaling.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_Amazon-Application-Auto-Scaling _64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Compute/Batch.svg b/aws-icons-svg/Compute/Batch.svg new file mode 100644 index 0000000..3f03ed9 --- /dev/null +++ b/aws-icons-svg/Compute/Batch.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Batch_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Compute/Bottlerocket.svg b/aws-icons-svg/Compute/Bottlerocket.svg new file mode 100644 index 0000000..879d8f3 --- /dev/null +++ b/aws-icons-svg/Compute/Bottlerocket.svg @@ -0,0 +1,20 @@ + + + + Icon-Architecture/64/Arch_AWS-Bottlerocket_64 + Created with Sketch. + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Compute/Compute-Optimizer.svg b/aws-icons-svg/Compute/Compute-Optimizer.svg new file mode 100644 index 0000000..3368308 --- /dev/null +++ b/aws-icons-svg/Compute/Compute-Optimizer.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_AWS-Compute-Optimizer_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Compute/EC2-Auto-Scaling.svg b/aws-icons-svg/Compute/EC2-Auto-Scaling.svg new file mode 100644 index 0000000..8dcac5f --- /dev/null +++ b/aws-icons-svg/Compute/EC2-Auto-Scaling.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-EC2-Auto-Scaling_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Compute/EC2-Image-Builder.svg b/aws-icons-svg/Compute/EC2-Image-Builder.svg new file mode 100644 index 0000000..18f7b7a --- /dev/null +++ b/aws-icons-svg/Compute/EC2-Image-Builder.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-EC2-Image-Builder_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Compute/EC2.svg b/aws-icons-svg/Compute/EC2.svg new file mode 100644 index 0000000..14f083f --- /dev/null +++ b/aws-icons-svg/Compute/EC2.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-EC2_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Compute/Elastic-Beanstalk.svg b/aws-icons-svg/Compute/Elastic-Beanstalk.svg new file mode 100644 index 0000000..d3af257 --- /dev/null +++ b/aws-icons-svg/Compute/Elastic-Beanstalk.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Elastic-Beanstalk_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Compute/Elastic-Fabric-Adapter.svg b/aws-icons-svg/Compute/Elastic-Fabric-Adapter.svg new file mode 100644 index 0000000..1378d3c --- /dev/null +++ b/aws-icons-svg/Compute/Elastic-Fabric-Adapter.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_Elastic-Fabric-Adapter_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Compute/Fargate.svg b/aws-icons-svg/Compute/Fargate.svg new file mode 100644 index 0000000..b494fc2 --- /dev/null +++ b/aws-icons-svg/Compute/Fargate.svg @@ -0,0 +1,20 @@ + + + + Icon-Architecture/64/Arch_AWS-Fargate_64 + Created with Sketch. + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Compute/Genomics-CLI.svg b/aws-icons-svg/Compute/Genomics-CLI.svg new file mode 100644 index 0000000..3b72d9e --- /dev/null +++ b/aws-icons-svg/Compute/Genomics-CLI.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_Amazon-Genomics-CLI_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Compute/Lambda.svg b/aws-icons-svg/Compute/Lambda.svg new file mode 100644 index 0000000..496ef0e --- /dev/null +++ b/aws-icons-svg/Compute/Lambda.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Lambda_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Compute/Lightsail.svg b/aws-icons-svg/Compute/Lightsail.svg new file mode 100644 index 0000000..30dafa2 --- /dev/null +++ b/aws-icons-svg/Compute/Lightsail.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-Lightsail_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Compute/Local-Zones.svg b/aws-icons-svg/Compute/Local-Zones.svg new file mode 100644 index 0000000..9140b09 --- /dev/null +++ b/aws-icons-svg/Compute/Local-Zones.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Local-Zones_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Compute/NICE-DCV.svg b/aws-icons-svg/Compute/NICE-DCV.svg new file mode 100644 index 0000000..27fcca4 --- /dev/null +++ b/aws-icons-svg/Compute/NICE-DCV.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_AWS-NICE-DCV_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Compute/NICE-EnginFrame.svg b/aws-icons-svg/Compute/NICE-EnginFrame.svg new file mode 100644 index 0000000..9c1df61 --- /dev/null +++ b/aws-icons-svg/Compute/NICE-EnginFrame.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_NICE-EnginFrame_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Compute/Nitro-Enclaves.svg b/aws-icons-svg/Compute/Nitro-Enclaves.svg new file mode 100644 index 0000000..6a964b0 --- /dev/null +++ b/aws-icons-svg/Compute/Nitro-Enclaves.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Nitro-Enclaves_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Compute/Outposts-family.svg b/aws-icons-svg/Compute/Outposts-family.svg new file mode 100644 index 0000000..519ad9a --- /dev/null +++ b/aws-icons-svg/Compute/Outposts-family.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_AWS-Outposts_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Compute/Outposts-rack.svg b/aws-icons-svg/Compute/Outposts-rack.svg new file mode 100644 index 0000000..d8e48f1 --- /dev/null +++ b/aws-icons-svg/Compute/Outposts-rack.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Outposts_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Compute/Outposts-servers.svg b/aws-icons-svg/Compute/Outposts-servers.svg new file mode 100644 index 0000000..918c313 --- /dev/null +++ b/aws-icons-svg/Compute/Outposts-servers.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_AWS-Scout_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Compute/ParallelCluster.svg b/aws-icons-svg/Compute/ParallelCluster.svg new file mode 100644 index 0000000..52566cd --- /dev/null +++ b/aws-icons-svg/Compute/ParallelCluster.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_AWS-Parallel-Cluster_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Compute/Serverless-Application-Repository.svg b/aws-icons-svg/Compute/Serverless-Application-Repository.svg new file mode 100644 index 0000000..de4f0d5 --- /dev/null +++ b/aws-icons-svg/Compute/Serverless-Application-Repository.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Serverless-Application-Repository_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Compute/SimSpace-Weaver.svg b/aws-icons-svg/Compute/SimSpace-Weaver.svg new file mode 100644 index 0000000..07147e3 --- /dev/null +++ b/aws-icons-svg/Compute/SimSpace-Weaver.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_AWS-SimSpace-Weaver_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Compute/Thinkbox-Deadline.svg b/aws-icons-svg/Compute/Thinkbox-Deadline.svg new file mode 100644 index 0000000..fe60ab6 --- /dev/null +++ b/aws-icons-svg/Compute/Thinkbox-Deadline.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_AWS-ThinkBox-Deadline_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Compute/Thinkbox-Frost.svg b/aws-icons-svg/Compute/Thinkbox-Frost.svg new file mode 100644 index 0000000..94f0019 --- /dev/null +++ b/aws-icons-svg/Compute/Thinkbox-Frost.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_AWS-ThinkBox-Frost_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Compute/Thinkbox-Krakatoa.svg b/aws-icons-svg/Compute/Thinkbox-Krakatoa.svg new file mode 100644 index 0000000..b335fba --- /dev/null +++ b/aws-icons-svg/Compute/Thinkbox-Krakatoa.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_AWS-ThinkBox-Krakatoa_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Compute/Thinkbox-Sequoia.svg b/aws-icons-svg/Compute/Thinkbox-Sequoia.svg new file mode 100644 index 0000000..df0c77e --- /dev/null +++ b/aws-icons-svg/Compute/Thinkbox-Sequoia.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_AWS-ThinkBox-Sequoia_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Compute/Thinkbox-Stoke.svg b/aws-icons-svg/Compute/Thinkbox-Stoke.svg new file mode 100644 index 0000000..0a22daf --- /dev/null +++ b/aws-icons-svg/Compute/Thinkbox-Stoke.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_AWS-ThinkBox-Stoke_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Compute/Thinkbox-XMesh.svg b/aws-icons-svg/Compute/Thinkbox-XMesh.svg new file mode 100644 index 0000000..89bcba3 --- /dev/null +++ b/aws-icons-svg/Compute/Thinkbox-XMesh.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_AWS-ThinkBox-XMesh_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Compute/VMware-Cloud-on-AWS.svg b/aws-icons-svg/Compute/VMware-Cloud-on-AWS.svg new file mode 100644 index 0000000..89dd76c --- /dev/null +++ b/aws-icons-svg/Compute/VMware-Cloud-on-AWS.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_VMware-Cloud-on-AWS_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Compute/Wavelength.svg b/aws-icons-svg/Compute/Wavelength.svg new file mode 100644 index 0000000..8aa5eca --- /dev/null +++ b/aws-icons-svg/Compute/Wavelength.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Wavelength_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Containers/ECS-Anywhere.svg b/aws-icons-svg/Containers/ECS-Anywhere.svg new file mode 100644 index 0000000..768dfc1 --- /dev/null +++ b/aws-icons-svg/Containers/ECS-Anywhere.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_Amazon-ECS-Anywhere_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Containers/EKS-Anywhere.svg b/aws-icons-svg/Containers/EKS-Anywhere.svg new file mode 100644 index 0000000..1b35c4f --- /dev/null +++ b/aws-icons-svg/Containers/EKS-Anywhere.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_Amazon-EKS-Anywhere_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Containers/EKS-Cloud.svg b/aws-icons-svg/Containers/EKS-Cloud.svg new file mode 100644 index 0000000..5c4ca06 --- /dev/null +++ b/aws-icons-svg/Containers/EKS-Cloud.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_Amazon-EKS-Cloud_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Containers/EKS-Distro.svg b/aws-icons-svg/Containers/EKS-Distro.svg new file mode 100644 index 0000000..9d072e5 --- /dev/null +++ b/aws-icons-svg/Containers/EKS-Distro.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_Amazon-EKS-Distro_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Containers/Elastic-Container-Registry.svg b/aws-icons-svg/Containers/Elastic-Container-Registry.svg new file mode 100644 index 0000000..2283358 --- /dev/null +++ b/aws-icons-svg/Containers/Elastic-Container-Registry.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-Elastic-Container-Registry_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Containers/Elastic-Container-Service.svg b/aws-icons-svg/Containers/Elastic-Container-Service.svg new file mode 100644 index 0000000..c2ef4c2 --- /dev/null +++ b/aws-icons-svg/Containers/Elastic-Container-Service.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-Elastic-Container-Service_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Containers/Elastic-Kubernetes-Service.svg b/aws-icons-svg/Containers/Elastic-Kubernetes-Service.svg new file mode 100644 index 0000000..8fa8530 --- /dev/null +++ b/aws-icons-svg/Containers/Elastic-Kubernetes-Service.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-Elastic-Container-Kubernetes_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Containers/Fargate.svg b/aws-icons-svg/Containers/Fargate.svg new file mode 100644 index 0000000..b395fa7 --- /dev/null +++ b/aws-icons-svg/Containers/Fargate.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Fargate_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Containers/Red-Hat-OpenShift-Service-on-AWS.svg b/aws-icons-svg/Containers/Red-Hat-OpenShift-Service-on-AWS.svg new file mode 100644 index 0000000..eeb06a7 --- /dev/null +++ b/aws-icons-svg/Containers/Red-Hat-OpenShift-Service-on-AWS.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_Red-Hat-OpenShift-Service-on-AWS_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Customer-Enablement/Activate.svg b/aws-icons-svg/Customer-Enablement/Activate.svg new file mode 100644 index 0000000..0213e68 --- /dev/null +++ b/aws-icons-svg/Customer-Enablement/Activate.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_AWS-Activate_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Customer-Enablement/IQ.svg b/aws-icons-svg/Customer-Enablement/IQ.svg new file mode 100644 index 0000000..255b369 --- /dev/null +++ b/aws-icons-svg/Customer-Enablement/IQ.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_AWS-IQ_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Customer-Enablement/Managed-Services.svg b/aws-icons-svg/Customer-Enablement/Managed-Services.svg new file mode 100644 index 0000000..44778d8 --- /dev/null +++ b/aws-icons-svg/Customer-Enablement/Managed-Services.svg @@ -0,0 +1,20 @@ + + + + Icon-Architecture/64/Arch_AWS-Managed-Services_64 + Created with Sketch. + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Customer-Enablement/Professional-Services.svg b/aws-icons-svg/Customer-Enablement/Professional-Services.svg new file mode 100644 index 0000000..6567bac --- /dev/null +++ b/aws-icons-svg/Customer-Enablement/Professional-Services.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_AWS-Professional-Services_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Customer-Enablement/Support.svg b/aws-icons-svg/Customer-Enablement/Support.svg new file mode 100644 index 0000000..d5f7bda --- /dev/null +++ b/aws-icons-svg/Customer-Enablement/Support.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Support_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Customer-Enablement/Training-Certification.svg b/aws-icons-svg/Customer-Enablement/Training-Certification.svg new file mode 100644 index 0000000..02271ac --- /dev/null +++ b/aws-icons-svg/Customer-Enablement/Training-Certification.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Training-Certification_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Customer-Enablement/rePost.svg b/aws-icons-svg/Customer-Enablement/rePost.svg new file mode 100644 index 0000000..779f644 --- /dev/null +++ b/aws-icons-svg/Customer-Enablement/rePost.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_AWS-rePost_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Database/Aurora.svg b/aws-icons-svg/Database/Aurora.svg new file mode 100644 index 0000000..e780939 --- /dev/null +++ b/aws-icons-svg/Database/Aurora.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-Aurora_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Database/Database-Migration-Service.svg b/aws-icons-svg/Database/Database-Migration-Service.svg new file mode 100644 index 0000000..990dc72 --- /dev/null +++ b/aws-icons-svg/Database/Database-Migration-Service.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Database-Migration_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Database/DocumentDB.svg b/aws-icons-svg/Database/DocumentDB.svg new file mode 100644 index 0000000..9beb442 --- /dev/null +++ b/aws-icons-svg/Database/DocumentDB.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-DocumentDB_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Database/DynamoDB.svg b/aws-icons-svg/Database/DynamoDB.svg new file mode 100644 index 0000000..bd4f2c3 --- /dev/null +++ b/aws-icons-svg/Database/DynamoDB.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-DynamoDB_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Database/ElastiCache.svg b/aws-icons-svg/Database/ElastiCache.svg new file mode 100644 index 0000000..640f358 --- /dev/null +++ b/aws-icons-svg/Database/ElastiCache.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-ElastiCache_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Database/Keyspaces.svg b/aws-icons-svg/Database/Keyspaces.svg new file mode 100644 index 0000000..43eee58 --- /dev/null +++ b/aws-icons-svg/Database/Keyspaces.svg @@ -0,0 +1,16 @@ + + + Icon-Architecture/64/Arch_Amazon-Keyspaces_64 + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Database/MemoryDB-for-Redis.svg b/aws-icons-svg/Database/MemoryDB-for-Redis.svg new file mode 100644 index 0000000..eaaf36e --- /dev/null +++ b/aws-icons-svg/Database/MemoryDB-for-Redis.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_Amazon-MemoryDB-for-Redis_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Database/Neptune.svg b/aws-icons-svg/Database/Neptune.svg new file mode 100644 index 0000000..6392ac5 --- /dev/null +++ b/aws-icons-svg/Database/Neptune.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-Neptune_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Database/RDS-on-VMware.svg b/aws-icons-svg/Database/RDS-on-VMware.svg new file mode 100644 index 0000000..205cf4e --- /dev/null +++ b/aws-icons-svg/Database/RDS-on-VMware.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-RDS-for-VMware_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Database/RDS.svg b/aws-icons-svg/Database/RDS.svg new file mode 100644 index 0000000..245d237 --- /dev/null +++ b/aws-icons-svg/Database/RDS.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-RDS_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Database/Timestream.svg b/aws-icons-svg/Database/Timestream.svg new file mode 100644 index 0000000..e377095 --- /dev/null +++ b/aws-icons-svg/Database/Timestream.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-Timestream_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Developer-Tools/Application-Composer.svg b/aws-icons-svg/Developer-Tools/Application-Composer.svg new file mode 100644 index 0000000..30a0f72 --- /dev/null +++ b/aws-icons-svg/Developer-Tools/Application-Composer.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_AWS-Application-Composer_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Developer-Tools/Cloud-Control-API.svg b/aws-icons-svg/Developer-Tools/Cloud-Control-API.svg new file mode 100644 index 0000000..8ed37e9 --- /dev/null +++ b/aws-icons-svg/Developer-Tools/Cloud-Control-API.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_AWS-Cloud-Control-API_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Developer-Tools/Cloud-Development-Kit.svg b/aws-icons-svg/Developer-Tools/Cloud-Development-Kit.svg new file mode 100644 index 0000000..b0e8ffd --- /dev/null +++ b/aws-icons-svg/Developer-Tools/Cloud-Development-Kit.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Cloud-Development-Kit_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Developer-Tools/Cloud9.svg b/aws-icons-svg/Developer-Tools/Cloud9.svg new file mode 100644 index 0000000..298751a --- /dev/null +++ b/aws-icons-svg/Developer-Tools/Cloud9.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Cloud9_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Developer-Tools/CloudShell.svg b/aws-icons-svg/Developer-Tools/CloudShell.svg new file mode 100644 index 0000000..b204b5c --- /dev/null +++ b/aws-icons-svg/Developer-Tools/CloudShell.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_AWS-CloudShell_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Developer-Tools/CodeArtifact.svg b/aws-icons-svg/Developer-Tools/CodeArtifact.svg new file mode 100644 index 0000000..cb48889 --- /dev/null +++ b/aws-icons-svg/Developer-Tools/CodeArtifact.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_AWS-CodeArtifact_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Developer-Tools/CodeBuild.svg b/aws-icons-svg/Developer-Tools/CodeBuild.svg new file mode 100644 index 0000000..9177bf0 --- /dev/null +++ b/aws-icons-svg/Developer-Tools/CodeBuild.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-CodeBuild_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Developer-Tools/CodeCatalyst.svg b/aws-icons-svg/Developer-Tools/CodeCatalyst.svg new file mode 100644 index 0000000..69061b1 --- /dev/null +++ b/aws-icons-svg/Developer-Tools/CodeCatalyst.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_Amazon-CodeCatalyst_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Developer-Tools/CodeCommit.svg b/aws-icons-svg/Developer-Tools/CodeCommit.svg new file mode 100644 index 0000000..a43d2a4 --- /dev/null +++ b/aws-icons-svg/Developer-Tools/CodeCommit.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-CodeCommit_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Developer-Tools/CodeDeploy.svg b/aws-icons-svg/Developer-Tools/CodeDeploy.svg new file mode 100644 index 0000000..da09b5c --- /dev/null +++ b/aws-icons-svg/Developer-Tools/CodeDeploy.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-CodeDeploy_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Developer-Tools/CodePipeline.svg b/aws-icons-svg/Developer-Tools/CodePipeline.svg new file mode 100644 index 0000000..2a0431c --- /dev/null +++ b/aws-icons-svg/Developer-Tools/CodePipeline.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-CodePipeline_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Developer-Tools/CodeStar.svg b/aws-icons-svg/Developer-Tools/CodeStar.svg new file mode 100644 index 0000000..3ad1385 --- /dev/null +++ b/aws-icons-svg/Developer-Tools/CodeStar.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-Codestar_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Developer-Tools/Command-Line-Interface.svg b/aws-icons-svg/Developer-Tools/Command-Line-Interface.svg new file mode 100644 index 0000000..e5d5b40 --- /dev/null +++ b/aws-icons-svg/Developer-Tools/Command-Line-Interface.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Command-Line-Interface_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Developer-Tools/Corretto.svg b/aws-icons-svg/Developer-Tools/Corretto.svg new file mode 100644 index 0000000..48c7f9f --- /dev/null +++ b/aws-icons-svg/Developer-Tools/Corretto.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_Amazon-Corretto_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Developer-Tools/Tools-and-SDKs.svg b/aws-icons-svg/Developer-Tools/Tools-and-SDKs.svg new file mode 100644 index 0000000..a2bace3 --- /dev/null +++ b/aws-icons-svg/Developer-Tools/Tools-and-SDKs.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Tools-and-SDK_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Developer-Tools/X-Ray.svg b/aws-icons-svg/Developer-Tools/X-Ray.svg new file mode 100644 index 0000000..39c446c --- /dev/null +++ b/aws-icons-svg/Developer-Tools/X-Ray.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-X-Ray_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/End-User-Computing/AppStream.svg b/aws-icons-svg/End-User-Computing/AppStream.svg new file mode 100644 index 0000000..a0108ee --- /dev/null +++ b/aws-icons-svg/End-User-Computing/AppStream.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-App-Stream_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/End-User-Computing/WorkLink.svg b/aws-icons-svg/End-User-Computing/WorkLink.svg new file mode 100644 index 0000000..79678c5 --- /dev/null +++ b/aws-icons-svg/End-User-Computing/WorkLink.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-WorkLink_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/End-User-Computing/WorkSpaces-Family.svg b/aws-icons-svg/End-User-Computing/WorkSpaces-Family.svg new file mode 100644 index 0000000..9a17c17 --- /dev/null +++ b/aws-icons-svg/End-User-Computing/WorkSpaces-Family.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_Amazon-WorkSpaces-Family_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Front-End-Web-Mobile/Amplify.svg b/aws-icons-svg/Front-End-Web-Mobile/Amplify.svg new file mode 100644 index 0000000..111b9e7 --- /dev/null +++ b/aws-icons-svg/Front-End-Web-Mobile/Amplify.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Amplify-Console_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Front-End-Web-Mobile/Device-Farm.svg b/aws-icons-svg/Front-End-Web-Mobile/Device-Farm.svg new file mode 100644 index 0000000..6fc4119 --- /dev/null +++ b/aws-icons-svg/Front-End-Web-Mobile/Device-Farm.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Device-Farm_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Front-End-Web-Mobile/Location-Service.svg b/aws-icons-svg/Front-End-Web-Mobile/Location-Service.svg new file mode 100644 index 0000000..0b727a2 --- /dev/null +++ b/aws-icons-svg/Front-End-Web-Mobile/Location-Service.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_Amazon-Location-Service_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Games/GameKit.svg b/aws-icons-svg/Games/GameKit.svg new file mode 100644 index 0000000..b0a2543 --- /dev/null +++ b/aws-icons-svg/Games/GameKit.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_AWS-GameKit_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Games/GameLift.svg b/aws-icons-svg/Games/GameLift.svg new file mode 100644 index 0000000..0e73c2f --- /dev/null +++ b/aws-icons-svg/Games/GameLift.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_Amazon-GameLift_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Games/GameSparks.svg b/aws-icons-svg/Games/GameSparks.svg new file mode 100644 index 0000000..7f2c820 --- /dev/null +++ b/aws-icons-svg/Games/GameSparks.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_Amazon-GameSparks_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Games/Lumberyard.svg b/aws-icons-svg/Games/Lumberyard.svg new file mode 100644 index 0000000..659ea28 --- /dev/null +++ b/aws-icons-svg/Games/Lumberyard.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_Amazon-Lumberyard_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Games/Open-3D-Engine.svg b/aws-icons-svg/Games/Open-3D-Engine.svg new file mode 100644 index 0000000..043e84a --- /dev/null +++ b/aws-icons-svg/Games/Open-3D-Engine.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_Open-3D-Engine_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/General-Icons/Marketplace_Dark.svg b/aws-icons-svg/General-Icons/Marketplace_Dark.svg new file mode 100644 index 0000000..4f73e1a --- /dev/null +++ b/aws-icons-svg/General-Icons/Marketplace_Dark.svg @@ -0,0 +1,10 @@ + + + Icon-Architecture/64/Arch_AWS-Marketplace_Dark_64 + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/General-Icons/Marketplace_Light.svg b/aws-icons-svg/General-Icons/Marketplace_Light.svg new file mode 100644 index 0000000..158546f --- /dev/null +++ b/aws-icons-svg/General-Icons/Marketplace_Light.svg @@ -0,0 +1,10 @@ + + + Icon-Architecture/64/Arch_AWS-Marketplace_Light_64 + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Internet-of-Things/FreeRTOS.svg b/aws-icons-svg/Internet-of-Things/FreeRTOS.svg new file mode 100644 index 0000000..27826ef --- /dev/null +++ b/aws-icons-svg/Internet-of-Things/FreeRTOS.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-FreeRTOS_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Internet-of-Things/IoT-1-Click.svg b/aws-icons-svg/Internet-of-Things/IoT-1-Click.svg new file mode 100644 index 0000000..a285637 --- /dev/null +++ b/aws-icons-svg/Internet-of-Things/IoT-1-Click.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-IoT-1-Click_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Internet-of-Things/IoT-Analytics.svg b/aws-icons-svg/Internet-of-Things/IoT-Analytics.svg new file mode 100644 index 0000000..eeade03 --- /dev/null +++ b/aws-icons-svg/Internet-of-Things/IoT-Analytics.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-IoT-Analytics_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Internet-of-Things/IoT-Button.svg b/aws-icons-svg/Internet-of-Things/IoT-Button.svg new file mode 100644 index 0000000..c0f23f9 --- /dev/null +++ b/aws-icons-svg/Internet-of-Things/IoT-Button.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-IoT-Button_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Internet-of-Things/IoT-Core.svg b/aws-icons-svg/Internet-of-Things/IoT-Core.svg new file mode 100644 index 0000000..f2c84c3 --- /dev/null +++ b/aws-icons-svg/Internet-of-Things/IoT-Core.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-IoT-Core_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Internet-of-Things/IoT-Device-Defender.svg b/aws-icons-svg/Internet-of-Things/IoT-Device-Defender.svg new file mode 100644 index 0000000..4c76965 --- /dev/null +++ b/aws-icons-svg/Internet-of-Things/IoT-Device-Defender.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-IoT-Device-Defender_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Internet-of-Things/IoT-Device-Management.svg b/aws-icons-svg/Internet-of-Things/IoT-Device-Management.svg new file mode 100644 index 0000000..945761d --- /dev/null +++ b/aws-icons-svg/Internet-of-Things/IoT-Device-Management.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-IoT-Device-Management_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Internet-of-Things/IoT-EduKit.svg b/aws-icons-svg/Internet-of-Things/IoT-EduKit.svg new file mode 100644 index 0000000..2cf645b --- /dev/null +++ b/aws-icons-svg/Internet-of-Things/IoT-EduKit.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_AWS-IoT-EduKit_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Internet-of-Things/IoT-Events.svg b/aws-icons-svg/Internet-of-Things/IoT-Events.svg new file mode 100644 index 0000000..1c6479c --- /dev/null +++ b/aws-icons-svg/Internet-of-Things/IoT-Events.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-IoT-Events_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Internet-of-Things/IoT-ExpressLink.svg b/aws-icons-svg/Internet-of-Things/IoT-ExpressLink.svg new file mode 100644 index 0000000..4bf5cf3 --- /dev/null +++ b/aws-icons-svg/Internet-of-Things/IoT-ExpressLink.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_AWS-IoT-ExpressLink_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Internet-of-Things/IoT-FleetWise.svg b/aws-icons-svg/Internet-of-Things/IoT-FleetWise.svg new file mode 100644 index 0000000..85c2c83 --- /dev/null +++ b/aws-icons-svg/Internet-of-Things/IoT-FleetWise.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_AWS-IoT-FleetWise_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Internet-of-Things/IoT-Greengrass.svg b/aws-icons-svg/Internet-of-Things/IoT-Greengrass.svg new file mode 100644 index 0000000..b23fa17 --- /dev/null +++ b/aws-icons-svg/Internet-of-Things/IoT-Greengrass.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-IoT-Greengrass-Core_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Internet-of-Things/IoT-RoboRunner.svg b/aws-icons-svg/Internet-of-Things/IoT-RoboRunner.svg new file mode 100644 index 0000000..4c3e76e --- /dev/null +++ b/aws-icons-svg/Internet-of-Things/IoT-RoboRunner.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_AWS-IoT-RoboRunner_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Internet-of-Things/IoT-SiteWise.svg b/aws-icons-svg/Internet-of-Things/IoT-SiteWise.svg new file mode 100644 index 0000000..d7c8bca --- /dev/null +++ b/aws-icons-svg/Internet-of-Things/IoT-SiteWise.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-IoT-SiteWise_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Internet-of-Things/IoT-Things-Graph.svg b/aws-icons-svg/Internet-of-Things/IoT-Things-Graph.svg new file mode 100644 index 0000000..8a8452a --- /dev/null +++ b/aws-icons-svg/Internet-of-Things/IoT-Things-Graph.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-IoT-Things-Graph_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Internet-of-Things/IoT-TwinMaker.svg b/aws-icons-svg/Internet-of-Things/IoT-TwinMaker.svg new file mode 100644 index 0000000..fdf4999 --- /dev/null +++ b/aws-icons-svg/Internet-of-Things/IoT-TwinMaker.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/aws-icons-svg/Machine-Learning/Apache-MXNet-on-AWS.svg b/aws-icons-svg/Machine-Learning/Apache-MXNet-on-AWS.svg new file mode 100644 index 0000000..39bd25f --- /dev/null +++ b/aws-icons-svg/Machine-Learning/Apache-MXNet-on-AWS.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Apache-MXNet_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Machine-Learning/Augmented-AI-A2I.svg b/aws-icons-svg/Machine-Learning/Augmented-AI-A2I.svg new file mode 100644 index 0000000..677801f --- /dev/null +++ b/aws-icons-svg/Machine-Learning/Augmented-AI-A2I.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-Augmented-AI-A2I_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Machine-Learning/CodeGuru.svg b/aws-icons-svg/Machine-Learning/CodeGuru.svg new file mode 100644 index 0000000..43a7fea --- /dev/null +++ b/aws-icons-svg/Machine-Learning/CodeGuru.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-CodeGuru_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Machine-Learning/CodeWhisperer.svg b/aws-icons-svg/Machine-Learning/CodeWhisperer.svg new file mode 100644 index 0000000..748900f --- /dev/null +++ b/aws-icons-svg/Machine-Learning/CodeWhisperer.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_Amazon-CodeWhisperer_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Machine-Learning/Comprehend-Medical.svg b/aws-icons-svg/Machine-Learning/Comprehend-Medical.svg new file mode 100644 index 0000000..bff521d --- /dev/null +++ b/aws-icons-svg/Machine-Learning/Comprehend-Medical.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_Amazon-Comprehend-Medical_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Machine-Learning/Comprehend.svg b/aws-icons-svg/Machine-Learning/Comprehend.svg new file mode 100644 index 0000000..769b07d --- /dev/null +++ b/aws-icons-svg/Machine-Learning/Comprehend.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-Comprehend_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Machine-Learning/Deep-Learning-AMIs.svg b/aws-icons-svg/Machine-Learning/Deep-Learning-AMIs.svg new file mode 100644 index 0000000..18be917 --- /dev/null +++ b/aws-icons-svg/Machine-Learning/Deep-Learning-AMIs.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_AWS-Deep-Learning-AMIs_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Machine-Learning/Deep-Learning-Containers.svg b/aws-icons-svg/Machine-Learning/Deep-Learning-Containers.svg new file mode 100644 index 0000000..5c1c701 --- /dev/null +++ b/aws-icons-svg/Machine-Learning/Deep-Learning-Containers.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Deep-Learning-Containers_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Machine-Learning/DeepComposer.svg b/aws-icons-svg/Machine-Learning/DeepComposer.svg new file mode 100644 index 0000000..cf5e997 --- /dev/null +++ b/aws-icons-svg/Machine-Learning/DeepComposer.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Deep-Composer_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Machine-Learning/DeepLens.svg b/aws-icons-svg/Machine-Learning/DeepLens.svg new file mode 100644 index 0000000..e349eaf --- /dev/null +++ b/aws-icons-svg/Machine-Learning/DeepLens.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Deep-Lense_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Machine-Learning/DeepRacer.svg b/aws-icons-svg/Machine-Learning/DeepRacer.svg new file mode 100644 index 0000000..41ad61c --- /dev/null +++ b/aws-icons-svg/Machine-Learning/DeepRacer.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Deepracer_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Machine-Learning/DevOps-Guru.svg b/aws-icons-svg/Machine-Learning/DevOps-Guru.svg new file mode 100644 index 0000000..8b6d339 --- /dev/null +++ b/aws-icons-svg/Machine-Learning/DevOps-Guru.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_CN-Capstone_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Machine-Learning/Elastic-Inference.svg b/aws-icons-svg/Machine-Learning/Elastic-Inference.svg new file mode 100644 index 0000000..eb04d26 --- /dev/null +++ b/aws-icons-svg/Machine-Learning/Elastic-Inference.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-Elastic-Inference_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Machine-Learning/Forecast.svg b/aws-icons-svg/Machine-Learning/Forecast.svg new file mode 100644 index 0000000..96a6b1e --- /dev/null +++ b/aws-icons-svg/Machine-Learning/Forecast.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-Forecast_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Machine-Learning/Fraud-Detector.svg b/aws-icons-svg/Machine-Learning/Fraud-Detector.svg new file mode 100644 index 0000000..4124857 --- /dev/null +++ b/aws-icons-svg/Machine-Learning/Fraud-Detector.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-Fraud-Detector_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Machine-Learning/HealthLake.svg b/aws-icons-svg/Machine-Learning/HealthLake.svg new file mode 100644 index 0000000..17465b5 --- /dev/null +++ b/aws-icons-svg/Machine-Learning/HealthLake.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_AWS-Colossus_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Machine-Learning/Kendra.svg b/aws-icons-svg/Machine-Learning/Kendra.svg new file mode 100644 index 0000000..7b6c2d5 --- /dev/null +++ b/aws-icons-svg/Machine-Learning/Kendra.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-Kendra_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Machine-Learning/Lex.svg b/aws-icons-svg/Machine-Learning/Lex.svg new file mode 100644 index 0000000..a98de68 --- /dev/null +++ b/aws-icons-svg/Machine-Learning/Lex.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-Lex_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Machine-Learning/Lookout-for-Equipment.svg b/aws-icons-svg/Machine-Learning/Lookout-for-Equipment.svg new file mode 100644 index 0000000..6de0bac --- /dev/null +++ b/aws-icons-svg/Machine-Learning/Lookout-for-Equipment.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_Amazon-Hogun_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Machine-Learning/Lookout-for-Metrics.svg b/aws-icons-svg/Machine-Learning/Lookout-for-Metrics.svg new file mode 100644 index 0000000..1315f65 --- /dev/null +++ b/aws-icons-svg/Machine-Learning/Lookout-for-Metrics.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_Amazon-Poirot_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Machine-Learning/Lookout-for-Vision.svg b/aws-icons-svg/Machine-Learning/Lookout-for-Vision.svg new file mode 100644 index 0000000..8a75f85 --- /dev/null +++ b/aws-icons-svg/Machine-Learning/Lookout-for-Vision.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_CN-Lyra_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Machine-Learning/Monitron.svg b/aws-icons-svg/Machine-Learning/Monitron.svg new file mode 100644 index 0000000..9348f8c --- /dev/null +++ b/aws-icons-svg/Machine-Learning/Monitron.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_Amazon-Monitron_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Machine-Learning/Neuron.svg b/aws-icons-svg/Machine-Learning/Neuron.svg new file mode 100644 index 0000000..64692c4 --- /dev/null +++ b/aws-icons-svg/Machine-Learning/Neuron.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-Neuron-ML-SDK_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Machine-Learning/Omics.svg b/aws-icons-svg/Machine-Learning/Omics.svg new file mode 100644 index 0000000..134c4fa --- /dev/null +++ b/aws-icons-svg/Machine-Learning/Omics.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_Amazon-Omics_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Machine-Learning/Panorama.svg b/aws-icons-svg/Machine-Learning/Panorama.svg new file mode 100644 index 0000000..881e1df --- /dev/null +++ b/aws-icons-svg/Machine-Learning/Panorama.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_AWS-Omni_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Machine-Learning/Personalize.svg b/aws-icons-svg/Machine-Learning/Personalize.svg new file mode 100644 index 0000000..2c51942 --- /dev/null +++ b/aws-icons-svg/Machine-Learning/Personalize.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-Personalize_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Machine-Learning/Polly.svg b/aws-icons-svg/Machine-Learning/Polly.svg new file mode 100644 index 0000000..a95d8dd --- /dev/null +++ b/aws-icons-svg/Machine-Learning/Polly.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-Polly_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Machine-Learning/Rekognition.svg b/aws-icons-svg/Machine-Learning/Rekognition.svg new file mode 100644 index 0000000..9b512be --- /dev/null +++ b/aws-icons-svg/Machine-Learning/Rekognition.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-Rekognition_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Machine-Learning/SageMaker-Ground-Truth.svg b/aws-icons-svg/Machine-Learning/SageMaker-Ground-Truth.svg new file mode 100644 index 0000000..18807c6 --- /dev/null +++ b/aws-icons-svg/Machine-Learning/SageMaker-Ground-Truth.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-SageMaker-Ground-Truth_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Machine-Learning/SageMaker-Studio-Lab.svg b/aws-icons-svg/Machine-Learning/SageMaker-Studio-Lab.svg new file mode 100644 index 0000000..5e5f1e5 --- /dev/null +++ b/aws-icons-svg/Machine-Learning/SageMaker-Studio-Lab.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_Amazon-SageMaker-Studio-Lab_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Machine-Learning/SageMaker.svg b/aws-icons-svg/Machine-Learning/SageMaker.svg new file mode 100644 index 0000000..542ea19 --- /dev/null +++ b/aws-icons-svg/Machine-Learning/SageMaker.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-SageMaker_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Machine-Learning/TensorFlow-on-AWS.svg b/aws-icons-svg/Machine-Learning/TensorFlow-on-AWS.svg new file mode 100644 index 0000000..e38f078 --- /dev/null +++ b/aws-icons-svg/Machine-Learning/TensorFlow-on-AWS.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-TensorFlow-on-AWS_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Machine-Learning/Textract.svg b/aws-icons-svg/Machine-Learning/Textract.svg new file mode 100644 index 0000000..fa00d88 --- /dev/null +++ b/aws-icons-svg/Machine-Learning/Textract.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Textract_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Machine-Learning/TorchServe.svg b/aws-icons-svg/Machine-Learning/TorchServe.svg new file mode 100644 index 0000000..43d1ae5 --- /dev/null +++ b/aws-icons-svg/Machine-Learning/TorchServe.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_TorchServe_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Machine-Learning/Transcribe.svg b/aws-icons-svg/Machine-Learning/Transcribe.svg new file mode 100644 index 0000000..ff998cc --- /dev/null +++ b/aws-icons-svg/Machine-Learning/Transcribe.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-Transcribe_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Machine-Learning/Translate.svg b/aws-icons-svg/Machine-Learning/Translate.svg new file mode 100644 index 0000000..67e9a39 --- /dev/null +++ b/aws-icons-svg/Machine-Learning/Translate.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-Translate_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Management-Governance/AppConfig.svg b/aws-icons-svg/Management-Governance/AppConfig.svg new file mode 100644 index 0000000..b30a6c3 --- /dev/null +++ b/aws-icons-svg/Management-Governance/AppConfig.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-App-Config_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Management-Governance/Application-Auto-Scaling.svg b/aws-icons-svg/Management-Governance/Application-Auto-Scaling.svg new file mode 100644 index 0000000..f82f9f0 --- /dev/null +++ b/aws-icons-svg/Management-Governance/Application-Auto-Scaling.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_AWS-Application-Auto-Scaling_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Management-Governance/Auto-Scaling.svg b/aws-icons-svg/Management-Governance/Auto-Scaling.svg new file mode 100644 index 0000000..1ac5561 --- /dev/null +++ b/aws-icons-svg/Management-Governance/Auto-Scaling.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Auto-Scaling_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Management-Governance/Backint-Agent.svg b/aws-icons-svg/Management-Governance/Backint-Agent.svg new file mode 100644 index 0000000..85abaa4 --- /dev/null +++ b/aws-icons-svg/Management-Governance/Backint-Agent.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_AWS-Backint-Agent_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Management-Governance/Chatbot.svg b/aws-icons-svg/Management-Governance/Chatbot.svg new file mode 100644 index 0000000..0cd7546 --- /dev/null +++ b/aws-icons-svg/Management-Governance/Chatbot.svg @@ -0,0 +1,16 @@ + + + Icon-Architecture/64/Arch_AWS-Chatbot_64 + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Management-Governance/CloudFormation.svg b/aws-icons-svg/Management-Governance/CloudFormation.svg new file mode 100644 index 0000000..c2271fc --- /dev/null +++ b/aws-icons-svg/Management-Governance/CloudFormation.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-CloudFormation_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Management-Governance/CloudTrail.svg b/aws-icons-svg/Management-Governance/CloudTrail.svg new file mode 100644 index 0000000..0c75225 --- /dev/null +++ b/aws-icons-svg/Management-Governance/CloudTrail.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Cloud-Trail_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Management-Governance/CloudWatch.svg b/aws-icons-svg/Management-Governance/CloudWatch.svg new file mode 100644 index 0000000..103369a --- /dev/null +++ b/aws-icons-svg/Management-Governance/CloudWatch.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-CloudWatch_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Management-Governance/Config.svg b/aws-icons-svg/Management-Governance/Config.svg new file mode 100644 index 0000000..c530ce1 --- /dev/null +++ b/aws-icons-svg/Management-Governance/Config.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Config_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Management-Governance/Control-Tower.svg b/aws-icons-svg/Management-Governance/Control-Tower.svg new file mode 100644 index 0000000..9983907 --- /dev/null +++ b/aws-icons-svg/Management-Governance/Control-Tower.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Control-Tower_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Management-Governance/Distro-for-OpenTelemetry.svg b/aws-icons-svg/Management-Governance/Distro-for-OpenTelemetry.svg new file mode 100644 index 0000000..c90de52 --- /dev/null +++ b/aws-icons-svg/Management-Governance/Distro-for-OpenTelemetry.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_AWS-Distro-for-OpenTelemetry_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Management-Governance/Fault-Injection-Simulator.svg b/aws-icons-svg/Management-Governance/Fault-Injection-Simulator.svg new file mode 100644 index 0000000..47532d9 --- /dev/null +++ b/aws-icons-svg/Management-Governance/Fault-Injection-Simulator.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_AWS-Valkyrie_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Management-Governance/Launch-Wizard.svg b/aws-icons-svg/Management-Governance/Launch-Wizard.svg new file mode 100644 index 0000000..d7d4696 --- /dev/null +++ b/aws-icons-svg/Management-Governance/Launch-Wizard.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-App-Wizard_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Management-Governance/License-Manager.svg b/aws-icons-svg/Management-Governance/License-Manager.svg new file mode 100644 index 0000000..d88507f --- /dev/null +++ b/aws-icons-svg/Management-Governance/License-Manager.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-License-Manager_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Management-Governance/Managed-Grafana.svg b/aws-icons-svg/Management-Governance/Managed-Grafana.svg new file mode 100644 index 0000000..024d707 --- /dev/null +++ b/aws-icons-svg/Management-Governance/Managed-Grafana.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_Amazon-Managed-Service-for-Grafana_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Management-Governance/Managed-Service-for-Prometheus.svg b/aws-icons-svg/Management-Governance/Managed-Service-for-Prometheus.svg new file mode 100644 index 0000000..6832970 --- /dev/null +++ b/aws-icons-svg/Management-Governance/Managed-Service-for-Prometheus.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_Amazon-Managed-Service-for-Prometheus_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Management-Governance/Management-Console.svg b/aws-icons-svg/Management-Governance/Management-Console.svg new file mode 100644 index 0000000..4cc9cca --- /dev/null +++ b/aws-icons-svg/Management-Governance/Management-Console.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Management-Console_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Management-Governance/OpsWorks.svg b/aws-icons-svg/Management-Governance/OpsWorks.svg new file mode 100644 index 0000000..03afbd9 --- /dev/null +++ b/aws-icons-svg/Management-Governance/OpsWorks.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-OpsWorks_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Management-Governance/Organizations.svg b/aws-icons-svg/Management-Governance/Organizations.svg new file mode 100644 index 0000000..c2956fe --- /dev/null +++ b/aws-icons-svg/Management-Governance/Organizations.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Organizations_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Management-Governance/Personal-Health-Dashboard.svg b/aws-icons-svg/Management-Governance/Personal-Health-Dashboard.svg new file mode 100644 index 0000000..354efc0 --- /dev/null +++ b/aws-icons-svg/Management-Governance/Personal-Health-Dashboard.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Personal-Health-Dashboard_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Management-Governance/Proton.svg b/aws-icons-svg/Management-Governance/Proton.svg new file mode 100644 index 0000000..0fcab17 --- /dev/null +++ b/aws-icons-svg/Management-Governance/Proton.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_CN-Arrow_Management-Governance_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Management-Governance/Resilience-Hub.svg b/aws-icons-svg/Management-Governance/Resilience-Hub.svg new file mode 100644 index 0000000..a0cb340 --- /dev/null +++ b/aws-icons-svg/Management-Governance/Resilience-Hub.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_AWS-Resilience-Hub_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Management-Governance/Resource-Explorer.svg b/aws-icons-svg/Management-Governance/Resource-Explorer.svg new file mode 100644 index 0000000..9f35e9c --- /dev/null +++ b/aws-icons-svg/Management-Governance/Resource-Explorer.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_AWS-Resource-Explorer_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Management-Governance/Service-Catalog.svg b/aws-icons-svg/Management-Governance/Service-Catalog.svg new file mode 100644 index 0000000..5373c21 --- /dev/null +++ b/aws-icons-svg/Management-Governance/Service-Catalog.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Service-Catalog_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Management-Governance/Service-Management-Connector.svg b/aws-icons-svg/Management-Governance/Service-Management-Connector.svg new file mode 100644 index 0000000..e81dd2f --- /dev/null +++ b/aws-icons-svg/Management-Governance/Service-Management-Connector.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/aws-icons-svg/Management-Governance/Systems-Manager.svg b/aws-icons-svg/Management-Governance/Systems-Manager.svg new file mode 100644 index 0000000..70f9e8c --- /dev/null +++ b/aws-icons-svg/Management-Governance/Systems-Manager.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Systems-Manager_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Management-Governance/Trusted-Advisor.svg b/aws-icons-svg/Management-Governance/Trusted-Advisor.svg new file mode 100644 index 0000000..7aa8a35 --- /dev/null +++ b/aws-icons-svg/Management-Governance/Trusted-Advisor.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Trusted-Advisor_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Management-Governance/Well-Architected-Tool.svg b/aws-icons-svg/Management-Governance/Well-Architected-Tool.svg new file mode 100644 index 0000000..1d5234f --- /dev/null +++ b/aws-icons-svg/Management-Governance/Well-Architected-Tool.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Well-Architect-Tool_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Media-Services/Elastic-Transcoder.svg b/aws-icons-svg/Media-Services/Elastic-Transcoder.svg new file mode 100644 index 0000000..d5a44cf --- /dev/null +++ b/aws-icons-svg/Media-Services/Elastic-Transcoder.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-Elastic-Transcoder_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Media-Services/Elemental-Appliances-&-Software.svg b/aws-icons-svg/Media-Services/Elemental-Appliances-&-Software.svg new file mode 100644 index 0000000..00d84c0 --- /dev/null +++ b/aws-icons-svg/Media-Services/Elemental-Appliances-&-Software.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Elemental-Appliances-&-Software_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Media-Services/Elemental-Conductor.svg b/aws-icons-svg/Media-Services/Elemental-Conductor.svg new file mode 100644 index 0000000..010be83 --- /dev/null +++ b/aws-icons-svg/Media-Services/Elemental-Conductor.svg @@ -0,0 +1,16 @@ + + + Icon-Architecture/64/Arch_AWS-Elemental-Conductor_64 + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Media-Services/Elemental-Delta.svg b/aws-icons-svg/Media-Services/Elemental-Delta.svg new file mode 100644 index 0000000..15e2e17 --- /dev/null +++ b/aws-icons-svg/Media-Services/Elemental-Delta.svg @@ -0,0 +1,16 @@ + + + Icon-Architecture/64/Arch_AWS-Elemental-Delta_64 + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Media-Services/Elemental-Link.svg b/aws-icons-svg/Media-Services/Elemental-Link.svg new file mode 100644 index 0000000..91488b6 --- /dev/null +++ b/aws-icons-svg/Media-Services/Elemental-Link.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_AWS-Elemental-Link_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Media-Services/Elemental-Live.svg b/aws-icons-svg/Media-Services/Elemental-Live.svg new file mode 100644 index 0000000..0101f11 --- /dev/null +++ b/aws-icons-svg/Media-Services/Elemental-Live.svg @@ -0,0 +1,16 @@ + + + Icon-Architecture/64/Arch_AWS-Elemental-Live_64 + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Media-Services/Elemental-MediaConnect.svg b/aws-icons-svg/Media-Services/Elemental-MediaConnect.svg new file mode 100644 index 0000000..16a5a06 --- /dev/null +++ b/aws-icons-svg/Media-Services/Elemental-MediaConnect.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Elemental-MediaConnect_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Media-Services/Elemental-MediaConvert.svg b/aws-icons-svg/Media-Services/Elemental-MediaConvert.svg new file mode 100644 index 0000000..b5e27ea --- /dev/null +++ b/aws-icons-svg/Media-Services/Elemental-MediaConvert.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Elemental-MediaConvert_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Media-Services/Elemental-MediaLive.svg b/aws-icons-svg/Media-Services/Elemental-MediaLive.svg new file mode 100644 index 0000000..a4f13c1 --- /dev/null +++ b/aws-icons-svg/Media-Services/Elemental-MediaLive.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Elemental-MediaLive_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Media-Services/Elemental-MediaPackage.svg b/aws-icons-svg/Media-Services/Elemental-MediaPackage.svg new file mode 100644 index 0000000..a6f9399 --- /dev/null +++ b/aws-icons-svg/Media-Services/Elemental-MediaPackage.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Elemental-MediaPackage_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Media-Services/Elemental-MediaStore.svg b/aws-icons-svg/Media-Services/Elemental-MediaStore.svg new file mode 100644 index 0000000..2cdb284 --- /dev/null +++ b/aws-icons-svg/Media-Services/Elemental-MediaStore.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Elemental-MediaStore_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Media-Services/Elemental-MediaTailor.svg b/aws-icons-svg/Media-Services/Elemental-MediaTailor.svg new file mode 100644 index 0000000..1967650 --- /dev/null +++ b/aws-icons-svg/Media-Services/Elemental-MediaTailor.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Elemental-MediaTailor_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Media-Services/Elemental-Server.svg b/aws-icons-svg/Media-Services/Elemental-Server.svg new file mode 100644 index 0000000..ab6e28a --- /dev/null +++ b/aws-icons-svg/Media-Services/Elemental-Server.svg @@ -0,0 +1,16 @@ + + + Icon-Architecture/64/Arch_AWS-Elemental-Server_64 + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Media-Services/Interactive-Video-Service.svg b/aws-icons-svg/Media-Services/Interactive-Video-Service.svg new file mode 100644 index 0000000..c299750 --- /dev/null +++ b/aws-icons-svg/Media-Services/Interactive-Video-Service.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_Amazon-Interactive-Video_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Media-Services/Kinesis-Video-Streams.svg b/aws-icons-svg/Media-Services/Kinesis-Video-Streams.svg new file mode 100644 index 0000000..6fd61a1 --- /dev/null +++ b/aws-icons-svg/Media-Services/Kinesis-Video-Streams.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-Kinesis-Video-Streams_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Media-Services/Nimble-Studio.svg b/aws-icons-svg/Media-Services/Nimble-Studio.svg new file mode 100644 index 0000000..4649ac2 --- /dev/null +++ b/aws-icons-svg/Media-Services/Nimble-Studio.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_Amazon-Nimble-Studio_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Migration-Transfer/Application-Discovery-Service.svg b/aws-icons-svg/Migration-Transfer/Application-Discovery-Service.svg new file mode 100644 index 0000000..9cd5fea --- /dev/null +++ b/aws-icons-svg/Migration-Transfer/Application-Discovery-Service.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Application-Discovery-Service_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Migration-Transfer/Application-Migration-Service.svg b/aws-icons-svg/Migration-Transfer/Application-Migration-Service.svg new file mode 100644 index 0000000..dac8a5e --- /dev/null +++ b/aws-icons-svg/Migration-Transfer/Application-Migration-Service.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-CloudEndure-Migration_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Migration-Transfer/DataSync.svg b/aws-icons-svg/Migration-Transfer/DataSync.svg new file mode 100644 index 0000000..64c227d --- /dev/null +++ b/aws-icons-svg/Migration-Transfer/DataSync.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Data-Sync_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Migration-Transfer/Mainframe-Modernization.svg b/aws-icons-svg/Migration-Transfer/Mainframe-Modernization.svg new file mode 100644 index 0000000..792cd07 --- /dev/null +++ b/aws-icons-svg/Migration-Transfer/Mainframe-Modernization.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_AWS-Mainframe-Modernization_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Migration-Transfer/Migration-Evaluator.svg b/aws-icons-svg/Migration-Transfer/Migration-Evaluator.svg new file mode 100644 index 0000000..06d3dad --- /dev/null +++ b/aws-icons-svg/Migration-Transfer/Migration-Evaluator.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_AWS-Migration-Evaluator_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Migration-Transfer/Migration-Hub.svg b/aws-icons-svg/Migration-Transfer/Migration-Hub.svg new file mode 100644 index 0000000..457422c --- /dev/null +++ b/aws-icons-svg/Migration-Transfer/Migration-Hub.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Migration-Hub_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Migration-Transfer/Server-Migration-Service.svg b/aws-icons-svg/Migration-Transfer/Server-Migration-Service.svg new file mode 100644 index 0000000..97a5dc4 --- /dev/null +++ b/aws-icons-svg/Migration-Transfer/Server-Migration-Service.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Server-Migration-Service_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Migration-Transfer/Transfer-Family.svg b/aws-icons-svg/Migration-Transfer/Transfer-Family.svg new file mode 100644 index 0000000..1e0c1d6 --- /dev/null +++ b/aws-icons-svg/Migration-Transfer/Transfer-Family.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Transfer-Family_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Networking-Content-Delivery/App-Mesh.svg b/aws-icons-svg/Networking-Content-Delivery/App-Mesh.svg new file mode 100644 index 0000000..93d6f45 --- /dev/null +++ b/aws-icons-svg/Networking-Content-Delivery/App-Mesh.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-App-Mesh_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Networking-Content-Delivery/Client-VPN.svg b/aws-icons-svg/Networking-Content-Delivery/Client-VPN.svg new file mode 100644 index 0000000..0d9346e --- /dev/null +++ b/aws-icons-svg/Networking-Content-Delivery/Client-VPN.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-VPN_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Networking-Content-Delivery/Cloud-Directory.svg b/aws-icons-svg/Networking-Content-Delivery/Cloud-Directory.svg new file mode 100644 index 0000000..55115a3 --- /dev/null +++ b/aws-icons-svg/Networking-Content-Delivery/Cloud-Directory.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-Cloud-Directory_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Networking-Content-Delivery/Cloud-Map.svg b/aws-icons-svg/Networking-Content-Delivery/Cloud-Map.svg new file mode 100644 index 0000000..f4af2aa --- /dev/null +++ b/aws-icons-svg/Networking-Content-Delivery/Cloud-Map.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-CloudMap_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Networking-Content-Delivery/Cloud-WAN.svg b/aws-icons-svg/Networking-Content-Delivery/Cloud-WAN.svg new file mode 100644 index 0000000..e796dc6 --- /dev/null +++ b/aws-icons-svg/Networking-Content-Delivery/Cloud-WAN.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_CN-CloudWAN_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Networking-Content-Delivery/CloudFront.svg b/aws-icons-svg/Networking-Content-Delivery/CloudFront.svg new file mode 100644 index 0000000..49ca192 --- /dev/null +++ b/aws-icons-svg/Networking-Content-Delivery/CloudFront.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-CloudFront_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Networking-Content-Delivery/Direct-Connect.svg b/aws-icons-svg/Networking-Content-Delivery/Direct-Connect.svg new file mode 100644 index 0000000..ff4fdb6 --- /dev/null +++ b/aws-icons-svg/Networking-Content-Delivery/Direct-Connect.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Direct-Connect_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Networking-Content-Delivery/Elastic-Load-Balancing.svg b/aws-icons-svg/Networking-Content-Delivery/Elastic-Load-Balancing.svg new file mode 100644 index 0000000..b79b920 --- /dev/null +++ b/aws-icons-svg/Networking-Content-Delivery/Elastic-Load-Balancing.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Elastic-Load-Balancing_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Networking-Content-Delivery/Global-Accelerator.svg b/aws-icons-svg/Networking-Content-Delivery/Global-Accelerator.svg new file mode 100644 index 0000000..75d3b73 --- /dev/null +++ b/aws-icons-svg/Networking-Content-Delivery/Global-Accelerator.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Global-Accelerator_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Networking-Content-Delivery/Private-5G.svg b/aws-icons-svg/Networking-Content-Delivery/Private-5G.svg new file mode 100644 index 0000000..22565ed --- /dev/null +++ b/aws-icons-svg/Networking-Content-Delivery/Private-5G.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_AWS-Private-5G_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Networking-Content-Delivery/PrivateLink.svg b/aws-icons-svg/Networking-Content-Delivery/PrivateLink.svg new file mode 100644 index 0000000..3057703 --- /dev/null +++ b/aws-icons-svg/Networking-Content-Delivery/PrivateLink.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-PrivateLink_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Networking-Content-Delivery/Route-53.svg b/aws-icons-svg/Networking-Content-Delivery/Route-53.svg new file mode 100644 index 0000000..dbf747c --- /dev/null +++ b/aws-icons-svg/Networking-Content-Delivery/Route-53.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-Route-53_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Networking-Content-Delivery/Site-to-Site-VPN.svg b/aws-icons-svg/Networking-Content-Delivery/Site-to-Site-VPN.svg new file mode 100644 index 0000000..9bd4cd0 --- /dev/null +++ b/aws-icons-svg/Networking-Content-Delivery/Site-to-Site-VPN.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_AWS-Site-to-Site-VPN_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Networking-Content-Delivery/Transit-Gateway.svg b/aws-icons-svg/Networking-Content-Delivery/Transit-Gateway.svg new file mode 100644 index 0000000..6a31681 --- /dev/null +++ b/aws-icons-svg/Networking-Content-Delivery/Transit-Gateway.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Transit-Gateway_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Networking-Content-Delivery/VPC-Lattice.svg b/aws-icons-svg/Networking-Content-Delivery/VPC-Lattice.svg new file mode 100644 index 0000000..1004c2d --- /dev/null +++ b/aws-icons-svg/Networking-Content-Delivery/VPC-Lattice.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_Amazon-VPC-Lattice_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Networking-Content-Delivery/Verified-Access.svg b/aws-icons-svg/Networking-Content-Delivery/Verified-Access.svg new file mode 100644 index 0000000..5840b5a --- /dev/null +++ b/aws-icons-svg/Networking-Content-Delivery/Verified-Access.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_AWS-Verified-Access_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Networking-Content-Delivery/Virtual-Private-Cloud.svg b/aws-icons-svg/Networking-Content-Delivery/Virtual-Private-Cloud.svg new file mode 100644 index 0000000..0568d19 --- /dev/null +++ b/aws-icons-svg/Networking-Content-Delivery/Virtual-Private-Cloud.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-Virtual-Private-Cloud_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Quantum-Technologies/Braket.svg b/aws-icons-svg/Quantum-Technologies/Braket.svg new file mode 100644 index 0000000..21ca6a6 --- /dev/null +++ b/aws-icons-svg/Quantum-Technologies/Braket.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Braket_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Robotics/RoboMaker.svg b/aws-icons-svg/Robotics/RoboMaker.svg new file mode 100644 index 0000000..e4e8533 --- /dev/null +++ b/aws-icons-svg/Robotics/RoboMaker.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-RoboMaker_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Satellite/Ground-Station.svg b/aws-icons-svg/Satellite/Ground-Station.svg new file mode 100644 index 0000000..4baeff2 --- /dev/null +++ b/aws-icons-svg/Satellite/Ground-Station.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Ground-Station_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Security-Identity-Compliance/Artifact.svg b/aws-icons-svg/Security-Identity-Compliance/Artifact.svg new file mode 100644 index 0000000..3fdc29d --- /dev/null +++ b/aws-icons-svg/Security-Identity-Compliance/Artifact.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Artifact_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Security-Identity-Compliance/Audit-Manager.svg b/aws-icons-svg/Security-Identity-Compliance/Audit-Manager.svg new file mode 100644 index 0000000..49a845b --- /dev/null +++ b/aws-icons-svg/Security-Identity-Compliance/Audit-Manager.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_AWS-Audit-Manager_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Security-Identity-Compliance/Certificate-Manager.svg b/aws-icons-svg/Security-Identity-Compliance/Certificate-Manager.svg new file mode 100644 index 0000000..f7388f8 --- /dev/null +++ b/aws-icons-svg/Security-Identity-Compliance/Certificate-Manager.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Certificate-Manager_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Security-Identity-Compliance/Cloud-Directory.svg b/aws-icons-svg/Security-Identity-Compliance/Cloud-Directory.svg new file mode 100644 index 0000000..3faa4a2 --- /dev/null +++ b/aws-icons-svg/Security-Identity-Compliance/Cloud-Directory.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_Amazon-Cloud-Directory_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Security-Identity-Compliance/CloudHSM.svg b/aws-icons-svg/Security-Identity-Compliance/CloudHSM.svg new file mode 100644 index 0000000..25a4ccc --- /dev/null +++ b/aws-icons-svg/Security-Identity-Compliance/CloudHSM.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-CloudHSM_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Security-Identity-Compliance/Cognito.svg b/aws-icons-svg/Security-Identity-Compliance/Cognito.svg new file mode 100644 index 0000000..d9a808e --- /dev/null +++ b/aws-icons-svg/Security-Identity-Compliance/Cognito.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-Cognito_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Security-Identity-Compliance/Detective.svg b/aws-icons-svg/Security-Identity-Compliance/Detective.svg new file mode 100644 index 0000000..132d1c3 --- /dev/null +++ b/aws-icons-svg/Security-Identity-Compliance/Detective.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-Detective_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Security-Identity-Compliance/Directory-Service.svg b/aws-icons-svg/Security-Identity-Compliance/Directory-Service.svg new file mode 100644 index 0000000..0eb0aba --- /dev/null +++ b/aws-icons-svg/Security-Identity-Compliance/Directory-Service.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Directory-Service_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Security-Identity-Compliance/Firewall-Manager.svg b/aws-icons-svg/Security-Identity-Compliance/Firewall-Manager.svg new file mode 100644 index 0000000..3b2f79b --- /dev/null +++ b/aws-icons-svg/Security-Identity-Compliance/Firewall-Manager.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Firewall-Manager_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Security-Identity-Compliance/GuardDuty.svg b/aws-icons-svg/Security-Identity-Compliance/GuardDuty.svg new file mode 100644 index 0000000..e4260ca --- /dev/null +++ b/aws-icons-svg/Security-Identity-Compliance/GuardDuty.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-GuardDuty_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Security-Identity-Compliance/IAM-Identity-Center.svg b/aws-icons-svg/Security-Identity-Compliance/IAM-Identity-Center.svg new file mode 100644 index 0000000..f3be42b --- /dev/null +++ b/aws-icons-svg/Security-Identity-Compliance/IAM-Identity-Center.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Single-Sign-On_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Security-Identity-Compliance/Identity-and-Access-Management.svg b/aws-icons-svg/Security-Identity-Compliance/Identity-and-Access-Management.svg new file mode 100644 index 0000000..618d0b4 --- /dev/null +++ b/aws-icons-svg/Security-Identity-Compliance/Identity-and-Access-Management.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Identity-and-Access-Management_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Security-Identity-Compliance/Inspector.svg b/aws-icons-svg/Security-Identity-Compliance/Inspector.svg new file mode 100644 index 0000000..6b17c21 --- /dev/null +++ b/aws-icons-svg/Security-Identity-Compliance/Inspector.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-Inspector_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Security-Identity-Compliance/Key-Management-Service.svg b/aws-icons-svg/Security-Identity-Compliance/Key-Management-Service.svg new file mode 100644 index 0000000..01a811a --- /dev/null +++ b/aws-icons-svg/Security-Identity-Compliance/Key-Management-Service.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Key-Management-Services_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Security-Identity-Compliance/Macie.svg b/aws-icons-svg/Security-Identity-Compliance/Macie.svg new file mode 100644 index 0000000..0a3c33a --- /dev/null +++ b/aws-icons-svg/Security-Identity-Compliance/Macie.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-Macie_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Security-Identity-Compliance/Network-Firewall.svg b/aws-icons-svg/Security-Identity-Compliance/Network-Firewall.svg new file mode 100644 index 0000000..619d7e4 --- /dev/null +++ b/aws-icons-svg/Security-Identity-Compliance/Network-Firewall.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_AWS-Network-Firewall_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Security-Identity-Compliance/Private-Certificate-Authority.svg b/aws-icons-svg/Security-Identity-Compliance/Private-Certificate-Authority.svg new file mode 100644 index 0000000..9c3f2b3 --- /dev/null +++ b/aws-icons-svg/Security-Identity-Compliance/Private-Certificate-Authority.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/aws-icons-svg/Security-Identity-Compliance/Resource-Access-Manager.svg b/aws-icons-svg/Security-Identity-Compliance/Resource-Access-Manager.svg new file mode 100644 index 0000000..3ff041e --- /dev/null +++ b/aws-icons-svg/Security-Identity-Compliance/Resource-Access-Manager.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Resources-Access-Manager_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Security-Identity-Compliance/Secrets-Manager.svg b/aws-icons-svg/Security-Identity-Compliance/Secrets-Manager.svg new file mode 100644 index 0000000..558b227 --- /dev/null +++ b/aws-icons-svg/Security-Identity-Compliance/Secrets-Manager.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Secrets-Manager_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Security-Identity-Compliance/Security-Hub.svg b/aws-icons-svg/Security-Identity-Compliance/Security-Hub.svg new file mode 100644 index 0000000..b869adb --- /dev/null +++ b/aws-icons-svg/Security-Identity-Compliance/Security-Hub.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Security-Hub_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Security-Identity-Compliance/Security-Lake.svg b/aws-icons-svg/Security-Identity-Compliance/Security-Lake.svg new file mode 100644 index 0000000..4717f40 --- /dev/null +++ b/aws-icons-svg/Security-Identity-Compliance/Security-Lake.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_Amazon-Security-Lake_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Security-Identity-Compliance/Shield.svg b/aws-icons-svg/Security-Identity-Compliance/Shield.svg new file mode 100644 index 0000000..ae8dc6b --- /dev/null +++ b/aws-icons-svg/Security-Identity-Compliance/Shield.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Shield_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Security-Identity-Compliance/Signer.svg b/aws-icons-svg/Security-Identity-Compliance/Signer.svg new file mode 100644 index 0000000..a881711 --- /dev/null +++ b/aws-icons-svg/Security-Identity-Compliance/Signer.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_AWS-Signer_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Security-Identity-Compliance/Verified-Permissions.svg b/aws-icons-svg/Security-Identity-Compliance/Verified-Permissions.svg new file mode 100644 index 0000000..1a7e17c --- /dev/null +++ b/aws-icons-svg/Security-Identity-Compliance/Verified-Permissions.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_Amazon-Verified-Permissions_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Security-Identity-Compliance/WAF.svg b/aws-icons-svg/Security-Identity-Compliance/WAF.svg new file mode 100644 index 0000000..224b2f9 --- /dev/null +++ b/aws-icons-svg/Security-Identity-Compliance/WAF.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_AWS-WAF_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Storage/Backup.svg b/aws-icons-svg/Storage/Backup.svg new file mode 100644 index 0000000..f977328 --- /dev/null +++ b/aws-icons-svg/Storage/Backup.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Backup_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Storage/EFS.svg b/aws-icons-svg/Storage/EFS.svg new file mode 100644 index 0000000..55dbf79 --- /dev/null +++ b/aws-icons-svg/Storage/EFS.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_Amazon-EFS_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Storage/Elastic-Block-Store.svg b/aws-icons-svg/Storage/Elastic-Block-Store.svg new file mode 100644 index 0000000..f5d7ce3 --- /dev/null +++ b/aws-icons-svg/Storage/Elastic-Block-Store.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-Elastic-Block-Store_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Storage/Elastic-Disaster-Recovery.svg b/aws-icons-svg/Storage/Elastic-Disaster-Recovery.svg new file mode 100644 index 0000000..16e4156 --- /dev/null +++ b/aws-icons-svg/Storage/Elastic-Disaster-Recovery.svg @@ -0,0 +1,16 @@ + + + Icon-Architecture/64/Arch_AWS-Elastic-Disaster-Recovery_64 + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Storage/FSx-for-Lustre.svg b/aws-icons-svg/Storage/FSx-for-Lustre.svg new file mode 100644 index 0000000..fc84cb1 --- /dev/null +++ b/aws-icons-svg/Storage/FSx-for-Lustre.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-FSx-for-Lustre_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Storage/FSx-for-NetApp-ONTAP.svg b/aws-icons-svg/Storage/FSx-for-NetApp-ONTAP.svg new file mode 100644 index 0000000..03e16c9 --- /dev/null +++ b/aws-icons-svg/Storage/FSx-for-NetApp-ONTAP.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_Amazon-FSx-for-NetApp_Storage_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Storage/FSx-for-OpenZFS.svg b/aws-icons-svg/Storage/FSx-for-OpenZFS.svg new file mode 100644 index 0000000..88203d3 --- /dev/null +++ b/aws-icons-svg/Storage/FSx-for-OpenZFS.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_Amazon-FSx_OpenZFS_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Storage/FSx-for-WFS.svg b/aws-icons-svg/Storage/FSx-for-WFS.svg new file mode 100644 index 0000000..6e9a788 --- /dev/null +++ b/aws-icons-svg/Storage/FSx-for-WFS.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_Amazon-FSx-for-WFS_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Storage/FSx.svg b/aws-icons-svg/Storage/FSx.svg new file mode 100644 index 0000000..5d06d15 --- /dev/null +++ b/aws-icons-svg/Storage/FSx.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_Amazon-FSx_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Storage/File-Cache.svg b/aws-icons-svg/Storage/File-Cache.svg new file mode 100644 index 0000000..b01caf8 --- /dev/null +++ b/aws-icons-svg/Storage/File-Cache.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_Amazon-File-Cache_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Storage/S3-on-Outposts.svg b/aws-icons-svg/Storage/S3-on-Outposts.svg new file mode 100644 index 0000000..f60b01e --- /dev/null +++ b/aws-icons-svg/Storage/S3-on-Outposts.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_Amazon-S3-On-Outposts_Storage_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Storage/Simple-Storage-Service-Glacier.svg b/aws-icons-svg/Storage/Simple-Storage-Service-Glacier.svg new file mode 100644 index 0000000..d3e8d06 --- /dev/null +++ b/aws-icons-svg/Storage/Simple-Storage-Service-Glacier.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_Amazon-Glacier_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Storage/Simple-Storage-Service.svg b/aws-icons-svg/Storage/Simple-Storage-Service.svg new file mode 100644 index 0000000..425fe62 --- /dev/null +++ b/aws-icons-svg/Storage/Simple-Storage-Service.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_Amazon-Simple-Storage-Service_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Storage/Snowball-Edge.svg b/aws-icons-svg/Storage/Snowball-Edge.svg new file mode 100644 index 0000000..f38bd45 --- /dev/null +++ b/aws-icons-svg/Storage/Snowball-Edge.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Snowball-Edge_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Storage/Snowball.svg b/aws-icons-svg/Storage/Snowball.svg new file mode 100644 index 0000000..799f759 --- /dev/null +++ b/aws-icons-svg/Storage/Snowball.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Snowball_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Storage/Snowcone.svg b/aws-icons-svg/Storage/Snowcone.svg new file mode 100644 index 0000000..fcee172 --- /dev/null +++ b/aws-icons-svg/Storage/Snowcone.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_AWS-Snowcone_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Storage/Snowmobile.svg b/aws-icons-svg/Storage/Snowmobile.svg new file mode 100644 index 0000000..7250539 --- /dev/null +++ b/aws-icons-svg/Storage/Snowmobile.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Snowmobile_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/Storage/Storage-Gateway.svg b/aws-icons-svg/Storage/Storage-Gateway.svg new file mode 100644 index 0000000..76949d2 --- /dev/null +++ b/aws-icons-svg/Storage/Storage-Gateway.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Storage-Gateway_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-icons-svg/VR-AR/Sumerian.svg b/aws-icons-svg/VR-AR/Sumerian.svg new file mode 100644 index 0000000..0767896 --- /dev/null +++ b/aws-icons-svg/VR-AR/Sumerian.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Sumerian_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-schema.png b/aws-schema.png deleted file mode 100644 index fef8f37..0000000 Binary files a/aws-schema.png and /dev/null differ diff --git a/packages/api/src/adapter-aws/AwsApiGatewayAdapter.test.ts b/packages/api/src/adapter-aws/AwsApiGatewayAdapter.test.ts index 9379533..e4f18a0 100644 --- a/packages/api/src/adapter-aws/AwsApiGatewayAdapter.test.ts +++ b/packages/api/src/adapter-aws/AwsApiGatewayAdapter.test.ts @@ -159,16 +159,16 @@ describe('AwsApiGatewayAdapter', () => { }) expect(apiCommands.map((command) => (command as {constructor: {name: string}}).constructor.name)).toEqual([ - 'GetResourcesCommand', 'GetResourcesCommand', 'CreateResourceCommand', 'PutMethodCommand', + 'GetResourcesCommand', 'CreateResourceCommand', 'PutMethodCommand', 'PutIntegrationCommand', 'CreateDeploymentCommand', ]) - expect((apiCommands[2] as CreateResourceCommand).input).toMatchObject({ + expect((apiCommands[1] as CreateResourceCommand).input).toMatchObject({ restApiId: 'a1b2c3', parentId: 'root123', pathPart: 'hello', }) - expect((apiCommands[3] as PutMethodCommand).input).toMatchObject({ + expect((apiCommands[2] as PutMethodCommand).input).toMatchObject({ httpMethod: 'GET', authorizationType: 'NONE', }) - expect((apiCommands[4] as PutIntegrationCommand).input).toMatchObject({ + expect((apiCommands[3] as PutIntegrationCommand).input).toMatchObject({ type: 'AWS_PROXY', integrationHttpMethod: 'POST', uri: 'arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:000000000000:function:hello/invocations', }) @@ -182,13 +182,77 @@ describe('AwsApiGatewayAdapter', () => { }) }) + test('creates missing nested resources and reuses existing path segments', async () => { + const apiCommands: unknown[] = [] + const adapter = new AwsApiGatewayAdapter( + fakeClient((command) => { + apiCommands.push(command) + if (command instanceof GetResourcesCommand) return {items: [ + {id: 'root123', path: '/'}, + {id: 'orders123', parentId: 'root123', pathPart: 'orders', path: '/orders'}, + ]} + if (command instanceof CreateResourceCommand) return {id: 'orderId123'} + if (command instanceof CreateDeploymentCommand) return {id: 'deployment789'} + return {} + }), + fakeLambda((command) => command instanceof GetFunctionCommand + ? {Configuration: {FunctionArn: 'arn:aws:lambda:us-east-1:000000000000:function:hello'}} + : {}), + ) + + const result = await adapter.createApiGatewayLambdaRoute('a1b2c3', { + path: '/orders/{id}', method: 'PATCH', stage: 'dev', functionName: 'hello', + }) + + const creates = apiCommands.filter((command) => command instanceof CreateResourceCommand) as CreateResourceCommand[] + expect(creates).toHaveLength(1) + expect(creates[0]?.input).toMatchObject({parentId: 'orders123', pathPart: '{id}'}) + expect((apiCommands.find((command) => command instanceof PutMethodCommand) as PutMethodCommand).input.httpMethod).toBe('PATCH') + expect(result).toMatchObject({resourceId: 'orderId123', invokeUrl: 'http://localhost:4566/execute-api/a1b2c3/dev/orders/{id}'}) + }) + + test('creates a greedy child under the explicitly selected parent', async () => { + const commands: unknown[] = [] + const lambdaCommands: unknown[] = [] + const adapter = new AwsApiGatewayAdapter(fakeClient((command) => { + commands.push(command) + if (command instanceof GetResourcesCommand) return {items: [ + {id: 'root123', path: '/'}, + {id: 'files123', parentId: 'root123', pathPart: 'files', path: '/files'}, + ]} + if (command instanceof CreateResourceCommand) return {id: 'proxy123'} + if (command instanceof CreateDeploymentCommand) return {id: 'deployment789'} + return {} + }), fakeLambda((command) => { + lambdaCommands.push(command) + return command instanceof GetFunctionCommand + ? {Configuration: {FunctionArn: 'arn:aws:lambda:us-east-1:000000000000:function:files'}} + : {} + })) + + await adapter.createApiGatewayLambdaRoute('a1b2c3', { + path: '/files/{proxy+}', parentResourceId: 'files123', pathPart: '{proxy+}', + method: 'ANY', stage: 'dev', functionName: 'files', + }) + + expect((commands.find((command) => command instanceof CreateResourceCommand) as CreateResourceCommand).input).toMatchObject({ + parentId: 'files123', pathPart: '{proxy+}', + }) + expect((lambdaCommands.find((command) => command instanceof AddPermissionCommand) as AddPermissionCommand).input).toMatchObject({ + SourceArn: 'arn:aws:execute-api:us-east-1:000000000000:a1b2c3/dev/*/files/*', + }) + }) + test('adds a method to an existing resource without creating a duplicate path', async () => { const apiCommands: unknown[] = [] const adapter = new AwsApiGatewayAdapter( fakeClient((command) => { apiCommands.push(command) if (command instanceof GetResourcesCommand) { - return {items: [{id: 'resource456', path: '/hello'}, {id: 'root123', path: '/'}]} + return {items: [ + {id: 'resource456', parentId: 'root123', pathPart: 'hello', path: '/hello'}, + {id: 'root123', path: '/'}, + ]} } if (command instanceof CreateDeploymentCommand) return {id: 'deployment789'} return {} @@ -264,6 +328,25 @@ describe('AwsApiGatewayAdapter', () => { } }) + test('uses PATCH and HEAD for deployed requests and rejects ANY', async () => { + const originalFetch = globalThis.fetch + const requests: Request[] = [] + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + requests.push(new Request(input, init)) + return new Response(null) + }) as typeof fetch + try { + const adapter = new AwsApiGatewayAdapter(fakeClient(() => ({}))) + await adapter.testApiGatewayRoute('a1b2c3', {path: '/orders/1', method: 'PATCH', stage: 'dev', body: '{}', headers: {}, query: ''}) + await adapter.testApiGatewayRoute('a1b2c3', {path: '/orders/1', method: 'HEAD', stage: 'dev', body: '{}', headers: {}, query: ''}) + await expect(adapter.testApiGatewayRoute('a1b2c3', {path: '/orders/1', method: 'ANY', stage: 'dev', body: '', headers: {}, query: ''})).rejects.toThrow('method must be') + expect(requests.map((request) => request.method)).toEqual(['PATCH', 'HEAD']) + expect(await requests[1]?.text()).toBe('') + } finally { + globalThis.fetch = originalFetch + } + }) + test('tests a configured method through the AWS SDK without a deployed stage', async () => { const commands: unknown[] = [] const adapter = new AwsApiGatewayAdapter(fakeClient((command) => { @@ -283,7 +366,7 @@ describe('AwsApiGatewayAdapter', () => { await expect(adapter.testApiGatewayMethod('a1b2c3', { resourceId: 'resource456', - httpMethod: 'POST', + httpMethod: 'ANY', pathWithQueryString: '/orders?dryRun=true', body: '{"name":"Ada"}', headers: {'content-type': 'application/json'}, @@ -298,7 +381,7 @@ describe('AwsApiGatewayAdapter', () => { latency: 17, }) expect((commands[0] as TestInvokeMethodCommand).input).toEqual({ - restApiId: 'a1b2c3', resourceId: 'resource456', httpMethod: 'POST', + restApiId: 'a1b2c3', resourceId: 'resource456', httpMethod: 'ANY', pathWithQueryString: '/orders?dryRun=true', body: '{"name":"Ada"}', headers: {'content-type': 'application/json'}, multiValueHeaders: {'x-tag': ['one', 'two']}, stageVariables: {environment: 'test'}, @@ -310,7 +393,7 @@ describe('AwsApiGatewayAdapter', () => { if (command instanceof GetResourcesCommand) { return {items: [ {id: 'root123', path: '/', resourceMethods: {}}, - {id: 'resource456', path: '/hello', resourceMethods: {GET: true, POST: true}}, + {id: 'resource456', parentId: 'root123', pathPart: 'hello', path: '/hello', resourceMethods: {GET: true, POST: true}}, ]} } if (command instanceof GetMethodCommand) { @@ -323,6 +406,13 @@ describe('AwsApiGatewayAdapter', () => { })) await expect(adapter.listApiGatewayRoutes('a1b2c3')).resolves.toEqual({ + resources: [ + {resourceId: 'root123', parentId: null, path: '/', pathPart: null, methods: []}, + {resourceId: 'resource456', parentId: 'root123', path: '/hello', pathPart: 'hello', methods: [ + {resourceId: 'resource456', path: '/hello', method: 'GET', functionName: 'hello', integrationType: null}, + {resourceId: 'resource456', path: '/hello', method: 'POST', functionName: 'hello', integrationType: null}, + ]}, + ], routes: [ {resourceId: 'resource456', path: '/hello', method: 'GET', functionName: 'hello', integrationType: null}, {resourceId: 'resource456', path: '/hello', method: 'POST', functionName: 'hello', integrationType: null}, @@ -331,6 +421,28 @@ describe('AwsApiGatewayAdapter', () => { }) }) + test('paginates and returns resources that have no configured methods', async () => { + const adapter = new AwsApiGatewayAdapter(fakeClient((command) => { + if (command instanceof GetResourcesCommand && !command.input.position) { + return {items: [{id: 'root123', path: '/', resourceMethods: {}}], position: 'next'} + } + if (command instanceof GetResourcesCommand) { + return {items: [{id: 'orders123', parentId: 'root123', pathPart: 'orders', path: '/orders'}]} + } + if (command instanceof GetStagesCommand) return {item: []} + throw new Error('Unexpected command') + })) + + await expect(adapter.listApiGatewayRoutes('a1b2c3')).resolves.toEqual({ + resources: [ + {resourceId: 'root123', parentId: null, path: '/', pathPart: null, methods: []}, + {resourceId: 'orders123', parentId: 'root123', path: '/orders', pathPart: 'orders', methods: []}, + ], + routes: [], + stages: [], + }) + }) + test('deletes one method and redeploys its stage', async () => { const commands: unknown[] = [] const lambdaCommands: unknown[] = [] diff --git a/packages/api/src/adapter-aws/AwsApiGatewayAdapter.ts b/packages/api/src/adapter-aws/AwsApiGatewayAdapter.ts index dc0d4a8..573ca4b 100644 --- a/packages/api/src/adapter-aws/AwsApiGatewayAdapter.ts +++ b/packages/api/src/adapter-aws/AwsApiGatewayAdapter.ts @@ -18,6 +18,7 @@ import { type Integration, type IntegrationResponse, type MethodResponse, + type Resource, type RestApi, } from '@aws-sdk/client-api-gateway' import {AddPermissionCommand, GetFunctionCommand, RemovePermissionCommand, type LambdaClient} from '@aws-sdk/client-lambda' @@ -82,18 +83,18 @@ export class AwsApiGatewayAdapter implements CloudServiceAdapter { } async createApiGatewayLambdaRoute(apiId: string, input: ApiGatewayLambdaRouteInput): Promise { - const pathPart = routePathPart(input.path) const method = input.method.toUpperCase() const stage = stageName(input.stage) const functionName = stringValue(input.functionName) if (!functionName) throw new ValidationError('functionName is required') - if (!['GET', 'POST', 'PUT', 'DELETE'].includes(method)) { - throw new ValidationError('method must be GET, POST, PUT, or DELETE') + if (!CONFIGURABLE_METHODS.includes(method)) { + throw new ValidationError('method must be GET, POST, PUT, DELETE, PATCH, ANY, or HEAD') } - const resource = await this.resourceForPath(apiId, `/${pathPart}`) ?? await this.createTopLevelResource(apiId, pathPart) + const resource = await this.resolveRouteResource(apiId, input) const resourceId = resource.id if (!resourceId) throw new ValidationError('API Gateway did not return a resource id') + const resourcePath = resource.path ?? routePath(input.path) await this.apiGateway.send(new PutMethodCommand({ restApiId: apiId, @@ -116,10 +117,10 @@ export class AwsApiGatewayAdapter implements CloudServiceAdapter { const permission = { FunctionName: functionName, - StatementId: apiGatewayPermissionStatementId(apiId, stage, method, `/${pathPart}`), + StatementId: apiGatewayPermissionStatementId(apiId, stage, method, resourcePath), Action: 'lambda:InvokeFunction', Principal: 'apigateway.amazonaws.com', - SourceArn: `arn:aws:execute-api:${this.region}:${this.accountId}:${apiId}/${stage}/${method}/${pathPart}`, + SourceArn: apiGatewaySourceArn(this.region, this.accountId, apiId, stage, method, resourcePath), } try { await this.lambda.send(new AddPermissionCommand(permission)) @@ -135,14 +136,14 @@ export class AwsApiGatewayAdapter implements CloudServiceAdapter { const deployment = await this.apiGateway.send(new CreateDeploymentCommand({ restApiId: apiId, stageName: stage, - description: `Lambda proxy route ${method} /${pathPart}`, + description: `Lambda proxy route ${method} ${resourcePath}`, })) if (!deployment.id) throw new ValidationError('API Gateway did not return a deployment id') return { resourceId, deploymentId: deployment.id, - invokeUrl: `${awsEndpoint().replace(/\/$/, '')}/execute-api/${apiId}/${stage}/${pathPart}`, + invokeUrl: `${awsEndpoint().replace(/\/$/, '')}/execute-api/${apiId}/${stage}${resourcePath}`, } } @@ -150,8 +151,8 @@ export class AwsApiGatewayAdapter implements CloudServiceAdapter { const path = testRoutePath(input.path) const method = input.method.toUpperCase() const stage = stageName(input.stage) - if (!['GET', 'POST', 'PUT', 'DELETE'].includes(method)) { - throw new ValidationError('method must be GET, POST, PUT, or DELETE') + if (!DEPLOYED_HTTP_METHODS.includes(method)) { + throw new ValidationError('method must be GET, POST, PUT, DELETE, PATCH, or HEAD') } const encodedPath = path.split('/').map(encodeURIComponent).join('/') @@ -179,8 +180,8 @@ export class AwsApiGatewayAdapter implements CloudServiceAdapter { const httpMethod = input.httpMethod.toUpperCase() const pathWithQueryString = stringValue(input.pathWithQueryString) if (!resourceId) throw new ValidationError('resourceId is required') - if (!['GET', 'POST', 'PUT', 'DELETE'].includes(httpMethod)) { - throw new ValidationError('httpMethod must be GET, POST, PUT, or DELETE') + if (!TEST_INVOKE_METHODS.includes(httpMethod)) { + throw new ValidationError('httpMethod must be a configured API Gateway method') } if (!pathWithQueryString.startsWith('/')) { throw new ValidationError('pathWithQueryString must start with /') @@ -207,19 +208,13 @@ export class AwsApiGatewayAdapter implements CloudServiceAdapter { } async listApiGatewayRoutes(apiId: string): Promise { - const resources = [] - let position: string | undefined - do { - const result = await this.apiGateway.send(new GetResourcesCommand({restApiId: apiId, position, embed: ['methods']})) - resources.push(...(result.items ?? [])) - position = result.position - } while (position) + const resources = await this.resources(apiId, true) const routes: ApiGatewayRouteOverview[] = [] for (const resource of resources) { const methods = resource.resourceMethods ? Object.keys(resource.resourceMethods).sort() - : ['DELETE', 'GET', 'HEAD', 'OPTIONS', 'PATCH', 'POST', 'PUT'] + : [] for (const method of methods) { if (!resource.id || !resource.path) continue let result @@ -244,7 +239,18 @@ export class AwsApiGatewayAdapter implements CloudServiceAdapter { } const stages = await this.apiGateway.send(new GetStagesCommand({restApiId: apiId})) + const resourcesOverview = resources + .filter((resource): resource is Resource & {id: string; path: string} => Boolean(resource.id && resource.path)) + .map((resource) => ({ + resourceId: resource.id, + parentId: resource.parentId ?? null, + path: resource.path, + pathPart: resource.pathPart ?? null, + methods: routes.filter((route) => route.resourceId === resource.id), + })) + .sort((left, right) => left.path.localeCompare(right.path)) return { + resources: resourcesOverview, routes: routes.sort((left, right) => left.path.localeCompare(right.path) || left.method.localeCompare(right.method)), stages: (stages.item ?? []).map((stage) => stage.stageName).filter((stage): stage is string => Boolean(stage)).sort(), } @@ -253,7 +259,7 @@ export class AwsApiGatewayAdapter implements CloudServiceAdapter { async deleteApiGatewayRoute(apiId: string, input: ApiGatewayDeleteRouteInput): Promise { const method = input.method.toUpperCase() const stage = stageName(input.stage) - if (!CORS_METHODS.includes(method)) throw new ValidationError('method must be GET, POST, PUT, DELETE, or OPTIONS') + if (!DELETABLE_METHODS.includes(method)) throw new ValidationError('method is not supported') if (!stringValue(input.resourceId)) throw new ValidationError('resourceId is required') const route = await this.apiGateway.send(new GetMethodCommand({ @@ -294,7 +300,7 @@ export class AwsApiGatewayAdapter implements CloudServiceAdapter { if (!origin) throw new ValidationError('origin is required') if (!headers) throw new ValidationError('headers are required') if (methods.length === 0 || methods.some((method) => !CORS_METHODS.includes(method))) { - throw new ValidationError('methods must contain one or more of GET, POST, PUT, DELETE, or OPTIONS') + throw new ValidationError('methods must contain one or more supported HTTP methods') } const corsParameters = corsResponseParameters(origin, headers, methods) @@ -371,39 +377,57 @@ export class AwsApiGatewayAdapter implements CloudServiceAdapter { return {configuredMethods, proxyMethods} } - private async rootResource(apiId: string) { + private async resources(apiId: string, embedMethods = false): Promise { + const resources: Resource[] = [] let position: string | undefined do { - const result = await this.apiGateway.send(new GetResourcesCommand({restApiId: apiId, position})) - const root = result.items?.find((resource) => resource.path === '/') - if (root?.id) return root + const result = await this.apiGateway.send(new GetResourcesCommand({ + restApiId: apiId, + position, + embed: embedMethods ? ['methods'] : undefined, + })) + resources.push(...(result.items ?? [])) position = result.position } while (position) - throw new ValidationError('API Gateway root resource was not found') + return resources } - private async resourceForPath(apiId: string, path: string) { - let position: string | undefined - do { - const result = await this.apiGateway.send(new GetResourcesCommand({restApiId: apiId, position})) - const resource = result.items?.find((item) => item.path === path) - if (resource?.id) return resource - position = result.position - } while (position) - return undefined - } + private async resolveRouteResource(apiId: string, input: ApiGatewayLambdaRouteInput): Promise { + const resources = await this.resources(apiId) + if (input.parentResourceId || input.pathPart) { + const parentId = stringValue(input.parentResourceId) + const pathPart = routePathPart(input.pathPart ?? '') + const parent = resources.find((resource) => resource.id === parentId) + if (!parent?.id || !parent.path) throw new ValidationError('parentResourceId must identify an existing API Gateway resource') + const existing = resources.find((resource) => resource.parentId === parent.id && resource.pathPart === pathPart) + if (existing?.id) return existing + const created = await this.apiGateway.send(new CreateResourceCommand({restApiId: apiId, parentId: parent.id, pathPart})) + return {...created, pathPart, parentId: parent.id, path: childPath(parent.path, pathPart)} + } - private async createTopLevelResource(apiId: string, pathPart: string) { - const root = await this.rootResource(apiId) - return this.apiGateway.send(new CreateResourceCommand({ - restApiId: apiId, - parentId: root.id, - pathPart, - })) + const parts = routePath(input.path).slice(1).split('/') + const root = resources.find((resource) => resource.path === '/') + if (!root?.id) throw new ValidationError('API Gateway root resource was not found') + let parent: Resource = root + for (const pathPart of parts) { + const existing = resources.find((resource) => resource.parentId === parent.id && resource.pathPart === pathPart) + if (existing?.id) { + parent = existing + continue + } + const created: Resource = await this.apiGateway.send(new CreateResourceCommand({restApiId: apiId, parentId: parent.id, pathPart})) + parent = {...created, pathPart, parentId: parent.id, path: childPath(parent.path ?? '/', pathPart)} + resources.push(parent) + } + return parent } } -const CORS_METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'] +const CONFIGURABLE_METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'ANY', 'HEAD'] +const DEPLOYED_HTTP_METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD'] +const TEST_INVOKE_METHODS = [...CONFIGURABLE_METHODS, 'OPTIONS'] +const CORS_METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'] +const DELETABLE_METHODS = [...CONFIGURABLE_METHODS, 'OPTIONS'] function corsMethodResponseParameters(): Record { return { @@ -424,13 +448,26 @@ function corsResponseParameters(origin: string, headers: string, methods: string } function routePathPart(path: string): string { - const value = stringValue(path).replace(/^\//, '') - if (!/^[A-Za-z0-9._~-]+$/.test(value)) { - throw new ValidationError('path must be one URL segment, for example /orders') + const value = stringValue(path) + if (!/^(?:[A-Za-z0-9._-]+|\{[A-Za-z0-9._-]+\+?})$/.test(value)) { + throw new ValidationError('pathPart must be one API Gateway segment, for example orders, {id}, or {proxy+}') } return value } +function routePath(path: string): string { + const value = stringValue(path) + if (!value.startsWith('/') || value === '/' || value.endsWith('/')) { + throw new ValidationError('path must start with / and contain at least one resource segment') + } + for (const part of value.slice(1).split('/')) routePathPart(part) + return value +} + +function childPath(parentPath: string, pathPart: string): string { + return parentPath === '/' ? `/${pathPart}` : `${parentPath}/${pathPart}` +} + function stageName(stage: string): string { const value = stringValue(stage) if (!/^[A-Za-z0-9_-]+$/.test(value)) { @@ -447,7 +484,7 @@ function queryString(query: string): string { function testRoutePath(path: string): string { const value = stringValue(path) - if (!/^\/(?:[A-Za-z0-9._~{}-]+\/)*[A-Za-z0-9._~{}-]+$/.test(value)) { + if (!/^\/(?:[A-Za-z0-9._~{}+%-]+\/)*[A-Za-z0-9._~{}+%-]+$/.test(value)) { throw new ValidationError('path must be an API Gateway route path, for example /orders or /users/{id}') } return value @@ -459,7 +496,13 @@ function lambdaFunctionName(uri?: string): string | null { } function apiGatewayPermissionStatementId(apiId: string, stage: string, method: string, path: string): string { - return `apigateway-${apiId}-${stage}-${method}-${routePathPart(path)}` + return `apigateway-${apiId}-${stage}-${method}-${path.replace(/[^A-Za-z0-9_-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '')}`.slice(0, 100) +} + +function apiGatewaySourceArn(region: string, accountId: string, apiId: string, stage: string, method: string, path: string): string { + const arnMethod = method === 'ANY' ? '*' : method + const arnPath = path.slice(1).split('/').map((part) => part.startsWith('{') ? '*' : part).join('/') + return `arn:aws:execute-api:${region}:${accountId}:${apiId}/${stage}/${arnMethod}/${arnPath}` } function toResource(api: RestApi): CloudResource { diff --git a/packages/api/src/adapter-aws/AwsCloudFormationAdapter.test.ts b/packages/api/src/adapter-aws/AwsCloudFormationAdapter.test.ts index 4946588..1a572ea 100644 --- a/packages/api/src/adapter-aws/AwsCloudFormationAdapter.test.ts +++ b/packages/api/src/adapter-aws/AwsCloudFormationAdapter.test.ts @@ -1,10 +1,15 @@ import {describe, expect, test} from 'bun:test' import { CreateStackCommand, + CreateChangeSetCommand, + DescribeChangeSetCommand, DeleteStackCommand, + DescribeStackEventsCommand, DescribeStacksCommand, + GetTemplateCommand, ListStackResourcesCommand, ListStacksCommand, + UpdateStackCommand, type CloudFormationClient, } from '@aws-sdk/client-cloudformation' import {AwsCloudFormationAdapter} from './AwsCloudFormationAdapter' @@ -81,6 +86,8 @@ describe('AwsCloudFormationAdapter', () => { const adapter = new AwsCloudFormationAdapter(fakeClient((command) => { if (command instanceof DescribeStacksCommand) return {Stacks: [ordersStack]} if (command instanceof ListStackResourcesCommand) return {StackResourceSummaries: [bucketResource]} + if (command instanceof DescribeStackEventsCommand) return {StackEvents: [{LogicalResourceId: 'OrdersBucket', ResourceStatus: 'CREATE_COMPLETE', Timestamp: new Date('2026-01-02T03:04:07.000Z')}]} + if (command instanceof GetTemplateCommand) return {TemplateBody: '{"Resources":{}}'} throw new Error('Unexpected command') })) @@ -96,6 +103,7 @@ describe('AwsCloudFormationAdapter', () => { resourceType: 'AWS::S3::Bucket', status: 'CREATE_COMPLETE', }]) + expect(result?.metadata.templateBody).toBe('{"Resources":{}}') }) test('paginates ListStackResources and combines resources from every page', async () => { @@ -107,6 +115,8 @@ describe('AwsCloudFormationAdapter', () => { } return {StackResourceSummaries: [{...bucketResource, LogicalResourceId: 'OrdersQueue', PhysicalResourceId: 'orders-queue', ResourceType: 'AWS::SQS::Queue'}]} } + if (command instanceof DescribeStackEventsCommand) return {StackEvents: []} + if (command instanceof GetTemplateCommand) return {TemplateBody: '{}'} throw new Error('Unexpected command') })) @@ -153,6 +163,43 @@ describe('AwsCloudFormationAdapter', () => { expect(result.metadata.stackId).toBe(ordersStack.StackId) }) + test('passes parameters and capabilities to CloudFormation', async () => { + const adapter = new AwsCloudFormationAdapter(fakeClient((command) => { + if (command instanceof CreateStackCommand) { + expect(command.input).toMatchObject({ + Parameters: [{ParameterKey: 'Environment', ParameterValue: 'test'}], + Capabilities: ['CAPABILITY_NAMED_IAM'], + }) + return {StackId: ordersStack.StackId} + } + if (command instanceof CreateChangeSetCommand) { + expect(command.input).toMatchObject({ + Parameters: [{ParameterKey: 'Environment', ParameterValue: 'test'}], + Capabilities: ['CAPABILITY_NAMED_IAM'], + }) + return {Id: 'change-set-id'} + } + if (command instanceof DescribeChangeSetCommand) return {ChangeSetName: 'planned', Status: 'CREATE_COMPLETE', ExecutionStatus: 'AVAILABLE'} + throw new Error('Unexpected command') + })) + + await adapter.create({values: {stackName: 'orders-stack', templateBody: '{}', parameters: '{"Environment":"test"}', capabilities: 'CAPABILITY_NAMED_IAM'}}) + await adapter.createCloudFormationChangeSet('orders-stack', {name: 'planned', templateBody: '{}', parameters: {Environment: 'test'}, capabilities: ['CAPABILITY_NAMED_IAM']}) + }) + + test('creates a stack from an S3 template URL', async () => { + const adapter = new AwsCloudFormationAdapter(fakeClient((command) => { + expect(command).toBeInstanceOf(CreateStackCommand) + expect((command as CreateStackCommand).input).toEqual({ + StackName: 'orders-stack', + TemplateURL: 'https://templates.s3.amazonaws.com/orders.yaml', + }) + return {StackId: ordersStack.StackId} + })) + + await expect(adapter.create({values: {stackName: 'orders-stack', templateUrl: 'https://templates.s3.amazonaws.com/orders.yaml'}})).resolves.toMatchObject({id: 'orders-stack'}) + }) + test('rejects a missing stack name before calling CloudFormation', async () => { let called = false const adapter = new AwsCloudFormationAdapter(fakeClient(() => { @@ -164,14 +211,14 @@ describe('AwsCloudFormationAdapter', () => { expect(called).toBeFalse() }) - test('rejects a missing template body before calling CloudFormation', async () => { + test('rejects missing template input before calling CloudFormation', async () => { let called = false const adapter = new AwsCloudFormationAdapter(fakeClient(() => { called = true return {} })) - await expect(adapter.create({values: {stackName: 'orders-stack'}})).rejects.toThrow('templateBody is required') + await expect(adapter.create({values: {stackName: 'orders-stack'}})).rejects.toThrow('templateBody or templateUrl is required') expect(called).toBeFalse() }) @@ -185,6 +232,22 @@ describe('AwsCloudFormationAdapter', () => { await adapter.delete('orders-stack') }) + test('updates a stack template then reads back the stack workspace', async () => { + const adapter = new AwsCloudFormationAdapter(fakeClient((command) => { + if (command instanceof UpdateStackCommand) { + expect(command.input).toEqual({StackName: 'orders-stack', TemplateBody: '{"Resources":{"Queue":{"Type":"AWS::SQS::Queue"}}}'}) + return {StackId: ordersStack.StackId} + } + if (command instanceof DescribeStacksCommand) return {Stacks: [ordersStack]} + if (command instanceof ListStackResourcesCommand) return {StackResourceSummaries: []} + if (command instanceof DescribeStackEventsCommand) return {StackEvents: []} + if (command instanceof GetTemplateCommand) return {TemplateBody: '{}'} + throw new Error('Unexpected command') + })) + + await expect(adapter.update('orders-stack', {values: {templateBody: '{"Resources":{"Queue":{"Type":"AWS::SQS::Queue"}}}'}})).resolves.toMatchObject({id: 'orders-stack'}) + }) + test('returns the AWS CloudFormation schema', () => { const adapter = new AwsCloudFormationAdapter(fakeClient(() => ({}))) @@ -192,7 +255,7 @@ describe('AwsCloudFormationAdapter', () => { cloud: 'aws', service: 'iac', displayName: 'CloudFormation', - actions: ['list', 'create', 'delete', 'inspect'], + actions: ['list', 'create', 'update', 'delete', 'inspect'], }) }) }) diff --git a/packages/api/src/adapter-aws/AwsCloudFormationAdapter.ts b/packages/api/src/adapter-aws/AwsCloudFormationAdapter.ts index 12925a8..8f1072b 100644 --- a/packages/api/src/adapter-aws/AwsCloudFormationAdapter.ts +++ b/packages/api/src/adapter-aws/AwsCloudFormationAdapter.ts @@ -2,17 +2,28 @@ import { CloudFormationClient, CreateStackCommand, DeleteStackCommand, + CreateChangeSetCommand, + DeleteChangeSetCommand, + DescribeStackEventsCommand, + DescribeChangeSetCommand, DescribeStacksCommand, + GetTemplateCommand, + ExecuteChangeSetCommand, ListStackResourcesCommand, ListStacksCommand, + ListChangeSetsCommand, + UpdateStackCommand, + UpdateTerminationProtectionCommand, type Stack, + type DescribeChangeSetOutput, + type Capability, type StackResourceSummary, type StackSummary, } from '@aws-sdk/client-cloudformation' import {cloudformation as defaultCloudFormation} from '../aws' import {awsCloudformationSchema} from '../cloud-spi/cloudformationSchema' import {ValidationError} from '../cloud-spi/errors' -import type {CloudResource, CloudServiceAdapter, CreateResourceInput, ResourceQuery, ServiceSchema} from '../cloud-spi/types' +import type {CloudFormationChangeSet, CloudFormationChangeSetInput, CloudResource, CloudServiceAdapter, CreateResourceInput, ResourceQuery, ServiceSchema, UpdateResourceInput} from '../cloud-spi/types' export class AwsCloudFormationAdapter implements CloudServiceAdapter { readonly cloud = 'aws' as const @@ -51,7 +62,11 @@ export class AwsCloudFormationAdapter implements CloudServiceAdapter { nextToken = resources.NextToken } while (nextToken) - return stackToResource(stack, resourceSummaries) + const [events, template] = await Promise.all([ + this.cloudformation.send(new DescribeStackEventsCommand({StackName: id})), + this.cloudformation.send(new GetTemplateCommand({StackName: id, TemplateStage: 'Original'})), + ]) + return stackToResource(stack, resourceSummaries, events.StackEvents ?? [], template.TemplateBody) } catch (error) { if (isStackNotFound(error)) return null throw error @@ -61,10 +76,12 @@ export class AwsCloudFormationAdapter implements CloudServiceAdapter { async create(input: CreateResourceInput): Promise { const stackName = stringValue(input.values.stackName) const templateBody = stringValue(input.values.templateBody) + const templateUrl = stringValue(input.values.templateUrl) if (!stackName) throw new ValidationError('stackName is required') - if (!templateBody) throw new ValidationError('templateBody is required') + if (!templateBody && !templateUrl) throw new ValidationError('templateBody or templateUrl is required') - const res = await this.cloudformation.send(new CreateStackCommand({StackName: stackName, TemplateBody: templateBody})) + const options = stackOptions(input.values) + const res = await this.cloudformation.send(new CreateStackCommand({StackName: stackName, ...(templateBody ? {TemplateBody: templateBody} : {TemplateURL: templateUrl}), ...options})) return { id: stackName, name: stackName, @@ -84,6 +101,46 @@ export class AwsCloudFormationAdapter implements CloudServiceAdapter { async delete(id: string): Promise { await this.cloudformation.send(new DeleteStackCommand({StackName: id})) } + + async update(id: string, input: UpdateResourceInput): Promise { + const templateBody = stringValue(input.values.templateBody) + if (!templateBody) throw new ValidationError('templateBody is required') + await this.cloudformation.send(new UpdateStackCommand({StackName: id, TemplateBody: templateBody, ...stackOptions(input.values)})) + const stack = await this.get(id) + if (!stack) throw new ValidationError(`Stack ${id} was updated but could not be read back.`) + return stack + } + + async listCloudFormationChangeSets(stackName: string): Promise { + const response = await this.cloudformation.send(new ListChangeSetsCommand({StackName: stackName})) + return Promise.all((response.Summaries ?? []).filter((summary) => Boolean(summary.ChangeSetName)).map(async (summary) => { + const name = summary.ChangeSetName as string + const detail = await this.cloudformation.send(new DescribeChangeSetCommand({StackName: stackName, ChangeSetName: name})) + return changeSet(detail, name) + })) + } + + async createCloudFormationChangeSet(stackName: string, input: CloudFormationChangeSetInput): Promise { + const name = stringValue(input.name) + const templateBody = stringValue(input.templateBody) + if (!name || !templateBody) throw new ValidationError('change set name and templateBody are required') + const capabilities = capabilitiesValue(input.capabilities) + const created = await this.cloudformation.send(new CreateChangeSetCommand({StackName: stackName, ChangeSetName: name, ChangeSetType: 'UPDATE', TemplateBody: templateBody, ...(input.parameters ? {Parameters: parameterEntries(input.parameters)} : {}), ...(capabilities.length ? {Capabilities: capabilities} : {})})) + const detail = await this.cloudformation.send(new DescribeChangeSetCommand({StackName: stackName, ChangeSetName: created.Id ?? name})) + return changeSet(detail, name) + } + + async executeCloudFormationChangeSet(stackName: string, changeSetName: string): Promise { + await this.cloudformation.send(new ExecuteChangeSetCommand({StackName: stackName, ChangeSetName: changeSetName})) + } + + async deleteCloudFormationChangeSet(stackName: string, changeSetName: string): Promise { + await this.cloudformation.send(new DeleteChangeSetCommand({StackName: stackName, ChangeSetName: changeSetName})) + } + + async updateCloudFormationTerminationProtection(stackName: string, enabled: boolean): Promise { + await this.cloudformation.send(new UpdateTerminationProtectionCommand({StackName: stackName, EnableTerminationProtection: enabled})) + } } function summaryToResource(summary: StackSummary): CloudResource { @@ -105,7 +162,7 @@ function summaryToResource(summary: StackSummary): CloudResource { } } -function stackToResource(stack: Stack, resources: StackResourceSummary[]): CloudResource { +function stackToResource(stack: Stack, resources: StackResourceSummary[], events: Array<{Timestamp?: Date; ResourceStatus?: string; LogicalResourceId?: string; ResourceStatusReason?: string}> = [], templateBody?: string): CloudResource { const id = stack.StackName ?? '' return { id, @@ -122,6 +179,8 @@ function stackToResource(stack: Stack, resources: StackResourceSummary[]): Cloud description: stack.Description, lastUpdatedTime: stack.LastUpdatedTime?.toISOString(), capabilities: stack.Capabilities, + templateBody: templateBody ?? null, + parameters: (stack.Parameters ?? []).map((parameter) => ({key: parameter.ParameterKey, value: parameter.ParameterValue})), enableTerminationProtection: stack.EnableTerminationProtection, outputs: (stack.Outputs ?? []).map((output) => ({ key: output.OutputKey, @@ -135,14 +194,57 @@ function stackToResource(stack: Stack, resources: StackResourceSummary[]): Cloud resourceType: resource.ResourceType, status: resource.ResourceStatus, })), + events: events.map((event) => ({timestamp: event.Timestamp?.toISOString(), status: event.ResourceStatus, logicalId: event.LogicalResourceId, reason: event.ResourceStatusReason})), }, } } +function changeSet(detail: DescribeChangeSetOutput, fallbackName: string): CloudFormationChangeSet { + return { + name: detail.ChangeSetName ?? fallbackName, + id: detail.ChangeSetId ?? detail.ChangeSetName ?? fallbackName, + status: detail.Status ?? null, + executionStatus: detail.ExecutionStatus ?? null, + changes: (detail.Changes ?? []).map((change) => ({ + action: change.ResourceChange?.Action ?? null, + logicalResourceId: change.ResourceChange?.LogicalResourceId ?? null, + resourceType: change.ResourceChange?.ResourceType ?? null, + })), + } +} + function stringValue(value: unknown): string { return typeof value === 'string' ? value.trim() : '' } +function stackOptions(values: Record): {Parameters?: Array<{ParameterKey: string; ParameterValue: string}>; Capabilities?: Capability[]} { + const parameters = parametersValue(values.parameters) + const capabilities = capabilitiesValue(values.capabilities) + return {...(Object.keys(parameters).length ? {Parameters: parameterEntries(parameters)} : {}), ...(capabilities.length ? {Capabilities: capabilities} : {})} +} + +function parametersValue(value: unknown): Record { + if (!value) return {} + if (typeof value === 'object' && !Array.isArray(value)) return Object.fromEntries(Object.entries(value).filter((entry): entry is [string, string] => typeof entry[1] === 'string')) + if (typeof value !== 'string' || !value.trim()) return {} + try { + const parsed: unknown = JSON.parse(value) + if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed) || Object.values(parsed).some((item) => typeof item !== 'string')) throw new Error() + return parsed as Record + } catch { + throw new ValidationError('parameters must be a JSON object with string values') + } +} + +function capabilitiesValue(value: unknown): Capability[] { + const capabilities = Array.isArray(value) ? value.filter((item): item is string => typeof item === 'string' && item.trim().length > 0) : typeof value === 'string' ? value.split(',').map((item) => item.trim()).filter(Boolean) : [] + return capabilities as Capability[] +} + +function parameterEntries(parameters: Record): Array<{ParameterKey: string; ParameterValue: string}> { + return Object.entries(parameters).map(([ParameterKey, ParameterValue]) => ({ParameterKey, ParameterValue})) +} + function filterBySearch(resources: CloudResource[], search?: string): CloudResource[] { const normalized = search?.trim().toLowerCase() if (!normalized) return resources diff --git a/packages/api/src/adapter-aws/AwsDynamoDbAdapter.test.ts b/packages/api/src/adapter-aws/AwsDynamoDbAdapter.test.ts index 7ad645b..9eaa046 100644 --- a/packages/api/src/adapter-aws/AwsDynamoDbAdapter.test.ts +++ b/packages/api/src/adapter-aws/AwsDynamoDbAdapter.test.ts @@ -1,17 +1,19 @@ import {describe, expect, test} from 'bun:test' import { CreateTableCommand, + DeleteItemCommand, DeleteTableCommand, DescribeTableCommand, ListTablesCommand, PutItemCommand, + QueryCommand, ScanCommand, type DynamoDBClient, } from '@aws-sdk/client-dynamodb' import {AwsDynamoDbAdapter} from './AwsDynamoDbAdapter' import {ValidationError} from '../cloud-spi/errors' -type Command = CreateTableCommand | DeleteTableCommand | DescribeTableCommand | ListTablesCommand | PutItemCommand | ScanCommand +type Command = CreateTableCommand | DeleteItemCommand | DeleteTableCommand | DescribeTableCommand | ListTablesCommand | PutItemCommand | QueryCommand | ScanCommand function fakeClient(send: (command: Command) => Promise): DynamoDBClient { return {send} as unknown as DynamoDBClient @@ -192,7 +194,43 @@ describe('AwsDynamoDbAdapter', () => { expect(captured?.input.TableName).toBe('orders') }) - test('lists and unmarshalls table records across scan pages', async () => { + test('deletes an item with its typed DynamoDB key', async () => { + let captured: DeleteItemCommand | undefined + const client = fakeClient(async (command) => { + if (command instanceof DescribeTableCommand) return {Table: { + KeySchema: [{AttributeName: 'accountId', KeyType: 'HASH'}, {AttributeName: 'sequence', KeyType: 'RANGE'}], + AttributeDefinitions: [{AttributeName: 'accountId', AttributeType: 'S'}, {AttributeName: 'sequence', AttributeType: 'N'}], + }} + captured = command as DeleteItemCommand + return {} + }) + + await new AwsDynamoDbAdapter(client).items.deleteItem!('orders', '{"accountId":"acct-1","sequence":"9007199254740993"}') + + expect(captured?.input).toEqual({ + TableName: 'orders', + Key: {accountId: {S: 'acct-1'}, sequence: {N: '9007199254740993'}}, + }) + }) + + test('does not delete an item without its complete primary key', async () => { + let deleteCalled = false + const client = fakeClient(async (command) => { + if (command instanceof DescribeTableCommand) return {Table: { + KeySchema: [{AttributeName: 'accountId', KeyType: 'HASH'}, {AttributeName: 'sequence', KeyType: 'RANGE'}], + AttributeDefinitions: [{AttributeName: 'accountId', AttributeType: 'S'}, {AttributeName: 'sequence', AttributeType: 'N'}], + }} + if (command instanceof DeleteItemCommand) deleteCalled = true + return {} + }) + + await expect(new AwsDynamoDbAdapter(client).items.deleteItem!('orders', '{"accountId":"acct-1"}')) + .rejects.toThrow('Key attribute sequence is required') + + expect(deleteCalled).toBe(false) + }) + + test('lists one unmarshalled scan page and returns an opaque cursor', async () => { const client = fakeClient(async (command) => { if (command instanceof DescribeTableCommand) { return {Table: {KeySchema: [ @@ -201,31 +239,25 @@ describe('AwsDynamoDbAdapter', () => { ]}} } if (command instanceof ScanCommand) { - expect(command.input.Limit).toBe(command.input.ExclusiveStartKey ? 99 : 100) - return command.input.ExclusiveStartKey - ? {Items: [{accountId: {S: 'acct-2'}, createdAt: {N: '9007199254740993'}, active: {BOOL: false}, roles: {SS: ['admin', 'editor']}}]} - : { - Items: [{accountId: {S: 'acct-1'}, createdAt: {N: '1'}, active: {BOOL: true}}], - LastEvaluatedKey: {accountId: {S: 'acct-1'}, createdAt: {N: '1'}}, - } + expect(command.input.Limit).toBe(100) + return { + Items: [{accountId: {S: 'acct-1'}, createdAt: {N: '1'}, active: {BOOL: true}}], + LastEvaluatedKey: {accountId: {S: 'acct-1'}, createdAt: {N: '1'}}, Count: 1, ScannedCount: 3, + ConsumedCapacity: {CapacityUnits: 0.5}, + } } throw new Error('Unexpected command') }) - const result = await new AwsDynamoDbAdapter(client).listNoSqlItems('orders') + const result = await new AwsDynamoDbAdapter(client).items.listItems('orders') - expect(result).toEqual([ + expect(result).toMatchObject({items: [ { - id: '{"accountId":"acct-1","createdAt":1}', - key: {accountId: 'acct-1', createdAt: 1}, - document: {accountId: 'acct-1', createdAt: 1, active: true}, + id: '{"accountId":"acct-1","createdAt":"1"}', collectionId: null, timestamp: null, + body: {accountId: 'acct-1', createdAt: 1, active: true}, metadata: {key: {accountId: 'acct-1', createdAt: '1'}}, }, - { - id: '{"accountId":"acct-2","createdAt":"9007199254740993"}', - key: {accountId: 'acct-2', createdAt: '9007199254740993'}, - document: {accountId: 'acct-2', createdAt: '9007199254740993', active: false, roles: ['admin', 'editor']}, - }, - ]) + ], count: 1, scannedCount: 3, consumedCapacity: 0.5}) + expect(result.nextCursor).toBeString() }) test('stops scanning after 100 records', async () => { @@ -245,10 +277,84 @@ describe('AwsDynamoDbAdapter', () => { throw new Error('Unexpected command') }) - const result = await new AwsDynamoDbAdapter(client).listNoSqlItems('orders') + const result = await new AwsDynamoDbAdapter(client).items.listItems('orders') - expect(result).toHaveLength(100) + expect(result.items).toHaveLength(100) expect(scanCount).toBe(1) + expect(result.nextCursor).toBeString() + }) + + test('round-trips typed pagination keys through the opaque cursor', async () => { + const scans: ScanCommand[] = [] + const client = fakeClient(async (command) => { + if (command instanceof DescribeTableCommand) return {Table: { + KeySchema: [{AttributeName: 'pk', KeyType: 'HASH'}, {AttributeName: 'sk', KeyType: 'RANGE'}], + AttributeDefinitions: [{AttributeName: 'pk', AttributeType: 'B'}, {AttributeName: 'sk', AttributeType: 'N'}], + }} + if (command instanceof ScanCommand) { + scans.push(command) + return scans.length === 1 ? {LastEvaluatedKey: {pk: {B: Buffer.from('key')}, sk: {N: '9007199254740993'}}} : {} + } + throw new Error('Unexpected command') + }) + const adapter = new AwsDynamoDbAdapter(client) + + const first = await adapter.items.listItems('orders', {limit: 10}) + await adapter.items.listItems('orders', {limit: 10, cursor: first.nextCursor!}) + + expect(first.nextCursor).not.toContain('9007199254740993') + expect(scans[1].input.ExclusiveStartKey).toEqual({pk: {B: Buffer.from('key')}, sk: {N: '9007199254740993'}}) + await expect(adapter.items.listItems('other', {cursor: first.nextCursor!})).rejects.toThrow('cursor is not valid') + }) + + test('builds a typed server-side scan filter', async () => { + let captured: ScanCommand | undefined + const client = fakeClient(async (command) => { + if (command instanceof DescribeTableCommand) return {Table: {KeySchema: [{AttributeName: 'id', KeyType: 'HASH'}]}} + captured = command as ScanCommand + return {Count: 0, ScannedCount: 8} + }) + + const result = await new AwsDynamoDbAdapter(client).items.queryItems!('orders', { + operation: 'scan', filter: {attribute: 'total', operator: 'BETWEEN', type: 'N', value: '10.5', secondValue: '20'}, + }, {limit: 25}) + + expect(captured?.input).toMatchObject({ + TableName: 'orders', Limit: 25, FilterExpression: '#filter BETWEEN :filter AND :filter2', + ExpressionAttributeNames: {'#filter': 'total'}, + ExpressionAttributeValues: {':filter': {N: '10.5'}, ':filter2': {N: '20'}}, + }) + expect(result).toMatchObject({count: 0, scannedCount: 8}) + }) + + test('queries a selected GSI with typed partition and sort conditions', async () => { + let captured: QueryCommand | undefined + const client = fakeClient(async (command) => { + if (command instanceof DescribeTableCommand) return {Table: { + KeySchema: [{AttributeName: 'id', KeyType: 'HASH'}], + AttributeDefinitions: [ + {AttributeName: 'id', AttributeType: 'S'}, {AttributeName: 'accountId', AttributeType: 'S'}, {AttributeName: 'sequence', AttributeType: 'N'}, + ], + GlobalSecondaryIndexes: [{IndexName: 'account-sequence', KeySchema: [ + {AttributeName: 'accountId', KeyType: 'HASH'}, {AttributeName: 'sequence', KeyType: 'RANGE'}, + ]}], + }} + captured = command as QueryCommand + return {Items: [{id: {S: 'order-1'}, accountId: {S: 'acct-1'}, sequence: {N: '12'}}], Count: 1} + }) + + const result = await new AwsDynamoDbAdapter(client).items.queryItems!('orders', { + operation: 'query', indexName: 'account-sequence', partitionKeyValue: 'acct-1', + sortKeyCondition: {operator: 'BETWEEN', value: '10', secondValue: '20'}, + }, {limit: 15}) + + expect(captured?.input).toMatchObject({ + TableName: 'orders', IndexName: 'account-sequence', Limit: 15, + KeyConditionExpression: '#pk = :pk AND #sk BETWEEN :sk AND :sk2', + ExpressionAttributeNames: {'#pk': 'accountId', '#sk': 'sequence'}, + ExpressionAttributeValues: {':pk': {S: 'acct-1'}, ':sk': {N: '10'}, ':sk2': {N: '20'}}, + }) + expect(result.items[0].metadata.key).toEqual({id: 'order-1'}) }) test('puts a record using the table key types', async () => { @@ -273,7 +379,7 @@ describe('AwsDynamoDbAdapter', () => { throw new Error('Unexpected command') }) - const result = await new AwsDynamoDbAdapter(client).putNoSqlItem('orders', { + const result = await new AwsDynamoDbAdapter(client).items.putItem('orders', { accountId: 'acct-1', sequence: '9007199254740993', active: true, @@ -289,8 +395,10 @@ describe('AwsDynamoDbAdapter', () => { }) expect(result).toEqual({ id: '{"accountId":"acct-1","sequence":"9007199254740993"}', - key: {accountId: 'acct-1', sequence: '9007199254740993'}, - document: {accountId: 'acct-1', sequence: '9007199254740993', active: true}, + collectionId: null, + timestamp: null, + body: {accountId: 'acct-1', sequence: '9007199254740993', active: true}, + metadata: {key: {accountId: 'acct-1', sequence: '9007199254740993'}}, }) }) @@ -307,10 +415,32 @@ describe('AwsDynamoDbAdapter', () => { return {} }) - const result = await new AwsDynamoDbAdapter(client).putNoSqlItem('blobs', {id: 'aGVsbG8=', name: 'hello'}) + const result = await new AwsDynamoDbAdapter(client).items.putItem('blobs', {id: 'aGVsbG8=', name: 'hello'}) expect(captured?.input.Item?.id.B).toEqual(Buffer.from('hello')) - expect(result.key).toEqual({id: 'aGVsbG8='}) + expect(result.metadata.key).toEqual({id: 'aGVsbG8='}) + }) + + test('puts DynamoDB AttributeValue JSON without remarshalshalling typed attributes', async () => { + let captured: PutItemCommand | undefined + const client = fakeClient(async (command) => { + if (command instanceof DescribeTableCommand) return {Table: { + KeySchema: [{AttributeName: 'id', KeyType: 'HASH'}], + AttributeDefinitions: [{AttributeName: 'id', AttributeType: 'S'}], + }} + captured = command as PutItemCommand + return {} + }) + + await new AwsDynamoDbAdapter(client).items.putItem('orders', { + id: {S: 'order-1'}, count: {N: '9007199254740993'}, tags: {SS: ['new', 'priority']}, + payload: {B: 'aGVsbG8='}, details: {M: {active: {BOOL: true}}}, entries: {L: [{NULL: true}]}, + }) + + expect(captured?.input.Item).toEqual({ + id: {S: 'order-1'}, count: {N: '9007199254740993'}, tags: {SS: ['new', 'priority']}, + payload: {B: Buffer.from('hello')}, details: {M: {active: {BOOL: true}}}, entries: {L: [{NULL: true}]}, + }) }) test('validates required and typed keys before putting a record', async () => { @@ -327,10 +457,10 @@ describe('AwsDynamoDbAdapter', () => { }) const adapter = new AwsDynamoDbAdapter(client) - await expect(adapter.putNoSqlItem('orders', {})).rejects.toThrow('Key attribute id is required') - await expect(adapter.putNoSqlItem('orders', {id: 'not-a-number'})).rejects.toThrow('Key attribute id must be a number') + await expect(adapter.items.putItem('orders', {})).rejects.toThrow('Key attribute id is required') + await expect(adapter.items.putItem('orders', {id: 'not-a-number'})).rejects.toThrow('Key attribute id must be a number') for (const json of ['1', '1.5', '9007199254740993', '123456789.123456789', '1.0000000000000000001']) { - await expect(adapter.putNoSqlItem('orders', {id: JSON.parse(json)})) + await expect(adapter.items.putItem('orders', {id: JSON.parse(json)})) .rejects.toThrow('must be a quoted number to preserve precision') } expect(putCalls).toBe(0) @@ -351,12 +481,12 @@ describe('AwsDynamoDbAdapter', () => { }) const adapter = new AwsDynamoDbAdapter(client) - const created = await adapter.putNoSqlItem('orders', {id}) - const listed = await adapter.listNoSqlItems('orders') + const created = await adapter.items.putItem('orders', {id}) + const listed = await adapter.items.listItems('orders') expect(captured?.input.Item?.id).toEqual({N: id}) - expect(created).toEqual({id: JSON.stringify({id}), key: {id}, document: {id}}) - expect(listed).toEqual([created]) + expect(created).toEqual({id: JSON.stringify({id}), collectionId: null, timestamp: null, body: {id}, metadata: {key: {id}}}) + expect(listed.items).toEqual([created]) }) test.each([ @@ -377,7 +507,7 @@ describe('AwsDynamoDbAdapter', () => { return {} }) - await expect(new AwsDynamoDbAdapter(client).putNoSqlItem('orders', JSON.parse(json))) + await expect(new AwsDynamoDbAdapter(client).items.putItem('orders', JSON.parse(json))) .rejects.toThrow(new ValidationError(`Item value at ${path} must quote integers outside JavaScript's safe range.`)) expect(putCalls).toBe(0) }) @@ -421,12 +551,12 @@ describe('AwsDynamoDbAdapter', () => { }) const adapter = new AwsDynamoDbAdapter(client) - const created = await adapter.putNoSqlItem('orders', {accountId: 'acct-1', sequence: input}) - const listed = await adapter.listNoSqlItems('orders') - const key = {accountId: 'acct-1', sequence: normalized} + const created = await adapter.items.putItem('orders', {accountId: 'acct-1', sequence: input}) + const listed = await adapter.items.listItems('orders') + const key = {accountId: 'acct-1', sequence: String(normalized)} - expect(created).toEqual({id: JSON.stringify(key), key, document: key}) - expect(listed).toEqual([created]) + expect(created).toEqual({id: JSON.stringify(key), collectionId: null, timestamp: null, body: {accountId: 'acct-1', sequence: normalized}, metadata: {key}}) + expect(listed.items).toEqual([created]) }) test('preserves safe integers, fractions, and quoted large values in nested records', async () => { @@ -447,13 +577,13 @@ describe('AwsDynamoDbAdapter', () => { details: {count: '9007199254740993', active: true, optional: null}, } - const result = await new AwsDynamoDbAdapter(client).putNoSqlItem('orders', document) + const result = await new AwsDynamoDbAdapter(client).items.putItem('orders', document) expect(captured?.input.Item?.values).toEqual({L: [ {N: '-9007199254740991'}, {N: '9007199254740991'}, {N: '0'}, {N: '1.5'}, ]}) expect(captured?.input.Item?.details.M?.count).toEqual({S: '9007199254740993'}) - expect(result.document).toEqual(document) + expect(result.body).toEqual(document) }) test('returns the AWS DynamoDB schema', () => { diff --git a/packages/api/src/adapter-aws/AwsDynamoDbAdapter.ts b/packages/api/src/adapter-aws/AwsDynamoDbAdapter.ts index 6b9d26a..a5433a4 100644 --- a/packages/api/src/adapter-aws/AwsDynamoDbAdapter.ts +++ b/packages/api/src/adapter-aws/AwsDynamoDbAdapter.ts @@ -1,9 +1,11 @@ import { CreateTableCommand, + DeleteItemCommand, DeleteTableCommand, DescribeTableCommand, ListTablesCommand, PutItemCommand, + QueryCommand, ScanCommand, type AttributeDefinition, type AttributeValue, @@ -13,6 +15,7 @@ import { type TableDescription, } from '@aws-sdk/client-dynamodb' import {marshall, unmarshall} from '@aws-sdk/util-dynamodb' +import {DEFAULT_PAGE_LIMIT, type ChildItem, type CollectionPage, type ItemStoreAdapter, type PageQuery} from '../cloud-spi/childCollections' import {RuntimeError, ValidationError} from '../cloud-spi/errors' import {dynamodb as defaultDynamoDb} from '../aws' import {awsDynamoDbSchema} from '../cloud-spi/dynamodbSchema' @@ -20,19 +23,29 @@ import type { CloudResource, CloudServiceAdapter, CreateResourceInput, - NoSqlItem, ResourceQuery, ServiceSchema, } from '../cloud-spi/types' const DESCRIBE_BATCH_SIZE = 10 -const ITEM_SCAN_LIMIT = 100 +interface DynamoDbItemStore extends ItemStoreAdapter { + putItem(resourceId: string, body: Record): Promise + deleteItem(resourceId: string, itemId: string): Promise +} export class AwsDynamoDbAdapter implements CloudServiceAdapter { readonly cloud = 'aws' as const readonly service = 'nosql' as const + readonly items: DynamoDbItemStore - constructor(private readonly dynamodb: DynamoDBClient = defaultDynamoDb) {} + constructor(private readonly dynamodb: DynamoDBClient = defaultDynamoDb) { + this.items = { + listItems: (resourceId, page) => this.listItems(resourceId, page), + queryItems: (resourceId, query, page) => this.queryItems(resourceId, query, page), + putItem: (resourceId, document) => this.putItem(resourceId, document), + deleteItem: (resourceId, itemId) => this.deleteItem(resourceId, itemId), + } + } schema(): ServiceSchema { return awsDynamoDbSchema() @@ -104,31 +117,76 @@ export class AwsDynamoDbAdapter implements CloudServiceAdapter { await this.dynamodb.send(new DeleteTableCommand({TableName: id})) } - async listNoSqlItems(resourceId: string): Promise { - const table = await this.describeTable(resourceId) - const keyNames = keyAttributes(table).map(({name}) => name) - const items: NoSqlItem[] = [] - let exclusiveStartKey: Record | undefined - - do { - const response = await this.dynamodb.send(new ScanCommand({ - TableName: resourceId, - ExclusiveStartKey: exclusiveStartKey, - Limit: ITEM_SCAN_LIMIT - items.length, - })) - for (const rawItem of response.Items ?? []) { - const document = normalizeDocument(unmarshall(rawItem, {wrapNumbers: normalizeNumber})) - const key = Object.fromEntries(keyNames.map((name) => [name, document[name]])) - items.push({id: JSON.stringify(key), key, document}) - if (items.length === ITEM_SCAN_LIMIT) break - } - exclusiveStartKey = response.LastEvaluatedKey - } while (items.length < ITEM_SCAN_LIMIT && exclusiveStartKey && Object.keys(exclusiveStartKey).length > 0) - - return items + private async listItems(resourceId: string, page: PageQuery = {}): Promise> { + return this.scanItems(resourceId, {}, page) } - async putNoSqlItem(resourceId: string, document: Record): Promise { + private async queryItems(resourceId: string, rawQuery: string | Record, page: PageQuery = {}): Promise> { + if (!rawQuery || typeof rawQuery !== 'object' || Array.isArray(rawQuery)) { + throw new ValidationError('DynamoDB item query must be an object.') + } + const query = rawQuery as DynamoItemQuery + if (query.operation === 'scan') return this.scanItems(resourceId, query, page) + if (query.operation === 'query') return this.queryTableItems(resourceId, query, page) + throw new ValidationError('operation must be scan or query.') + } + + private async scanItems(resourceId: string, query: DynamoItemQuery, page: PageQuery): Promise> { + const table = await this.describeTable(resourceId) + const keyNames = keyAttributes(table).map(({name}) => name) + const indexName = optionalString(query.indexName) + indexKeyAttributes(table, indexName) + const filter = filterExpression(query.filter) + const response = await this.dynamodb.send(new ScanCommand({ + TableName: resourceId, + ...(indexName ? {IndexName: indexName} : {}), + ExclusiveStartKey: decodeCursor(page.cursor, resourceId, indexName), + Limit: page.limit ?? DEFAULT_PAGE_LIMIT, + ReturnConsumedCapacity: 'TOTAL', + ...filter, + })) + return itemPage(response, keyNames, resourceId, indexName) + } + + private async queryTableItems(resourceId: string, query: DynamoItemQuery, page: PageQuery): Promise> { + const table = await this.describeTable(resourceId) + const primaryKeyNames = keyAttributes(table).map(({name}) => name) + const indexName = optionalString(query.indexName) + const keys = indexKeyAttributes(table, indexName) + const partitionKey = keys.find(({keyType}) => keyType === 'HASH') + const sortKey = keys.find(({keyType}) => keyType === 'RANGE') + if (!partitionKey) throw new RuntimeError(`DynamoDB table ${resourceId} has no partition key`) + if (typeof query.partitionKeyValue !== 'string' || query.partitionKeyValue.length === 0) { + throw new ValidationError(`Partition key ${partitionKey.name} is required.`) + } + + const names: Record = {'#pk': partitionKey.name} + const values: Record = {':pk': marshalKey(partitionKey, query.partitionKeyValue)} + let keyConditionExpression = '#pk = :pk' + if (query.sortKeyCondition !== undefined) { + if (!sortKey) throw new ValidationError('The selected table or index does not have a sort key.') + const condition = keyCondition(query.sortKeyCondition, sortKey) + names['#sk'] = sortKey.name + Object.assign(values, condition.values) + keyConditionExpression += ` AND ${condition.expression}` + } + + const filter = filterExpression(query.filter, names, values) + const response = await this.dynamodb.send(new QueryCommand({ + TableName: resourceId, + ...(indexName ? {IndexName: indexName} : {}), + KeyConditionExpression: keyConditionExpression, + ExpressionAttributeNames: names, + ExpressionAttributeValues: values, + ExclusiveStartKey: decodeCursor(page.cursor, resourceId, indexName), + Limit: page.limit ?? DEFAULT_PAGE_LIMIT, + ReturnConsumedCapacity: 'TOTAL', + ...filter, + })) + return itemPage(response, primaryKeyNames, resourceId, indexName) + } + + private async putItem(resourceId: string, document: Record): Promise { if (!document || typeof document !== 'object' || Array.isArray(document)) { throw new ValidationError('Item must be a JSON object.') } @@ -139,12 +197,21 @@ export class AwsDynamoDbAdapter implements CloudServiceAdapter { const untypedKey = keys.find(({type}) => !type) if (untypedKey) throw new RuntimeError(`DynamoDB table ${resourceId} has no type definition for key ${untypedKey.name}`) - const item = marshalDocument(document, keys) + const item = attributeValueDocument(document) ?? marshalDocument(document, keys) + validateTypedKeys(item, keys) await this.dynamodb.send(new PutItemCommand({TableName: resourceId, Item: item})) const normalizedDocument = normalizeDocument(unmarshall(item, {wrapNumbers: normalizeNumber})) - const key = Object.fromEntries(keys.map(({name}) => [name, normalizedDocument[name]])) - return {id: JSON.stringify(key), key, document: normalizedDocument} + const key = itemKeyDocument(item, keys.map(({name}) => name)) + return toItem(JSON.stringify(key), key, normalizedDocument) + } + + private async deleteItem(resourceId: string, itemId: string): Promise { + const table = await this.describeTable(resourceId) + const keys = keyAttributes(table) + if (keys.length === 0) throw new RuntimeError(`DynamoDB table ${resourceId} has no key schema`) + const key = keyFromItemId(itemId, keys) + await this.dynamodb.send(new DeleteItemCommand({TableName: resourceId, Key: key})) } private async listTableNames(): Promise { @@ -173,9 +240,82 @@ export class AwsDynamoDbAdapter implements CloudServiceAdapter { } } +function toItem(id: string, key: Record, body: Record): ChildItem { + return {id, collectionId: null, timestamp: null, body, metadata: {key}} +} + +function keyFromItemId(itemId: string, keys: KeyAttribute[]): Record { + let key: unknown + try { + key = JSON.parse(itemId) + } catch { + throw new ValidationError('Item identifier must be a DynamoDB key JSON object.') + } + if (!key || typeof key !== 'object' || Array.isArray(key)) { + throw new ValidationError('Item identifier must be a DynamoDB key JSON object.') + } + const document = key as Record + return Object.fromEntries(keys.map((keyAttribute) => { + if (!Object.prototype.hasOwnProperty.call(document, keyAttribute.name)) { + throw new ValidationError(`Key attribute ${keyAttribute.name} is required.`) + } + return [keyAttribute.name, marshalKey(keyAttribute, document[keyAttribute.name])] + })) +} + +function attributeValueDocument(document: Record): Record | undefined { + const entries = Object.entries(document) + if (entries.length === 0 || !entries.every(([, value]) => isAttributeValueJson(value))) return undefined + return Object.fromEntries(entries.map(([name, value]) => [name, fromAttributeValueJson(value as Record)])) +} + +function isAttributeValueJson(value: unknown): value is Record { + if (!value || typeof value !== 'object' || Array.isArray(value)) return false + const entries = Object.entries(value) + if (entries.length !== 1) return false + const [type, raw] = entries[0] + if (type === 'S') return typeof raw === 'string' + if (type === 'N') return typeof raw === 'string' && isDynamoNumber(raw) + if (type === 'B') return typeof raw === 'string' && isBase64(raw) + if (type === 'BOOL') return typeof raw === 'boolean' + if (type === 'NULL') return raw === true + if (type === 'SS') return Array.isArray(raw) && raw.length > 0 && raw.every((item) => typeof item === 'string') + if (type === 'NS') return Array.isArray(raw) && raw.length > 0 && raw.every((item) => typeof item === 'string' && isDynamoNumber(item)) + if (type === 'BS') return Array.isArray(raw) && raw.length > 0 && raw.every((item) => typeof item === 'string' && isBase64(item)) + if (type === 'L') return Array.isArray(raw) && raw.every(isAttributeValueJson) + if (type === 'M') return Boolean(raw) && typeof raw === 'object' && !Array.isArray(raw) && Object.values(raw).every(isAttributeValueJson) + return false +} + +function fromAttributeValueJson(value: Record): AttributeValue { + const [type, raw] = Object.entries(value)[0] + if (type === 'B') return {B: Buffer.from(raw as string, 'base64')} + if (type === 'BS') return {BS: (raw as string[]).map((item) => Buffer.from(item, 'base64'))} + if (type === 'L') return {L: (raw as Record[]).map(fromAttributeValueJson)} + if (type === 'M') return {M: Object.fromEntries(Object.entries(raw as Record>).map(([name, child]) => [name, fromAttributeValueJson(child)]))} + if (type === 'S') return {S: raw as string} + if (type === 'N') return {N: raw as string} + if (type === 'BOOL') return {BOOL: raw as boolean} + if (type === 'NULL') return {NULL: true} + if (type === 'SS') return {SS: raw as string[]} + if (type === 'NS') return {NS: raw as string[]} + throw new ValidationError('Item contains an invalid DynamoDB AttributeValue.') +} + +function validateTypedKeys(item: Record, keys: KeyAttribute[]): void { + for (const key of keys) { + if (!Object.prototype.hasOwnProperty.call(item, key.name)) throw new ValidationError(`Key attribute ${key.name} is required.`) + const value = item[key.name] + if (key.type === 'N' && typeof value.N !== 'string') throw new ValidationError(`Key attribute ${key.name} must be a number.`) + if (key.type === 'B' && !value.B) throw new ValidationError(`Binary key attribute ${key.name} must be a non-empty base64 string.`) + if (key.type === 'S' && !value.S) throw new ValidationError(`Key attribute ${key.name} must be a non-empty string.`) + } +} + interface KeyAttribute { name: string type?: ScalarAttributeType + keyType?: 'HASH' | 'RANGE' } function keyAttributes(table: TableDescription): KeyAttribute[] { @@ -186,7 +326,181 @@ function keyAttributes(table: TableDescription): KeyAttribute[] { ) return (table.KeySchema ?? []) .filter((key): key is KeySchemaElement & {AttributeName: string} => Boolean(key.AttributeName)) - .map((key) => ({name: key.AttributeName, type: attributeTypes.get(key.AttributeName)})) + .map((key) => ({name: key.AttributeName, type: attributeTypes.get(key.AttributeName), keyType: key.KeyType})) +} + +interface DynamoItemQuery { + operation?: unknown + indexName?: unknown + partitionKeyValue?: unknown + sortKeyCondition?: unknown + filter?: unknown +} + +interface DynamoCondition { + operator?: unknown + value?: unknown + secondValue?: unknown +} + +interface DynamoFilter extends DynamoCondition { + attribute?: unknown + type?: unknown +} + +const COMPARISON_OPERATORS = new Map([ + ['EQ', '='], ['NE', '<>'], ['LT', '<'], ['LE', '<='], ['GT', '>'], ['GE', '>='], +]) + +function indexKeyAttributes(table: TableDescription, indexName?: string): KeyAttribute[] { + if (!indexName) return keyAttributes(table) + const indexes = [...(table.GlobalSecondaryIndexes ?? []), ...(table.LocalSecondaryIndexes ?? [])] + const index = indexes.find((candidate) => candidate.IndexName === indexName) + if (!index) throw new ValidationError(`DynamoDB index ${indexName} does not exist.`) + return keyAttributes({...table, KeySchema: index.KeySchema}) +} + +function keyCondition(raw: unknown, sortKey: KeyAttribute): {expression: string; values: Record} { + if (!raw || typeof raw !== 'object' || Array.isArray(raw)) throw new ValidationError('sortKeyCondition must be an object.') + const condition = raw as DynamoCondition + const operator = typeof condition.operator === 'string' ? condition.operator : '' + const value = requiredConditionValue(condition.value, 'Sort key value') + const values: Record = {':sk': marshalKey(sortKey, value)} + const comparison = COMPARISON_OPERATORS.get(operator) + if (comparison) return {expression: `#sk ${comparison} :sk`, values} + if (operator === 'begins_with') return {expression: 'begins_with(#sk, :sk)', values} + if (operator === 'BETWEEN') { + const secondValue = requiredConditionValue(condition.secondValue, 'Second sort key value') + values[':sk2'] = marshalKey(sortKey, secondValue) + return {expression: '#sk BETWEEN :sk AND :sk2', values} + } + throw new ValidationError('Sort key operator must be EQ, LT, LE, GT, GE, BETWEEN, or begins_with.') +} + +function filterExpression( + raw: unknown, + names: Record = {}, + values: Record = {}, +): {FilterExpression?: string; ExpressionAttributeNames?: Record; ExpressionAttributeValues?: Record} { + if (raw === undefined || raw === null) return {} + if (typeof raw !== 'object' || Array.isArray(raw)) throw new ValidationError('filter must be an object.') + const filter = raw as DynamoFilter + const attribute = requiredConditionValue(filter.attribute, 'Filter attribute') + const operator = typeof filter.operator === 'string' ? filter.operator : '' + names['#filter'] = attribute + + if (operator === 'attribute_exists' || operator === 'attribute_not_exists') { + return {FilterExpression: `${operator}(#filter)`, ExpressionAttributeNames: names, ...(Object.keys(values).length ? {ExpressionAttributeValues: values} : {})} + } + + values[':filter'] = typedAttributeValue(filter.type, filter.value, 'Filter value') + const comparison = COMPARISON_OPERATORS.get(operator) + let expression: string + if (comparison) expression = `#filter ${comparison} :filter` + else if (operator === 'begins_with' || operator === 'contains') expression = `${operator}(#filter, :filter)` + else if (operator === 'BETWEEN') { + values[':filter2'] = typedAttributeValue(filter.type, filter.secondValue, 'Second filter value') + expression = '#filter BETWEEN :filter AND :filter2' + } else { + throw new ValidationError('Filter operator is not supported.') + } + return {FilterExpression: expression, ExpressionAttributeNames: names, ExpressionAttributeValues: values} +} + +function typedAttributeValue(type: unknown, raw: unknown, field: string): AttributeValue { + const value = requiredConditionValue(raw, field) + if (type === 'S') return {S: value} + if (type === 'N') { + if (!isDynamoNumber(value)) throw new ValidationError(`${field} must be a number.`) + return {N: value} + } + if (type === 'B') { + if (!isBase64(value)) throw new ValidationError(`${field} must be non-empty base64.`) + return {B: Buffer.from(value, 'base64')} + } + if (type === 'BOOL') { + if (value !== 'true' && value !== 'false') throw new ValidationError(`${field} must be true or false.`) + return {BOOL: value === 'true'} + } + throw new ValidationError('Filter type must be S, N, B, or BOOL.') +} + +function requiredConditionValue(value: unknown, field: string): string { + if (typeof value !== 'string' || value.length === 0) throw new ValidationError(`${field} is required.`) + return value +} + +function optionalString(value: unknown): string | undefined { + return typeof value === 'string' && value.length > 0 ? value : undefined +} + +function itemPage( + response: {Items?: Record[]; LastEvaluatedKey?: Record; Count?: number; ScannedCount?: number; ConsumedCapacity?: {CapacityUnits?: number}}, + keyNames: string[], + tableName: string, + indexName?: string, +): CollectionPage { + const items = (response.Items ?? []).map((rawItem) => { + const document = normalizeDocument(unmarshall(rawItem, {wrapNumbers: normalizeNumber})) + const key = itemKeyDocument(rawItem, keyNames) + return toItem(JSON.stringify(key), key, document) + }) + return { + items, + nextCursor: response.LastEvaluatedKey && Object.keys(response.LastEvaluatedKey).length + ? encodeCursor(response.LastEvaluatedKey, tableName, indexName) + : null, + count: response.Count ?? items.length, + ...(response.ScannedCount === undefined ? {} : {scannedCount: response.ScannedCount}), + ...(response.ConsumedCapacity?.CapacityUnits === undefined ? {} : {consumedCapacity: response.ConsumedCapacity.CapacityUnits}), + } +} + +function itemKeyDocument(item: Record, keyNames: string[]): Record { + return Object.fromEntries(keyNames.map((name) => { + const value = item[name] + if (!value) throw new RuntimeError(`DynamoDB item is missing key attribute ${name}`) + if (value.S !== undefined) return [name, value.S] + if (value.N !== undefined) return [name, String(normalizeNumber(value.N))] + if (value.B !== undefined) return [name, Buffer.from(value.B).toString('base64')] + throw new RuntimeError(`DynamoDB key attribute ${name} has an unsupported type`) + })) +} + +function encodeCursor(key: Record, tableName: string, indexName?: string): string { + const payload = {v: 1, tableName, indexName: indexName ?? null, key: Object.fromEntries(Object.entries(key).map(([name, value]) => [name, toAttributeValueJson(value)]))} + return Buffer.from(JSON.stringify(payload)).toString('base64url') +} + +function decodeCursor(cursor: string | undefined, tableName: string, indexName?: string): Record | undefined { + if (!cursor) return undefined + try { + const decoded = Buffer.from(cursor, 'base64url') + if (decoded.toString('base64url') !== cursor) throw new Error('non-canonical cursor') + const payload = JSON.parse(decoded.toString()) as {v?: unknown; tableName?: unknown; indexName?: unknown; key?: unknown} + if (payload.v !== 1 || payload.tableName !== tableName || payload.indexName !== (indexName ?? null) || !payload.key || typeof payload.key !== 'object' || Array.isArray(payload.key)) { + throw new Error('cursor context mismatch') + } + const entries = Object.entries(payload.key) + if (!entries.length || !entries.every(([, value]) => isAttributeValueJson(value))) throw new Error('invalid cursor key') + return Object.fromEntries(entries.map(([name, value]) => [name, fromAttributeValueJson(value as Record)])) + } catch (error) { + throw new ValidationError('cursor is not valid for this DynamoDB table and index.', {cause: error}) + } +} + +function toAttributeValueJson(value: AttributeValue): Record { + if ('B' in value && value.B !== undefined) return {B: Buffer.from(value.B).toString('base64')} + if ('BS' in value && value.BS !== undefined) return {BS: value.BS.map((item) => Buffer.from(item).toString('base64'))} + if ('L' in value && value.L !== undefined) return {L: value.L.map(toAttributeValueJson)} + if ('M' in value && value.M !== undefined) return {M: Object.fromEntries(Object.entries(value.M).map(([name, child]) => [name, toAttributeValueJson(child)]))} + if ('S' in value) return {S: value.S} + if ('N' in value) return {N: value.N} + if ('BOOL' in value) return {BOOL: value.BOOL} + if ('NULL' in value) return {NULL: value.NULL} + if ('SS' in value) return {SS: value.SS} + if ('NS' in value) return {NS: value.NS} + throw new ValidationError('DynamoDB returned an unsupported pagination key.') } function marshalDocument(document: Record, keys: KeyAttribute[]): Record { diff --git a/packages/api/src/adapter-aws/AwsEventBridgeAdapter.test.ts b/packages/api/src/adapter-aws/AwsEventBridgeAdapter.test.ts index 43c3a9b..8e8db9c 100644 --- a/packages/api/src/adapter-aws/AwsEventBridgeAdapter.test.ts +++ b/packages/api/src/adapter-aws/AwsEventBridgeAdapter.test.ts @@ -8,6 +8,7 @@ import { ListRulesCommand, ListTargetsByRuleCommand, PutRuleCommand, + PutEventsCommand, PutTargetsCommand, RemoveTargetsCommand, type EventBridgeClient, @@ -120,6 +121,16 @@ describe('AwsEventBridgeAdapter', () => { expect((commands[2] as AddPermissionCommand).input).toMatchObject({FunctionName: 'arn:aws:lambda:us-east-1:000000000000:function:worker', Principal: 'events.amazonaws.com', SourceArn: 'arn:aws:events:us-east-1:000000000000:rule/orders/hourly'}) }) + test('publishes a validated event to the selected bus', async () => { + const adapter = createAdapter(async (command) => { + expect(command).toBeInstanceOf(PutEventsCommand) + expect((command as PutEventsCommand).input).toEqual({Entries: [{EventBusName: 'orders', Source: 'orders', DetailType: 'order.created', Detail: '{"id":"123"}'}]}) + return {Entries: [{EventId: 'event-123'}]} + }) + + await expect(adapter.putEventBridgeEvent('event-bus:orders', {source: 'orders', detailType: 'order.created', detail: '{"id":"123"}'})).resolves.toEqual({eventId: 'event-123', errorCode: null, errorMessage: null}) + }) + test('removes targets before deleting a rule', async () => { const commands: unknown[] = [] const adapter = createAdapter(async (command) => { diff --git a/packages/api/src/adapter-aws/AwsEventBridgeAdapter.ts b/packages/api/src/adapter-aws/AwsEventBridgeAdapter.ts index 088761c..f67c396 100644 --- a/packages/api/src/adapter-aws/AwsEventBridgeAdapter.ts +++ b/packages/api/src/adapter-aws/AwsEventBridgeAdapter.ts @@ -7,6 +7,7 @@ import { ListRulesCommand, ListTargetsByRuleCommand, PutRuleCommand, + PutEventsCommand, PutTargetsCommand, RemoveTargetsCommand, type EventBridgeClient, @@ -25,6 +26,8 @@ import type { EventBridgeRuleInput, EventBridgeRuleOverview, EventBridgeRuleTarget, + EventBridgePutEventInput, + EventBridgePutEventResult, ResourceQuery, ServiceSchema, } from '../cloud-spi/types' @@ -152,6 +155,24 @@ export class AwsEventBridgeAdapter implements CloudServiceAdapter { await this.eventbridge.send(new DeleteRuleCommand({Name: name, ...eventBusInput(busName)})) } + async putEventBridgeEvent(eventBusId: string, input: EventBridgePutEventInput): Promise { + const source = requiredValue(input.source, 'source') + const detailType = requiredValue(input.detailType, 'detailType') + const detail = requiredValue(input.detail, 'detail') + try { + const parsed: unknown = JSON.parse(detail) + if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error('not an object') + } catch { + throw new ValidationError('detail must be a JSON object') + } + const response = await this.eventbridge.send(new PutEventsCommand({Entries: [{ + EventBusName: eventBusName(eventBusId), Source: source, DetailType: detailType, Detail: detail, + ...(input.resources?.length ? {Resources: input.resources} : {}), + }]})) + const entry = response.Entries?.[0] + return {eventId: entry?.EventId ?? null, errorCode: entry?.ErrorCode ?? null, errorMessage: entry?.ErrorMessage ?? null} + } + private async listTargets(ruleName: string, busName: string): Promise { const targets: Target[] = [] let nextToken: string | undefined diff --git a/packages/api/src/adapter-aws/AwsLogsAdapter.test.ts b/packages/api/src/adapter-aws/AwsLogsAdapter.test.ts index 38c22a1..5b4ce19 100644 --- a/packages/api/src/adapter-aws/AwsLogsAdapter.test.ts +++ b/packages/api/src/adapter-aws/AwsLogsAdapter.test.ts @@ -215,6 +215,42 @@ describe('AwsLogsAdapter queryLogs', () => { }) }) +describe('AwsLogsAdapter filterLogEvents', () => { + test('filters across streams and preserves event identity and pagination', async () => { + const {client, calls} = stubClient({ + FilterLogEventsCommand: () => ({ + events: [{eventId: 'event-1', logStreamName: 'stream-a', timestamp: 1785260000000, message: 'ERROR failed', ingestionTime: 1785260001000}], + nextToken: 'page-2', + }), + }) + + const page = await new AwsLogsAdapter(client).filterLogEvents('/floci/probe', { + filterPattern: 'ERROR', + logStreamNames: ['stream-a'], + startTime: 1000, + endTime: 2000, + }, {cursor: 'page-1', limit: 25}) + + expect(calls[0]).toMatchObject({command: 'FilterLogEventsCommand', input: { + logGroupName: '/floci/probe', filterPattern: 'ERROR', logStreamNames: ['stream-a'], + startTime: 1000, endTime: 2000, nextToken: 'page-1', limit: 25, + }}) + expect(page).toEqual({ + items: [{ + id: 'event-1', collectionId: 'stream-a', timestamp: new Date(1785260000000).toISOString(), + body: {message: 'ERROR failed'}, metadata: {ingestionTime: new Date(1785260001000).toISOString()}, + }], + nextCursor: 'page-2', + }) + }) + + test('retains a continuation token on an empty page', async () => { + const {client} = stubClient({FilterLogEventsCommand: () => ({events: [], nextToken: 'keep-going'})}) + await expect(new AwsLogsAdapter(client).filterLogEvents('/floci/probe', {})) + .resolves.toEqual({items: [], nextCursor: 'keep-going'}) + }) +}) + describe('AwsLogsAdapter documents', () => { test('lists log streams as collections, newest activity first', async () => { const {client, calls} = stubClient({ diff --git a/packages/api/src/adapter-aws/AwsLogsAdapter.ts b/packages/api/src/adapter-aws/AwsLogsAdapter.ts index a2402a4..a396bdd 100644 --- a/packages/api/src/adapter-aws/AwsLogsAdapter.ts +++ b/packages/api/src/adapter-aws/AwsLogsAdapter.ts @@ -6,6 +6,7 @@ import { DeleteLogStreamCommand, DescribeLogGroupsCommand, DescribeLogStreamsCommand, + FilterLogEventsCommand, GetLogEventsCommand, GetQueryResultsCommand, StartQueryCommand, @@ -25,6 +26,7 @@ import type { CreateResourceInput, LogsInsightsQueryInput, LogsInsightsQueryResult, + LogsFilterInput, ResourceQuery, ServiceSchema, } from '../cloud-spi/types' @@ -150,6 +152,30 @@ export class AwsLogsAdapter implements CloudServiceAdapter { } } + async filterLogEvents(logGroupName: string, input: LogsFilterInput, page: PageQuery = {}): Promise> { + const response = await this.client.send(new FilterLogEventsCommand({ + logGroupName, + filterPattern: input.filterPattern || undefined, + logStreamNames: input.logStreamNames?.length ? input.logStreamNames : undefined, + startTime: input.startTime, + endTime: input.endTime, + limit: page.limit, + nextToken: page.cursor, + })) + return { + items: (response.events ?? []).map((event, index) => ({ + id: event.eventId ?? `${event.logStreamName ?? ''}-${event.timestamp ?? 0}-${index}`, + collectionId: event.logStreamName ?? null, + timestamp: event.timestamp ? new Date(event.timestamp).toISOString() : null, + body: {message: event.message ?? ''}, + metadata: { + ingestionTime: event.ingestionTime ? new Date(event.ingestionTime).toISOString() : null, + }, + })), + nextCursor: response.nextToken ?? null, + } + } + /** * DescribeLogGroups filters by prefix, so `/floci/probe` also returns * `/floci/probe-two`. Filtering to an exact match keeps `get` honest. diff --git a/packages/api/src/adapter-aws/AwsSqsAdapter.ts b/packages/api/src/adapter-aws/AwsSqsAdapter.ts index 50bd1e0..165e5ad 100644 --- a/packages/api/src/adapter-aws/AwsSqsAdapter.ts +++ b/packages/api/src/adapter-aws/AwsSqsAdapter.ts @@ -1,9 +1,15 @@ import { CreateQueueCommand, + DeleteMessageCommand, DeleteQueueCommand, GetQueueAttributesCommand, GetQueueUrlCommand, ListQueuesCommand, + PurgeQueueCommand, + ReceiveMessageCommand, + SendMessageCommand, + SetQueueAttributesCommand, + StartMessageMoveTaskCommand, type SQSClient, } from '@aws-sdk/client-sqs' import {NotFoundError, ValidationError} from '../cloud-spi/errors' @@ -15,6 +21,12 @@ import type { CreateResourceInput, ResourceQuery, ServiceSchema, + SqsMessage, + SqsMoveTaskInput, + SqsMoveTaskResult, + SqsReceiveMessagesInput, + SqsSendMessageInput, + UpdateResourceInput, } from '../cloud-spi/types' /** @@ -38,6 +50,7 @@ interface QueueAttributes { CreatedTimestamp?: string LastModifiedTimestamp?: string FifoQueue?: string + RedrivePolicy?: string } export class AwsSqsAdapter implements CloudServiceAdapter { @@ -111,6 +124,69 @@ export class AwsSqsAdapter implements CloudServiceAdapter { await this.sqs.send(new DeleteQueueCommand({QueueUrl: url})) } + async update(id: string, input: UpdateResourceInput): Promise { + const url = await this.requireQueueUrl(id) + const attributes: Record = {} + const visibility = numericAttribute(input.values.visibilityTimeout, 'visibilityTimeout', 0, 43_200) + const retention = numericAttribute(input.values.messageRetentionPeriod, 'messageRetentionPeriod', 60, 1_209_600) + if (visibility) attributes.VisibilityTimeout = visibility + if (retention) attributes.MessageRetentionPeriod = retention + const redrive = input.values.redrivePolicy + if (redrive !== undefined) { + if (!redrive || typeof redrive !== 'object' || Array.isArray(redrive)) throw new ValidationError('redrivePolicy must be an object.') + const policy = redrive as {deadLetterTargetArn?: unknown; maxReceiveCount?: unknown} + if (typeof policy.deadLetterTargetArn !== 'string' || !policy.deadLetterTargetArn) throw new ValidationError('A dead-letter queue is required.') + const count = numericAttribute(policy.maxReceiveCount, 'maxReceiveCount', 1, 1_000) + if (!count) throw new ValidationError('maxReceiveCount is required.') + attributes.RedrivePolicy = JSON.stringify({deadLetterTargetArn: policy.deadLetterTargetArn, maxReceiveCount: count}) + } + if (Object.keys(attributes).length === 0) throw new ValidationError('No SQS queue settings were supplied.') + await this.sqs.send(new SetQueueAttributesCommand({QueueUrl: url, Attributes: attributes})) + return (await this.get(id))! + } + + async sendSqsMessage(queueId: string, input: SqsSendMessageInput): Promise<{messageId: string}> { + const url = await this.requireQueueUrl(queueId) + if (!input.messageBody) throw new ValidationError('messageBody is required.') + const queue = await this.get(queueId) + if (queue?.type === 'fifo-queue' && !input.messageGroupId) throw new ValidationError('messageGroupId is required for FIFO queues.') + const messageAttributes = Object.fromEntries(Object.entries(input.messageAttributes ?? {}).map(([name, value]) => [name, { + DataType: value.dataType, + ...(value.stringValue !== undefined ? {StringValue: value.stringValue} : {}), + ...(value.binaryValue !== undefined ? {BinaryValue: Buffer.from(value.binaryValue, 'base64')} : {}), + }])) + const response = await this.sqs.send(new SendMessageCommand({QueueUrl: url, MessageBody: input.messageBody, MessageAttributes: messageAttributes, MessageGroupId: input.messageGroupId, MessageDeduplicationId: input.messageDeduplicationId})) + return {messageId: response.MessageId ?? ''} + } + + async receiveSqsMessages(queueId: string, input: SqsReceiveMessagesInput): Promise { + const url = await this.requireQueueUrl(queueId) + const max = input.maxNumberOfMessages ?? 1 + if (!Number.isInteger(max) || max < 1 || max > 10) throw new ValidationError('maxNumberOfMessages must be between 1 and 10.') + const response = await this.sqs.send(new ReceiveMessageCommand({QueueUrl: url, MaxNumberOfMessages: max, MessageSystemAttributeNames: ['All'], MessageAttributeNames: ['All']})) + return (response.Messages ?? []).map((message) => ({ + messageId: message.MessageId ?? '', receiptHandle: message.ReceiptHandle ?? '', body: message.Body ?? '', + attributes: message.Attributes ?? {}, + messageAttributes: Object.fromEntries(Object.entries(message.MessageAttributes ?? {}).map(([name, value]) => [name, {dataType: value.DataType ?? '', stringValue: value.StringValue, binaryValue: value.BinaryValue ? Buffer.from(value.BinaryValue).toString('base64') : undefined}])), + })) + } + + async deleteSqsMessage(queueId: string, receiptHandle: string): Promise { + await this.sqs.send(new DeleteMessageCommand({QueueUrl: await this.requireQueueUrl(queueId), ReceiptHandle: receiptHandle})) + } + + async purgeSqsQueue(queueId: string): Promise { + await this.sqs.send(new PurgeQueueCommand({QueueUrl: await this.requireQueueUrl(queueId)})) + } + + async startSqsMoveTask(queueId: string, input: SqsMoveTaskInput): Promise { + const queue = await this.get(queueId) + const sourceArn = typeof queue?.metadata.arn === 'string' ? queue.metadata.arn : undefined + if (!sourceArn) throw new NotFoundError(`Queue ${queueId} does not exist`) + const response = await this.sqs.send(new StartMessageMoveTaskCommand({SourceArn: sourceArn, DestinationArn: input.destinationArn})) + return {taskHandle: response.TaskHandle ?? ''} + } + /** Listing queue names is cheaper than describing each one. */ async health(): Promise { await this.sqs.send(new ListQueuesCommand({})) @@ -126,6 +202,12 @@ export class AwsSqsAdapter implements CloudServiceAdapter { } } + private async requireQueueUrl(name: string): Promise { + const url = await this.queueUrl(name) + if (!url) throw new NotFoundError(`Queue ${name} does not exist`) + return url + } + private async attributesFor(url: string): Promise { if (!url) return {} const res = await this.sqs.send( @@ -157,10 +239,21 @@ function toResource(name: string, url: string, attributes: QueueAttributes): Clo maximumMessageSize: numberValue(attributes.MaximumMessageSize), delaySeconds: numberValue(attributes.DelaySeconds), lastModified: epochSecondsToIso(attributes.LastModifiedTimestamp), + redrivePolicy: parseRedrivePolicy(attributes.RedrivePolicy), }, } } +function parseRedrivePolicy(value: string | undefined): {deadLetterTargetArn?: string; maxReceiveCount?: number} | null { + if (!value) return null + try { + const policy = JSON.parse(value) as {deadLetterTargetArn?: unknown; maxReceiveCount?: unknown} + return {deadLetterTargetArn: typeof policy.deadLetterTargetArn === 'string' ? policy.deadLetterTargetArn : undefined, maxReceiveCount: typeof policy.maxReceiveCount === 'string' ? Number(policy.maxReceiveCount) : undefined} + } catch { + return null + } +} + /** SQS reports timestamps as epoch seconds; the contract uses ISO strings. */ function epochSecondsToIso(value: string | undefined): string | null { if (!value) return null diff --git a/packages/api/src/cloud-spi/childCollections.ts b/packages/api/src/cloud-spi/childCollections.ts index da61d65..13b4270 100644 --- a/packages/api/src/cloud-spi/childCollections.ts +++ b/packages/api/src/cloud-spi/childCollections.ts @@ -23,6 +23,12 @@ export interface CollectionPage { * round-trip a cursor to a different resource or collection. */ nextCursor: string | null + /** Provider-reported result count for this page. */ + count?: number + /** Provider-reported number of records examined before filtering. */ + scannedCount?: number + /** Provider-reported capacity consumed by this request. */ + consumedCapacity?: number } export interface PageQuery { @@ -30,6 +36,8 @@ export interface PageQuery { limit?: number } +export type ItemQuery = string | Record + /** The middle level: a log stream, a Cosmos container, a BigQuery table. */ export interface ChildCollection { id: string @@ -61,7 +69,7 @@ export interface DocumentStoreAdapter { getItem?(resourceId: string, collectionId: string, itemId: string): Promise putItem?(resourceId: string, collectionId: string, body: Record): Promise deleteItem?(resourceId: string, collectionId: string, itemId: string, partitionKey?: string | null): Promise - queryItems?(resourceId: string, collectionId: string, query: string): Promise> + queryItems?(resourceId: string, collectionId: string, query: ItemQuery, page?: PageQuery): Promise> } /** Flat store: the resource is the collection. */ @@ -70,7 +78,7 @@ export interface ItemStoreAdapter { getItem?(resourceId: string, itemId: string): Promise putItem?(resourceId: string, body: Record): Promise deleteItem?(resourceId: string, itemId: string, partitionKey?: string | null): Promise - queryItems?(resourceId: string, query: string): Promise> + queryItems?(resourceId: string, query: ItemQuery, page?: PageQuery): Promise> } /** diff --git a/packages/api/src/cloud-spi/cloudformationSchema.ts b/packages/api/src/cloud-spi/cloudformationSchema.ts index ee65391..b11929e 100644 --- a/packages/api/src/cloud-spi/cloudformationSchema.ts +++ b/packages/api/src/cloud-spi/cloudformationSchema.ts @@ -13,6 +13,7 @@ const cloudformationFilters: FieldSchema[] = [ const cloudformationResourceActions: CapabilitySchema[] = [ {name: 'list', label: 'List stacks', enabled: true, status: 'available', runtimeRequired: true}, {name: 'create', label: 'Create stack', enabled: true, status: 'available', runtimeRequired: true}, + {name: 'update', label: 'Update stack', enabled: true, status: 'available', runtimeRequired: true}, {name: 'delete', label: 'Delete stack', enabled: true, status: 'available', runtimeRequired: true}, {name: 'inspect', label: 'Inspect stack', enabled: true, status: 'available', runtimeRequired: false}, ] @@ -38,18 +39,19 @@ export function awsCloudformationSchema(): ServiceSchema { { name: 'templateBody', label: 'Template Body', - type: 'text', + type: 'textarea', required: true, span: true, - description: 'Single-line CloudFormation template JSON — the console form has no multi-line editor yet.', + description: 'CloudFormation JSON or YAML template.', }, ], - actions: ['list', 'create', 'delete', 'inspect'], + actions: ['list', 'create', 'update', 'delete', 'inspect'], capabilities: { resourceActions: cloudformationResourceActions, }, filters: cloudformationFilters, columns: cloudformationColumns, + updateFields: [{name: 'templateBody', label: 'Template Body', type: 'textarea', required: true, span: true, valuePath: 'metadata.templateBody'}], } } diff --git a/packages/api/src/cloud-spi/dynamodbSchema.ts b/packages/api/src/cloud-spi/dynamodbSchema.ts index 6297e1e..7872951 100644 --- a/packages/api/src/cloud-spi/dynamodbSchema.ts +++ b/packages/api/src/cloud-spi/dynamodbSchema.ts @@ -83,6 +83,12 @@ export function awsDynamoDbSchema(): ServiceSchema { {name: 'delete', label: 'Delete table', enabled: true, status: 'available', runtimeRequired: true}, {name: 'inspect', label: 'Inspect table', enabled: true, status: 'available', runtimeRequired: true}, ], + itemActions: [ + {name: 'list', label: 'List items', enabled: true, status: 'available', runtimeRequired: true}, + {name: 'query', label: 'Scan or query items', enabled: true, status: 'available', runtimeRequired: true}, + {name: 'put', label: 'Put item', enabled: true, status: 'available', runtimeRequired: true}, + {name: 'delete', label: 'Delete item', enabled: true, status: 'available', runtimeRequired: true}, + ], }, filters: dynamodbFilters, columns: dynamodbColumns, diff --git a/packages/api/src/cloud-spi/types.ts b/packages/api/src/cloud-spi/types.ts index 232f5b8..f415bff 100644 --- a/packages/api/src/cloud-spi/types.ts +++ b/packages/api/src/cloud-spi/types.ts @@ -2,10 +2,13 @@ // This type-only cycle with serviceCatalog.ts is erased at compile time. import type {CloudServiceType, ServiceGroup} from './serviceCatalog' import type { + ChildItem, CollectionActionName, + CollectionPage, DocumentStoreAdapter, ItemActionName, ItemStoreAdapter, + PageQuery, } from './childCollections' export type {CloudServiceType, ServiceGroup} @@ -321,10 +324,45 @@ export interface LogsInsightsQueryResult { rows: Array> } -export interface NoSqlItem { - id: string - key: Record - document: Record +/** Mirrors FilterLogEvents: epoch milliseconds and optional cross-stream filtering. */ +export interface LogsFilterInput { + filterPattern?: string + logStreamNames?: string[] + startTime?: number + endTime?: number +} + +export interface SqsMessageAttribute { + dataType: string + stringValue?: string + binaryValue?: string +} + +export interface SqsMessage { + messageId: string + receiptHandle: string + body: string + attributes: Record + messageAttributes: Record +} + +export interface SqsSendMessageInput { + messageBody: string + messageAttributes?: Record + messageGroupId?: string + messageDeduplicationId?: string +} + +export interface SqsReceiveMessagesInput { + maxNumberOfMessages?: number +} + +export interface SqsMoveTaskInput { + destinationArn?: string +} + +export interface SqsMoveTaskResult { + taskHandle: string } /** Provider-neutral representation of a managed Kubernetes node pool. */ @@ -476,6 +514,8 @@ export interface SnsSubscriptionOverview { export interface ApiGatewayLambdaRouteInput { path: string + parentResourceId?: string + pathPart?: string method: string stage: string functionName: string @@ -530,7 +570,16 @@ export interface ApiGatewayRouteOverview { integrationType: string | null } +export interface ApiGatewayResourceOverview { + resourceId: string + parentId: string | null + path: string + pathPart: string | null + methods: ApiGatewayRouteOverview[] +} + export interface ApiGatewayRoutesResult { + resources: ApiGatewayResourceOverview[] routes: ApiGatewayRouteOverview[] stages: string[] } @@ -612,6 +661,22 @@ export interface EventBridgeRuleOverview extends EventBridgeRuleInput { state: 'ENABLED' | 'DISABLED' } +export interface EventBridgePutEventInput { + source: string + detailType: string + detail: string + resources?: string[] +} + +export interface EventBridgePutEventResult { + eventId: string | null + errorCode: string | null + errorMessage: string | null +} + +export interface CloudFormationChangeSetInput { name: string; templateBody: string; parameters?: Record; capabilities?: string[] } +export interface CloudFormationChangeSet { name: string; id: string; status: string | null; executionStatus: string | null; changes: Array<{action: string | null; logicalResourceId: string | null; resourceType: string | null}> } + export interface SqsLambdaEventSourceMappingInput { functionName: string queueArn: string @@ -714,6 +779,12 @@ export interface CloudServiceAdapter { listEventBridgeRules?(eventBusId: string): Promise createEventBridgeRule?(eventBusId: string, input: EventBridgeRuleInput): Promise deleteEventBridgeRule?(eventBusId: string, ruleName: string): Promise + putEventBridgeEvent?(eventBusId: string, input: EventBridgePutEventInput): Promise + listCloudFormationChangeSets?(stackName: string): Promise + createCloudFormationChangeSet?(stackName: string, input: CloudFormationChangeSetInput): Promise + executeCloudFormationChangeSet?(stackName: string, changeSetName: string): Promise + deleteCloudFormationChangeSet?(stackName: string, changeSetName: string): Promise + updateCloudFormationTerminationProtection?(stackName: string, enabled: boolean): Promise createSqsLambdaEventSourceMapping?(input: SqsLambdaEventSourceMappingInput): Promise listSqsLambdaEventSourceMappings?(functionName: string): Promise deleteSqsLambdaEventSourceMapping?(functionName: string, mappingId: string): Promise @@ -751,8 +822,12 @@ export interface CloudServiceAdapter { listSqlTables?(serverId: string, connection: SqlConnectionInput): Promise querySql?(serverId: string, connection: SqlConnectionInput, query: string): Promise queryLogs?(logGroupName: string, input: LogsInsightsQueryInput): Promise - listNoSqlItems?(resourceId: string): Promise - putNoSqlItem?(resourceId: string, document: Record): Promise + filterLogEvents?(logGroupName: string, input: LogsFilterInput, page?: PageQuery): Promise> + sendSqsMessage?(queueId: string, input: SqsSendMessageInput): Promise<{messageId: string}> + receiveSqsMessages?(queueId: string, input: SqsReceiveMessagesInput): Promise + deleteSqsMessage?(queueId: string, receiptHandle: string): Promise + purgeSqsQueue?(queueId: string): Promise + startSqsMoveTask?(queueId: string, input: SqsMoveTaskInput): Promise listKubernetesNodegroups?(clusterId: string): Promise createKubernetesNodegroup?(clusterId: string, input: CreateKubernetesNodegroupInput): Promise deleteKubernetesNodegroup?(clusterId: string, nodegroupId: string): Promise diff --git a/packages/api/src/routes/clouds.test.ts b/packages/api/src/routes/clouds.test.ts index 24fa236..3087b27 100644 --- a/packages/api/src/routes/clouds.test.ts +++ b/packages/api/src/routes/clouds.test.ts @@ -35,7 +35,6 @@ import type { CreateResourceInput, DatabaseSnapshot, LogsInsightsQueryResult, - NoSqlItem, } from '../cloud-spi/types' import {CloudAdapterRegistry} from '../registry/CloudAdapterRegistry' import {CloudProxyService} from '../service/CloudProxyService' @@ -197,6 +196,44 @@ describe('API Gateway method test routes', () => { }) }) +describe('API Gateway resource routes', () => { + test('dispatches parent-scoped nested route creation and returns the resource tree', async () => { + const calls: unknown[] = [] + const tree = { + resources: [ + {resourceId: 'root-123', parentId: null, path: '/', pathPart: null, methods: []}, + {resourceId: 'orders-123', parentId: 'root-123', path: '/orders', pathPart: 'orders', methods: []}, + ], + routes: [], + stages: ['dev'], + } + const app = appWithRoutes([mockAdapter('aws', { + service: 'apigateway', + createApiGatewayLambdaRoute: async (apiId, input) => { + calls.push({apiId, input}) + return {resourceId: 'order-123', deploymentId: 'deployment-123', invokeUrl: '/orders/{id}'} + }, + listApiGatewayRoutes: async () => tree, + })]) + + const create = await app.request('/api/clouds/aws/services/apigateway/resources/api-123/routes', { + method: 'POST', headers: {'content-type': 'application/json'}, + body: JSON.stringify({ + path: '/orders/{id}', parentResourceId: 'orders-123', pathPart: '{id}', + method: 'PATCH', stage: 'dev', functionName: 'orders-handler', + }), + }) + const list = await app.request('/api/clouds/aws/services/apigateway/resources/api-123/routes') + + expect(create.status).toBe(201) + expect(await list.json()).toEqual(tree) + expect(calls).toEqual([{apiId: 'api-123', input: { + path: '/orders/{id}', parentResourceId: 'orders-123', pathPart: '{id}', + method: 'PATCH', stage: 'dev', functionName: 'orders-handler', + }}]) + }) +}) + describe('S3 website routes', () => { test('routes normalized website configuration lifecycle requests through the storage adapter', async () => { const calls: Array<{operation: string; args: unknown[]}> = [] @@ -579,46 +616,61 @@ describe('cloud schema routes', () => { expect(body[0].name).toBe('items') }) - test('lists DynamoDB records through the nosql adapter', async () => { + test('lists DynamoDB records through the generic item adapter', async () => { const app = appWithRoutes([mockAdapter('aws', { service: 'nosql', schema: awsDynamoDbSchema, - listNoSqlItems: async (resourceId: string): Promise => [{ - id: '{"pk":"item-1"}', - key: {pk: 'item-1'}, - document: {pk: 'item-1', name: 'First item', table: resourceId}, - }], + items: { + listItems: async (resourceId) => ({items: [{ + id: '{"pk":"item-1"}', collectionId: null, timestamp: null, + body: {pk: 'item-1', name: 'First item', table: resourceId}, metadata: {key: {pk: 'item-1'}}, + }], nextCursor: null}), + }, })]) const res = await app.request('/api/clouds/aws/services/nosql/resources/orders/items') const body = await res.json() expect(res.status).toBe(200) - expect(body[0].key).toEqual({pk: 'item-1'}) - expect(body[0].document.table).toBe('orders') + expect(body.items[0].metadata.key).toEqual({pk: 'item-1'}) + expect(body.items[0].body.table).toBe('orders') }) - test('puts a DynamoDB record through the nosql adapter', async () => { + test('puts and deletes DynamoDB records through the generic item adapter with the complete key', async () => { const calls: Array<{resourceId: string; document: Record}> = [] const app = appWithRoutes([mockAdapter('aws', { service: 'nosql', schema: awsDynamoDbSchema, - putNoSqlItem: async (resourceId, document): Promise => { - calls.push({resourceId, document}) - return {id: JSON.stringify({pk: document.pk}), key: {pk: document.pk}, document} + items: { + listItems: async () => ({items: [], nextCursor: null}), + putItem: async (resourceId, document) => { + calls.push({resourceId, document}) + return {id: JSON.stringify({pk: document.pk, sk: document.sk}), collectionId: null, timestamp: null, body: document, metadata: {key: {pk: document.pk, sk: document.sk}}} + }, + deleteItem: async (resourceId, itemId) => { + calls.push({resourceId, document: {itemId}}) + }, }, })]) const res = await app.request('/api/clouds/aws/services/nosql/resources/orders/items', { method: 'POST', headers: {'content-type': 'application/json'}, - body: JSON.stringify({pk: 'item-1', name: 'First item'}), + body: JSON.stringify({pk: 'item-1', sk: '2026-09-29', name: 'First item'}), }) const body = await res.json() expect(res.status).toBe(201) - expect(body.key).toEqual({pk: 'item-1'}) - expect(calls).toEqual([{resourceId: 'orders', document: {pk: 'item-1', name: 'First item'}}]) + expect(body.metadata.key).toEqual({pk: 'item-1', sk: '2026-09-29'}) + expect(calls).toEqual([{resourceId: 'orders', document: {pk: 'item-1', sk: '2026-09-29', name: 'First item'}}]) + + const deleteRes = await app.request('/api/clouds/aws/services/nosql/resources/orders/items/%7B%22pk%22%3A%22item-1%22%2C%22sk%22%3A%222026-09-29%22%7D', {method: 'DELETE'}) + + expect(deleteRes.status).toBe(200) + expect(calls).toEqual([ + {resourceId: 'orders', document: {pk: 'item-1', sk: '2026-09-29', name: 'First item'}}, + {resourceId: 'orders', document: {itemId: '{"pk":"item-1","sk":"2026-09-29"}'}}, + ]) }) test('clears the SES mailbox through the email adapter', async () => { @@ -804,6 +856,51 @@ describe('cloud schema routes', () => { expect(res.status).toBe(400) }) + test('filters log events with pagination through the route', async () => { + let received: Record = {} + const app = appWithRoutes([mockAdapter('aws', { + service: 'logs', + schema: awsLogsSchema, + filterLogEvents: async (logGroupName, input, page) => { + received = {logGroupName, input, page} + return {items: [{id: 'event-1', collectionId: 'stream-a', timestamp: null, body: {message: 'ERROR'}, metadata: {}}], nextCursor: 'next'} + }, + })]) + + const res = await app.request('/api/clouds/aws/services/logs/resources/%2Ffloci%2Fprobe/filter?cursor=previous&limit=25', { + method: 'POST', + body: JSON.stringify({filterPattern: 'ERROR', logStreamNames: ['stream-a'], startTime: 1000, endTime: 2000}), + }) + + expect(res.status).toBe(200) + expect(await res.json()).toMatchObject({nextCursor: 'next'}) + expect(received).toEqual({ + logGroupName: '/floci/probe', + input: {filterPattern: 'ERROR', logStreamNames: ['stream-a'], startTime: 1000, endTime: 2000}, + page: {cursor: 'previous', limit: 25}, + }) + }) + + test('rejects invalid log event filters before calling the adapter', async () => { + let called = false + const app = appWithRoutes([mockAdapter('aws', { + service: 'logs', + schema: awsLogsSchema, + filterLogEvents: async () => { + called = true + return {items: [], nextCursor: null} + }, + })]) + + const res = await app.request('/api/clouds/aws/services/logs/resources/%2Ffloci%2Fprobe/filter', { + method: 'POST', + body: JSON.stringify({logStreamNames: 'stream-a'}), + }) + + expect(res.status).toBe(400) + expect(called).toBe(false) + }) + test('normalizes runtime unavailable errors', async () => { const app = appWithRoutes([ mockAdapter('aws', { @@ -1621,6 +1718,30 @@ describe('child collection routes', () => { expect(await res.json()).toEqual({items: stubItems.items, nextCursor: null}) }) + test('passes a structured flat-item query and pagination through the existing query route', async () => { + let seen: unknown + const app = appWithRoutes([mockAdapter('aws', {items: { + ...itemsStub, + queryItems: async (resourceId, query, page) => { + seen = {resourceId, query, page} + return {...stubItems, count: 1, scannedCount: 4, consumedCapacity: 0.5} + }, + }})]) + const res = await app.request('/api/clouds/aws/services/storage/resources/table-1/query?cursor=opaque&limit=25', { + method: 'POST', + headers: {'content-type': 'application/json'}, + body: JSON.stringify({query: {operation: 'scan', filter: {attribute: 'status', operator: 'EQ', type: 'S', value: 'OPEN'}}}), + }) + + expect(res.status).toBe(200) + expect(seen).toEqual({ + resourceId: 'table-1', + query: {operation: 'scan', filter: {attribute: 'status', operator: 'EQ', type: 'S', value: 'OPEN'}}, + page: {cursor: 'opaque', limit: 25}, + }) + expect(await res.json()).toMatchObject({count: 1, scannedCount: 4, consumedCapacity: 0.5}) + }) + // A documents-only adapter has no flat shape, and vice versa. test('501s when the adapter has the other shape', async () => { const app = appWithRoutes([mockAdapter('aws', {documents: documentsStub})]) diff --git a/packages/api/src/routes/clouds.ts b/packages/api/src/routes/clouds.ts index d90bce4..f1e46c8 100644 --- a/packages/api/src/routes/clouds.ts +++ b/packages/api/src/routes/clouds.ts @@ -264,24 +264,79 @@ export function createCloudRoutes(injectedService?: CloudProxyService) { }) }) - app.get('/:cloud/services/nosql/resources/:id/items', async (c) => { + app.post('/:cloud/services/logs/resources/:id/filter', async (c) => { const cloud = c.req.param('cloud') as CloudProvider if (!isCloudProvider(cloud)) return c.json({error: 'Unknown cloud'}, 404) return withRuntime(c, async () => { - const items = await svc(c).listNoSqlItems(cloud, c.req.param('id')) - return c.json(items) + const body = await c.req.json<{filterPattern?: unknown; logStreamNames?: unknown; startTime?: unknown; endTime?: unknown}>() + if (body.filterPattern !== undefined && typeof body.filterPattern !== 'string') throw new ValidationError('filterPattern must be a string.') + if (body.logStreamNames !== undefined && (!Array.isArray(body.logStreamNames) || body.logStreamNames.some((name) => typeof name !== 'string'))) throw new ValidationError('logStreamNames must be an array of strings.') + if (body.startTime !== undefined && typeof body.startTime !== 'number') throw new ValidationError('startTime must be a number (epoch milliseconds).') + if (body.endTime !== undefined && typeof body.endTime !== 'number') throw new ValidationError('endTime must be a number (epoch milliseconds).') + if (typeof body.startTime === 'number' && typeof body.endTime === 'number' && body.startTime > body.endTime) throw new ValidationError('startTime must not be after endTime.') + + return c.json(await svc(c).filterLogEvents(cloud, c.req.param('id'), { + filterPattern: body.filterPattern, + logStreamNames: body.logStreamNames as string[] | undefined, + startTime: body.startTime, + endTime: body.endTime, + }, pageQuery(c))) }) }) - app.post('/:cloud/services/nosql/resources/:id/items', async (c) => { + app.post('/:cloud/services/messaging/resources/:id/messages', async (c) => { const cloud = c.req.param('cloud') as CloudProvider - if (!isCloudProvider(cloud)) return c.json({error: 'Unknown cloud'}, 404) - + if (cloud !== 'aws') return c.json({error: 'Unknown cloud or service'}, 404) return withRuntime(c, async () => { - const document = await c.req.json>() - const item = await svc(c).putNoSqlItem(cloud, c.req.param('id'), document) - return c.json(item, 201) + const body = await c.req.json<{messageBody?: unknown; messageAttributes?: unknown; messageGroupId?: unknown; messageDeduplicationId?: unknown}>() + if (typeof body.messageBody !== 'string') throw new ValidationError('messageBody must be a string.') + return c.json(await svc(c).sendSqsMessage(cloud, c.req.param('id'), { + messageBody: body.messageBody, + messageAttributes: body.messageAttributes as never, + messageGroupId: typeof body.messageGroupId === 'string' ? body.messageGroupId : undefined, + messageDeduplicationId: typeof body.messageDeduplicationId === 'string' ? body.messageDeduplicationId : undefined, + }), 201) + }) + }) + + app.post('/:cloud/services/messaging/resources/:id/messages/receive', async (c) => { + const cloud = c.req.param('cloud') as CloudProvider + if (cloud !== 'aws') return c.json({error: 'Unknown cloud or service'}, 404) + return withRuntime(c, async () => { + const body = await c.req.json<{maxNumberOfMessages?: unknown}>() + if (body.maxNumberOfMessages !== undefined && typeof body.maxNumberOfMessages !== 'number') throw new ValidationError('maxNumberOfMessages must be a number.') + return c.json(await svc(c).receiveSqsMessages(cloud, c.req.param('id'), {maxNumberOfMessages: body.maxNumberOfMessages})) + }) + }) + + app.post('/:cloud/services/messaging/resources/:id/messages/delete', async (c) => { + const cloud = c.req.param('cloud') as CloudProvider + if (cloud !== 'aws') return c.json({error: 'Unknown cloud or service'}, 404) + return withRuntime(c, async () => { + const body = await c.req.json<{receiptHandle?: unknown}>() + if (typeof body.receiptHandle !== 'string' || !body.receiptHandle) throw new ValidationError('receiptHandle is required.') + await svc(c).deleteSqsMessage(cloud, c.req.param('id'), body.receiptHandle) + return c.json({ok: true}) + }) + }) + + app.delete('/:cloud/services/messaging/resources/:id/messages', async (c) => { + const cloud = c.req.param('cloud') as CloudProvider + if (cloud !== 'aws') return c.json({error: 'Unknown cloud or service'}, 404) + return withRuntime(c, async () => { + await svc(c).purgeSqsQueue(cloud, c.req.param('id')) + return c.json({ok: true}) + }) + }) + + app.post('/:cloud/services/messaging/resources/:id/move-tasks', async (c) => { + const cloud = c.req.param('cloud') as CloudProvider + if (cloud !== 'aws') return c.json({error: 'Unknown cloud or service'}, 404) + return withRuntime(c, async () => { + const body = await c.req.json<{destinationArn?: unknown}>() + if (body.destinationArn !== undefined && typeof body.destinationArn !== 'string') throw new ValidationError('destinationArn must be a string.') + return c.json(await svc(c).startSqsMoveTask(cloud, c.req.param('id'), {destinationArn: body.destinationArn})) }) }) @@ -526,6 +581,8 @@ export function createCloudRoutes(injectedService?: CloudProxyService) { const input = await c.req.json>() const route = await svc(c).createApiGatewayLambdaRoute(cloud, c.req.param('id'), { path: typeof input.path === 'string' ? input.path : '', + parentResourceId: typeof input.parentResourceId === 'string' ? input.parentResourceId : undefined, + pathPart: typeof input.pathPart === 'string' ? input.pathPart : undefined, method: typeof input.method === 'string' ? input.method : '', stage: typeof input.stage === 'string' ? input.stage : '', functionName: typeof input.functionName === 'string' ? input.functionName : '', @@ -699,6 +756,45 @@ export function createCloudRoutes(injectedService?: CloudProxyService) { return withRuntime(c, async () => c.json(await svc(c).listEventBridgeRules(cloud, c.req.param('id')))) }) + app.post('/:cloud/services/events/resources/:id/events', async (c) => { + const cloud = c.req.param('cloud') as CloudProvider + if (cloud !== 'aws') return c.json({error: 'Unknown cloud or service'}, 404) + return withRuntime(c, async () => { + const input = await c.req.json>() + const result = await svc(c).putEventBridgeEvent(cloud, c.req.param('id'), { + source: typeof input.source === 'string' ? input.source : '', detailType: typeof input.detailType === 'string' ? input.detailType : '', detail: typeof input.detail === 'string' ? input.detail : '', + resources: Array.isArray(input.resources) ? input.resources.filter((resource): resource is string => typeof resource === 'string') : undefined, + }) + return c.json(result, result.errorCode ? 400 : 201) + }) + }) + + app.get('/:cloud/services/iac/resources/:id/change-sets', async (c) => { + const cloud = c.req.param('cloud') as CloudProvider + if (cloud !== 'aws') return c.json({error: 'Unknown cloud or service'}, 404) + return withRuntime(c, async () => c.json(await svc(c).listCloudFormationChangeSets(cloud, c.req.param('id')))) + }) + app.post('/:cloud/services/iac/resources/:id/change-sets', async (c) => { + const cloud = c.req.param('cloud') as CloudProvider + if (cloud !== 'aws') return c.json({error: 'Unknown cloud or service'}, 404) + return withRuntime(c, async () => { const input = await c.req.json<{name?: string; templateBody?: string; parameters?: Record; capabilities?: string[]}>(); return c.json(await svc(c).createCloudFormationChangeSet(cloud, c.req.param('id'), {name: input.name ?? '', templateBody: input.templateBody ?? '', parameters: input.parameters, capabilities: input.capabilities}), 201) }) + }) + app.post('/:cloud/services/iac/resources/:id/change-sets/:name/execute', async (c) => { + const cloud = c.req.param('cloud') as CloudProvider + if (cloud !== 'aws') return c.json({error: 'Unknown cloud or service'}, 404) + return withRuntime(c, async () => { await svc(c).executeCloudFormationChangeSet(cloud, c.req.param('id'), c.req.param('name')); return c.json({ok: true}) }) + }) + app.delete('/:cloud/services/iac/resources/:id/change-sets/:name', async (c) => { + const cloud = c.req.param('cloud') as CloudProvider + if (cloud !== 'aws') return c.json({error: 'Unknown cloud or service'}, 404) + return withRuntime(c, async () => { await svc(c).deleteCloudFormationChangeSet(cloud, c.req.param('id'), c.req.param('name')); return c.json({ok: true}) }) + }) + app.put('/:cloud/services/iac/resources/:id/termination-protection', async (c) => { + const cloud = c.req.param('cloud') as CloudProvider + if (cloud !== 'aws') return c.json({error: 'Unknown cloud or service'}, 404) + return withRuntime(c, async () => { const input = await c.req.json<{enabled?: boolean}>(); await svc(c).updateCloudFormationTerminationProtection(cloud, c.req.param('id'), input.enabled === true); return c.json({ok: true}) }) + }) + app.post('/:cloud/services/events/resources/:id/rules', async (c) => { const cloud = c.req.param('cloud') as CloudProvider if (cloud !== 'aws') return c.json({error: 'Unknown cloud or service'}, 404) @@ -863,8 +959,8 @@ export function createCloudRoutes(injectedService?: CloudProxyService) { if (!target) return c.json({error: 'Unknown cloud or service'}, 404) return withRuntime(c, async () => { - const body = await c.req.json<{query?: string}>() - const page = await svc(c).queryCollectionItems(target.cloud, target.service, c.req.param('id'), c.req.param('cid'), body.query ?? '') + const body = await c.req.json<{query?: string | Record}>() + const page = await svc(c).queryCollectionItems(target.cloud, target.service, c.req.param('id'), c.req.param('cid'), body.query ?? '', pageQuery(c)) return c.json(page) }) }) @@ -905,8 +1001,8 @@ export function createCloudRoutes(injectedService?: CloudProxyService) { if (!target) return c.json({error: 'Unknown cloud or service'}, 404) return withRuntime(c, async () => { - const body = await c.req.json<{query?: string}>() - const page = await svc(c).queryFlatItems(target.cloud, target.service, c.req.param('id'), body.query ?? '') + const body = await c.req.json<{query?: string | Record}>() + const page = await svc(c).queryFlatItems(target.cloud, target.service, c.req.param('id'), body.query ?? '', pageQuery(c)) return c.json(page) }) }) diff --git a/packages/api/src/service/CloudProxyService.ts b/packages/api/src/service/CloudProxyService.ts index 1aa0fd9..7ed4b84 100644 --- a/packages/api/src/service/CloudProxyService.ts +++ b/packages/api/src/service/CloudProxyService.ts @@ -37,18 +37,27 @@ import type { EventBridgeLambdaTriggerOverview, EventBridgeRuleInput, EventBridgeRuleOverview, + EventBridgePutEventInput, + EventBridgePutEventResult, + CloudFormationChangeSet, + CloudFormationChangeSetInput, SqsLambdaEventSourceMappingInput, SqsLambdaEventSourceMappingOverview, + SqsMessage, + SqsMoveTaskInput, + SqsMoveTaskResult, + SqsReceiveMessagesInput, + SqsSendMessageInput, DatabaseSnapshot, KubernetesFargateProfile, KubernetesNodegroup, LogsInsightsQueryInput, LogsInsightsQueryResult, + LogsFilterInput, KmsDecryptInput, KmsDecryptResult, KmsEncryptInput, KmsEncryptResult, - NoSqlItem, PublishNotificationInput, PublishNotificationResult, SnsSubscriptionInput, @@ -71,6 +80,7 @@ import type { CollectionPage, DocumentStoreAdapter, ItemStoreAdapter, + ItemQuery, PageQuery, } from '../cloud-spi/childCollections' import {NotSupportedError} from '../cloud-spi/errors' @@ -372,6 +382,42 @@ export class CloudProxyService { await adapter.deleteEventBridgeRule(eventBusId, ruleName) } + async putEventBridgeEvent(cloud: CloudProvider, eventBusId: string, input: EventBridgePutEventInput): Promise { + const adapter = this.requireAdapter(cloud, 'events') + if (!adapter.putEventBridgeEvent) throw new NotSupportedError(`Event publishing is not supported for ${cloud}`) + return adapter.putEventBridgeEvent(eventBusId, input) + } + + async listCloudFormationChangeSets(cloud: CloudProvider, stackName: string): Promise { + const adapter = this.requireAdapter(cloud, 'iac') + if (!adapter.listCloudFormationChangeSets) throw new NotSupportedError(`CloudFormation change sets are not supported for ${cloud}`) + return adapter.listCloudFormationChangeSets(stackName) + } + + async createCloudFormationChangeSet(cloud: CloudProvider, stackName: string, input: CloudFormationChangeSetInput): Promise { + const adapter = this.requireAdapter(cloud, 'iac') + if (!adapter.createCloudFormationChangeSet) throw new NotSupportedError(`CloudFormation change sets are not supported for ${cloud}`) + return adapter.createCloudFormationChangeSet(stackName, input) + } + + async executeCloudFormationChangeSet(cloud: CloudProvider, stackName: string, changeSetName: string): Promise { + const adapter = this.requireAdapter(cloud, 'iac') + if (!adapter.executeCloudFormationChangeSet) throw new NotSupportedError(`CloudFormation change sets are not supported for ${cloud}`) + await adapter.executeCloudFormationChangeSet(stackName, changeSetName) + } + + async deleteCloudFormationChangeSet(cloud: CloudProvider, stackName: string, changeSetName: string): Promise { + const adapter = this.requireAdapter(cloud, 'iac') + if (!adapter.deleteCloudFormationChangeSet) throw new NotSupportedError(`CloudFormation change sets are not supported for ${cloud}`) + await adapter.deleteCloudFormationChangeSet(stackName, changeSetName) + } + + async updateCloudFormationTerminationProtection(cloud: CloudProvider, stackName: string, enabled: boolean): Promise { + const adapter = this.requireAdapter(cloud, 'iac') + if (!adapter.updateCloudFormationTerminationProtection) throw new NotSupportedError(`CloudFormation termination protection is not supported for ${cloud}`) + await adapter.updateCloudFormationTerminationProtection(stackName, enabled) + } + async createSqsLambdaEventSourceMapping(cloud: CloudProvider, input: SqsLambdaEventSourceMappingInput): Promise { const adapter = this.requireAdapter(cloud, 'serverless') if (!adapter.createSqsLambdaEventSourceMapping) throw new NotSupportedError(`SQS Lambda event source mappings are not supported for ${cloud}`) @@ -390,6 +436,36 @@ export class CloudProxyService { await adapter.deleteSqsLambdaEventSourceMapping(functionName, mappingId) } + async sendSqsMessage(cloud: CloudProvider, queueId: string, input: SqsSendMessageInput): Promise<{messageId: string}> { + const adapter = this.requireAdapter(cloud, 'messaging') + if (!adapter.sendSqsMessage) throw new NotSupportedError(`SQS message sending is not supported for ${cloud}`) + return adapter.sendSqsMessage(queueId, input) + } + + async receiveSqsMessages(cloud: CloudProvider, queueId: string, input: SqsReceiveMessagesInput): Promise { + const adapter = this.requireAdapter(cloud, 'messaging') + if (!adapter.receiveSqsMessages) throw new NotSupportedError(`SQS message receiving is not supported for ${cloud}`) + return adapter.receiveSqsMessages(queueId, input) + } + + async deleteSqsMessage(cloud: CloudProvider, queueId: string, receiptHandle: string): Promise { + const adapter = this.requireAdapter(cloud, 'messaging') + if (!adapter.deleteSqsMessage) throw new NotSupportedError(`SQS message deletion is not supported for ${cloud}`) + await adapter.deleteSqsMessage(queueId, receiptHandle) + } + + async purgeSqsQueue(cloud: CloudProvider, queueId: string): Promise { + const adapter = this.requireAdapter(cloud, 'messaging') + if (!adapter.purgeSqsQueue) throw new NotSupportedError(`SQS queue purging is not supported for ${cloud}`) + await adapter.purgeSqsQueue(queueId) + } + + async startSqsMoveTask(cloud: CloudProvider, queueId: string, input: SqsMoveTaskInput): Promise { + const adapter = this.requireAdapter(cloud, 'messaging') + if (!adapter.startSqsMoveTask) throw new NotSupportedError(`SQS message moving is not supported for ${cloud}`) + return adapter.startSqsMoveTask(queueId, input) + } + async updateResource(cloud: CloudProvider, service: CloudServiceType, id: string, input: UpdateResourceInput): Promise { const adapter = this.requireAdapter(cloud, service) if (!adapter.update) throw new NotSupportedError(`Resource updates are not supported for ${cloud}/${service}`) @@ -517,10 +593,10 @@ export class CloudProxyService { await documents.deleteItem(resourceId, collectionId, itemId, partitionKey) } - async queryCollectionItems(cloud: CloudProvider, service: CloudServiceType, resourceId: string, collectionId: string, query: string): Promise> { + async queryCollectionItems(cloud: CloudProvider, service: CloudServiceType, resourceId: string, collectionId: string, query: ItemQuery, page: PageQuery): Promise> { const documents = this.requireDocuments(cloud, service) if (!documents.queryItems) throw new NotSupportedError(`Querying items is not supported for ${cloud}/${service}`) - return documents.queryItems(resourceId, collectionId, query) + return documents.queryItems(resourceId, collectionId, query, page) } async listFlatItems(cloud: CloudProvider, service: CloudServiceType, resourceId: string, page: PageQuery): Promise> { @@ -539,10 +615,10 @@ export class CloudProxyService { await items.deleteItem(resourceId, itemId, partitionKey) } - async queryFlatItems(cloud: CloudProvider, service: CloudServiceType, resourceId: string, query: string): Promise> { + async queryFlatItems(cloud: CloudProvider, service: CloudServiceType, resourceId: string, query: ItemQuery, page: PageQuery): Promise> { const items = this.requireItems(cloud, service) if (!items.queryItems) throw new NotSupportedError(`Querying items is not supported for ${cloud}/${service}`) - return items.queryItems(resourceId, query) + return items.queryItems(resourceId, query, page) } private requireDocuments(cloud: CloudProvider, service: CloudServiceType): DocumentStoreAdapter { @@ -641,16 +717,10 @@ export class CloudProxyService { return adapter.queryLogs(logGroupName, input) } - async listNoSqlItems(cloud: CloudProvider, resourceId: string): Promise { - const adapter = this.requireAdapter(cloud, 'nosql') - if (!adapter.listNoSqlItems) throw new NotSupportedError(`Item listing is not supported for ${cloud}/nosql`) - return adapter.listNoSqlItems(resourceId) - } - - async putNoSqlItem(cloud: CloudProvider, resourceId: string, document: Record): Promise { - const adapter = this.requireAdapter(cloud, 'nosql') - if (!adapter.putNoSqlItem) throw new NotSupportedError(`Item creation is not supported for ${cloud}/nosql`) - return adapter.putNoSqlItem(resourceId, document) + async filterLogEvents(cloud: CloudProvider, logGroupName: string, input: LogsFilterInput, page: PageQuery = {}): Promise> { + const adapter = this.requireAdapter(cloud, 'logs') + if (!adapter.filterLogEvents) throw new NotSupportedError(`Log event filtering is not supported for ${cloud}/logs`) + return adapter.filterLogEvents(logGroupName, input, page) } async listKubernetesNodegroups(cloud: CloudProvider, clusterId: string): Promise { diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 0f777d8..6469541 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -26,6 +26,7 @@ "react-dom": "^19.2.0", "react-router-dom": "^7.18.1", "tailwind-merge": "^3.6.0", + "yaml": "^2.9.1", "zustand": "^5.0.13" }, "devDependencies": { diff --git a/packages/frontend/src/App.tsx b/packages/frontend/src/App.tsx index d0a08ed..699c218 100644 --- a/packages/frontend/src/App.tsx +++ b/packages/frontend/src/App.tsx @@ -4,6 +4,7 @@ import {SecretsManagerPage} from '@/features/secretsmanager/SecretsManagerPage' import {CloudExplorerPage} from '@/pages/CloudExplorerPage' import {CloudConsoleHomePage} from '@/pages/CloudConsoleHomePage' import {DatabaseDataPage} from '@/pages/DatabaseDataPage' +import {DynamoDbItemEditorPage} from '@/pages/DynamoDbItemEditorPage' import {WorkflowStudioPage} from '@/pages/WorkflowStudioPage' export default function App() { @@ -18,6 +19,8 @@ export default function App() { }/> }/> }/> + }/> + }/> }/> }/> }/> diff --git a/packages/frontend/src/api/HttpClient.ts b/packages/frontend/src/api/HttpClient.ts index 529e96d..0dcc2a3 100644 --- a/packages/frontend/src/api/HttpClient.ts +++ b/packages/frontend/src/api/HttpClient.ts @@ -1,5 +1,3 @@ -import { emitApiRequest, type CloudProvider } from "@/telemetry"; - export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE"; export interface HttpClientConfig { @@ -9,11 +7,6 @@ export interface HttpClientConfig { timeout?: number; } -export interface RequestTelemetry { - provider?: CloudProvider; - service?: string; -} - export type EndpointRegistry = Map; export interface RequestOptions @@ -24,7 +17,6 @@ export interface RequestOptions cache?: RequestCache; timeout?: number; baseUrlOverride?: string; - telemetry?: RequestTelemetry; } export interface HttpResponse { @@ -127,7 +119,6 @@ export type EndpointDefinition = { method: HttpMethod; defaults?: Partial; baseUrlOverride?: string; - telemetry?: RequestTelemetry; }; export class HttpClient { @@ -159,7 +150,6 @@ export class HttpClient { endpoint: string, options: RequestOptions = {}, ): Promise> { - const started = performance.now(); const { params, body, @@ -167,11 +157,9 @@ export class HttpClient { timeout, cache, baseUrlOverride, - telemetry, ...restOptions } = options; let finalUrl = endpoint; - let statusCode = 0; try { const base = baseUrlOverride ?? this.config.baseUrl ?? ""; @@ -210,7 +198,6 @@ export class HttpClient { init, timeout ?? this.config.timeout, ); - statusCode = response.status; for (const interceptor of this.responseInterceptors) { response = await interceptor(response); @@ -228,18 +215,6 @@ export class HttpClient { }; } catch (error) { throw this.normalizeError(error, finalUrl, timeout ?? this.config.timeout); - } finally { - if (telemetry?.service && statusCode > 0) { - emitApiRequest({ - provider: telemetry.provider ?? "aws", - service: telemetry.service, - method, - path: endpoint, - statusCode, - latencyMs: Math.round(performance.now() - started), - timestamp: Date.now(), - }); - } } } @@ -262,10 +237,6 @@ export class HttpClient { ...endpoint.defaults, ...options, baseUrlOverride: endpoint.baseUrlOverride, - telemetry: { - ...endpoint.telemetry, - ...options.telemetry, - }, }); } diff --git a/packages/frontend/src/api/api.ts b/packages/frontend/src/api/api.ts index 3ce24d9..8a80703 100644 --- a/packages/frontend/src/api/api.ts +++ b/packages/frontend/src/api/api.ts @@ -46,7 +46,13 @@ export const apiEndpointKeys = { deleteEventBridgeLambdaTrigger: "clouds.services.resources.serverless.eventbridge-trigger.delete", createEventBridgeRule: "clouds.services.resources.events.rule.create", listEventBridgeRules: "clouds.services.resources.events.rules.list", - deleteEventBridgeRule: "clouds.services.resources.events.rule.delete", + deleteEventBridgeRule: "clouds.services.resources.events.rule.delete", + putEventBridgeEvent: "clouds.services.resources.events.event.put", + listCloudFormationChangeSets: "clouds.services.resources.iac.change-sets.list", + createCloudFormationChangeSet: "clouds.services.resources.iac.change-set.create", + executeCloudFormationChangeSet: "clouds.services.resources.iac.change-set.execute", + deleteCloudFormationChangeSet: "clouds.services.resources.iac.change-set.delete", + updateCloudFormationTerminationProtection: "clouds.services.resources.iac.termination-protection.update", createSqsLambdaEventSourceMapping: "clouds.services.resources.serverless.sqs-event-source-mapping.create", listSqsLambdaEventSourceMappings: "clouds.services.resources.serverless.sqs-event-source-mappings.list", deleteSqsLambdaEventSourceMapping: "clouds.services.resources.serverless.sqs-event-source-mapping.delete", @@ -79,10 +85,6 @@ export const apiEndpointKeys = { query: "clouds.services.nosql.cosmos.items.query", }, }, - items: { - list: "clouds.services.nosql.items.list", - put: "clouds.services.nosql.items.put", - }, }, database: { sql: { @@ -100,6 +102,11 @@ export const apiEndpointKeys = { }, logs: { query: "clouds.services.logs.query", + filter: "clouds.services.logs.filter", + }, + messaging: { + messages: {send: "clouds.services.messaging.messages.send", receive: "clouds.services.messaging.messages.receive", delete: "clouds.services.messaging.messages.delete", purge: "clouds.services.messaging.messages.purge"}, + moveTasks: "clouds.services.messaging.move-tasks", }, k8s: { nodegroups: { @@ -126,6 +133,7 @@ export const apiEndpointKeys = { }, flatItems: { list: "clouds.services.flatItems.list", + query: "clouds.services.flatItems.query", create: "clouds.services.flatItems.create", delete: "clouds.services.flatItems.delete", }, @@ -276,7 +284,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds", method: "GET", - telemetry: { provider: "system", service: "cloud-proxy" }, }, ], [ @@ -284,7 +291,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services", method: "GET", - telemetry: { service: "cloud-proxy" }, }, ], [ @@ -292,7 +298,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/status", method: "GET", - telemetry: { service: "cloud-proxy" }, }, ], [ @@ -300,7 +305,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/:service/status", method: "GET", - telemetry: { service: "cloud-proxy" }, }, ], [ @@ -308,7 +312,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/:service/schema", method: "GET", - telemetry: { service: "cloud-proxy" }, }, ], [ @@ -316,7 +319,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/:service/dependencies", method: "GET", - telemetry: { service: "cloud-proxy" }, }, ], [ @@ -324,7 +326,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/:service/resources", method: "GET", - telemetry: { service: "cloud-proxy" }, }, ], [ @@ -332,7 +333,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/:service/resources/:id/invoke", method: "POST", - telemetry: { service: "cloud-proxy" }, }, ], [ @@ -340,7 +340,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/apigateway/resources/:id/routes", method: "POST", - telemetry: { service: "apigateway" }, }, ], [ @@ -348,7 +347,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/notifications/resources/:id/publish", method: "POST", - telemetry: { service: "notifications" }, }, ], [ @@ -356,7 +354,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/notifications/resources/:id/subscriptions", method: "POST", - telemetry: { service: "notifications" }, }, ], [ @@ -364,7 +361,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/notifications/resources/:id/subscriptions", method: "GET", - telemetry: { service: "notifications" }, }, ], [ @@ -372,7 +368,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/notifications/resources/:id/subscriptions/:subscriptionArn", method: "DELETE", - telemetry: { service: "notifications" }, }, ], [ @@ -380,7 +375,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/apigateway/resources/:id/routes", method: "GET", - telemetry: { service: "apigateway" }, }, ], [ @@ -388,7 +382,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/apigateway/resources/:id/routes/:resourceId/:method", method: "DELETE", - telemetry: { service: "apigateway" }, }, ], [ @@ -396,7 +389,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/apigateway/resources/:id/routes/test", method: "POST", - telemetry: { service: "apigateway" }, }, ], [ @@ -404,7 +396,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/apigateway/resources/:id/methods/test", method: "POST", - telemetry: { service: "apigateway" }, }, ], [ @@ -412,7 +403,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/apigateway/resources/:id/cors", method: "POST", - telemetry: { service: "apigateway" }, }, ], [ @@ -420,7 +410,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/storage/resources/:id/lambda-triggers", method: "POST", - telemetry: { service: "storage" }, }, ], [ @@ -428,7 +417,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/storage/resources/:id/lambda-triggers", method: "GET", - telemetry: { service: "storage" }, }, ], [ @@ -436,7 +424,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/storage/resources/:id/lambda-triggers/:triggerId", method: "DELETE", - telemetry: { service: "storage" }, }, ], [ @@ -444,7 +431,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/storage/resources/:id/website", method: "GET", - telemetry: { service: "storage" }, }, ], [ @@ -452,7 +438,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/storage/resources/:id/website", method: "PUT", - telemetry: { service: "storage" }, }, ], [ @@ -460,7 +445,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/storage/resources/:id/website", method: "DELETE", - telemetry: { service: "storage" }, }, ], [ @@ -468,7 +452,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/serverless/resources/:id/eventbridge-triggers", method: "POST", - telemetry: { service: "serverless" }, }, ], [ @@ -476,7 +459,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/serverless/resources/:id/eventbridge-triggers", method: "GET", - telemetry: { service: "serverless" }, }, ], [ @@ -484,7 +466,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/serverless/resources/:id/eventbridge-triggers/:triggerId", method: "DELETE", - telemetry: { service: "serverless" }, }, ], [ @@ -492,7 +473,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/events/resources/:id/rules", method: "POST", - telemetry: { service: "events" }, }, ], [ @@ -500,7 +480,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/events/resources/:id/rules", method: "GET", - telemetry: { service: "events" }, }, ], [ @@ -508,15 +487,25 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/events/resources/:id/rules/:ruleName", method: "DELETE", - telemetry: { service: "events" }, }, ], + [ + apiEndpointKeys.clouds.resources.putEventBridgeEvent, + { + path: "/clouds/:cloud/services/events/resources/:id/events", + method: "POST", + }, + ], + [apiEndpointKeys.clouds.resources.listCloudFormationChangeSets, {path: "/clouds/:cloud/services/iac/resources/:id/change-sets", method: "GET"}], + [apiEndpointKeys.clouds.resources.createCloudFormationChangeSet, {path: "/clouds/:cloud/services/iac/resources/:id/change-sets", method: "POST"}], + [apiEndpointKeys.clouds.resources.executeCloudFormationChangeSet, {path: "/clouds/:cloud/services/iac/resources/:id/change-sets/:name/execute", method: "POST"}], + [apiEndpointKeys.clouds.resources.deleteCloudFormationChangeSet, {path: "/clouds/:cloud/services/iac/resources/:id/change-sets/:name", method: "DELETE"}], + [apiEndpointKeys.clouds.resources.updateCloudFormationTerminationProtection, {path: "/clouds/:cloud/services/iac/resources/:id/termination-protection", method: "PUT"}], [ apiEndpointKeys.clouds.resources.createSqsLambdaEventSourceMapping, { path: "/clouds/:cloud/services/serverless/resources/:id/sqs-event-source-mappings", method: "POST", - telemetry: { service: "serverless" }, }, ], [ @@ -524,7 +513,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/serverless/resources/:id/sqs-event-source-mappings", method: "GET", - telemetry: { service: "serverless" }, }, ], [ @@ -532,35 +520,33 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/serverless/resources/:id/sqs-event-source-mappings/:mappingId", method: "DELETE", - telemetry: { service: "serverless" }, }, ], [ apiEndpointKeys.clouds.resources.startWorkflowExecution, - { path: "/clouds/:cloud/services/workflows/resources/:id/executions", method: "POST", telemetry: { service: "workflows" } }, + { path: "/clouds/:cloud/services/workflows/resources/:id/executions", method: "POST" }, ], [ apiEndpointKeys.clouds.resources.listWorkflowExecutions, - { path: "/clouds/:cloud/services/workflows/resources/:id/executions", method: "GET", telemetry: { service: "workflows" } }, + { path: "/clouds/:cloud/services/workflows/resources/:id/executions", method: "GET" }, ], [ apiEndpointKeys.clouds.resources.describeWorkflowExecution, - { path: "/clouds/:cloud/services/workflows/resources/:id/executions/:executionArn", method: "GET", telemetry: { service: "workflows" } }, + { path: "/clouds/:cloud/services/workflows/resources/:id/executions/:executionArn", method: "GET" }, ], [ apiEndpointKeys.clouds.resources.getWorkflowExecutionHistory, - { path: "/clouds/:cloud/services/workflows/resources/:id/executions/:executionArn/history", method: "GET", telemetry: { service: "workflows" } }, + { path: "/clouds/:cloud/services/workflows/resources/:id/executions/:executionArn/history", method: "GET" }, ], [ apiEndpointKeys.clouds.resources.stopWorkflowExecution, - { path: "/clouds/:cloud/services/workflows/resources/:id/executions/:executionArn/stop", method: "POST", telemetry: { service: "workflows" } }, + { path: "/clouds/:cloud/services/workflows/resources/:id/executions/:executionArn/stop", method: "POST" }, ], [ apiEndpointKeys.clouds.resources.get, { path: "/clouds/:cloud/services/:service/resources/:id", method: "GET", - telemetry: { service: "cloud-proxy" }, }, ], [ @@ -568,7 +554,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/kms/resources/:id/encrypt", method: "POST", - telemetry: { service: "kms" }, }, ], [ @@ -576,7 +561,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/kms/resources/:id/decrypt", method: "POST", - telemetry: { service: "kms" }, }, ], [ @@ -584,7 +568,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/:service/resources", method: "POST", - telemetry: { service: "cloud-proxy" }, }, ], [ @@ -592,7 +575,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/:service/resources/:id", method: "PATCH", - telemetry: { service: "cloud-proxy" }, }, ], [ @@ -600,7 +582,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/:service/resources/:id", method: "DELETE", - telemetry: { service: "cloud-proxy" }, }, ], @@ -609,7 +590,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/:service/resources/:id/invoke", method: "POST", - telemetry: { service: "cloud-proxy" }, }, ], @@ -618,7 +598,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/storage/resources/:id/objects", method: "GET", - telemetry: { service: "cloud-proxy" }, }, ], [ @@ -626,7 +605,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/storage/resources/:id/object", method: "PUT", - telemetry: { service: "cloud-proxy" }, }, ], [ @@ -634,7 +612,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/storage/resources/:id/object", method: "GET", - telemetry: { service: "cloud-proxy" }, }, ], [ @@ -642,7 +619,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/storage/resources/:id/object", method: "DELETE", - telemetry: { service: "cloud-proxy" }, }, ], [ @@ -650,7 +626,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/storage/resources/:id/object/copy", method: "POST", - telemetry: { service: "cloud-proxy" }, }, ], [ @@ -658,7 +633,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/nosql/resources/:id/containers", method: "GET", - telemetry: { service: "cloud-proxy" }, }, ], [ @@ -666,7 +640,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/nosql/resources/:id/containers", method: "POST", - telemetry: { service: "cloud-proxy" }, }, ], [ @@ -674,7 +647,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/nosql/resources/:id/containers/:containerId", method: "DELETE", - telemetry: { service: "cloud-proxy" }, }, ], [ @@ -682,7 +654,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/nosql/resources/:id/containers/:containerId/items", method: "GET", - telemetry: { service: "cloud-proxy" }, }, ], [ @@ -690,7 +661,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/nosql/resources/:id/containers/:containerId/items", method: "POST", - telemetry: { service: "cloud-proxy" }, }, ], [ @@ -698,7 +668,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/nosql/resources/:id/containers/:containerId/items/:itemId", method: "DELETE", - telemetry: { service: "cloud-proxy" }, }, ], [ @@ -706,7 +675,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/nosql/resources/:id/containers/:containerId/query", method: "POST", - telemetry: { service: "cloud-proxy" }, }, ], [ @@ -714,7 +682,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/database/resources/:id/sql/databases", method: "POST", - telemetry: { service: "cloud-proxy" }, }, ], [ @@ -722,7 +689,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/database/resources/:id/sql/tables", method: "POST", - telemetry: { service: "cloud-proxy" }, }, ], [ @@ -730,7 +696,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/database/resources/:id/sql/query", method: "POST", - telemetry: { service: "cloud-proxy" }, }, ], [ @@ -738,15 +703,25 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/logs/resources/:id/query", method: "POST", - telemetry: { service: "cloud-proxy" }, }, ], + [ + apiEndpointKeys.clouds.logs.filter, + { + path: "/clouds/:cloud/services/logs/resources/:id/filter", + method: "POST", + }, + ], + [apiEndpointKeys.clouds.messaging.messages.send, {path: "/clouds/:cloud/services/messaging/resources/:id/messages", method: "POST"}], + [apiEndpointKeys.clouds.messaging.messages.receive, {path: "/clouds/:cloud/services/messaging/resources/:id/messages/receive", method: "POST"}], + [apiEndpointKeys.clouds.messaging.messages.delete, {path: "/clouds/:cloud/services/messaging/resources/:id/messages/delete", method: "POST"}], + [apiEndpointKeys.clouds.messaging.messages.purge, {path: "/clouds/:cloud/services/messaging/resources/:id/messages", method: "DELETE"}], + [apiEndpointKeys.clouds.messaging.moveTasks, {path: "/clouds/:cloud/services/messaging/resources/:id/move-tasks", method: "POST"}], [ apiEndpointKeys.clouds.database.snapshots.list, { path: "/clouds/:cloud/services/database/snapshots", method: "GET", - telemetry: { service: "cloud-proxy" }, }, ], [ @@ -754,7 +729,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/database/snapshots", method: "POST", - telemetry: { service: "cloud-proxy" }, }, ], [ @@ -762,23 +736,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/database/orderable-classes", method: "GET", - telemetry: { service: "cloud-proxy" }, - }, - ], - [ - apiEndpointKeys.clouds.nosql.items.list, - { - path: "/clouds/:cloud/services/nosql/resources/:id/items", - method: "GET", - telemetry: { service: "cloud-proxy" }, - }, - ], - [ - apiEndpointKeys.clouds.nosql.items.put, - { - path: "/clouds/:cloud/services/nosql/resources/:id/items", - method: "POST", - telemetry: { service: "cloud-proxy" }, }, ], [ @@ -786,7 +743,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/k8s/resources/:id/nodegroups", method: "GET", - telemetry: { service: "cloud-proxy" }, }, ], [ @@ -794,7 +750,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/k8s/resources/:id/nodegroups", method: "POST", - telemetry: { service: "cloud-proxy" }, }, ], [ @@ -802,7 +757,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/k8s/resources/:id/nodegroups/:nodegroupId", method: "DELETE", - telemetry: { service: "cloud-proxy" }, }, ], [ @@ -810,7 +764,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/k8s/resources/:id/fargate-profiles", method: "GET", - telemetry: { service: "cloud-proxy" }, }, ], [ @@ -818,7 +771,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/k8s/resources/:id/fargate-profiles", method: "POST", - telemetry: { service: "cloud-proxy" }, }, ], [ @@ -826,7 +778,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/k8s/resources/:id/fargate-profiles/:profileId", method: "DELETE", - telemetry: { service: "cloud-proxy" }, }, ], [ @@ -834,7 +785,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/email/inbox", method: "DELETE", - telemetry: { service: "cloud-proxy" }, }, ], [ @@ -842,7 +792,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/configuration/resources/:id/environments", method: "GET", - telemetry: { service: "cloud-proxy" }, }, ], [ @@ -850,7 +799,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/configuration/resources/:id/environments", method: "POST", - telemetry: { service: "cloud-proxy" }, }, ], [ @@ -858,7 +806,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/configuration/resources/:id/environments/:environmentId", method: "DELETE", - telemetry: { service: "cloud-proxy" }, }, ], [ @@ -866,7 +813,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/configuration/resources/:id/environments/:environmentId/deployments", method: "POST", - telemetry: { service: "cloud-proxy" }, }, ], [ @@ -874,7 +820,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/configuration/resources/:id/environments/:environmentId/deployments/:deploymentNumber", method: "GET", - telemetry: { service: "cloud-proxy" }, }, ], [ @@ -882,7 +827,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/configuration/resources/:id/configuration-profiles", method: "GET", - telemetry: { service: "cloud-proxy" }, }, ], [ @@ -890,7 +834,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/configuration/resources/:id/configuration-profiles", method: "POST", - telemetry: { service: "cloud-proxy" }, }, ], [ @@ -898,7 +841,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/configuration/resources/:id/configuration-profiles/:profileId", method: "DELETE", - telemetry: { service: "cloud-proxy" }, }, ], [ @@ -906,7 +848,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/configuration/resources/:id/configuration-profiles/:profileId/hosted-configuration-versions", method: "GET", - telemetry: { service: "cloud-proxy" }, }, ], [ @@ -914,7 +855,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/configuration/resources/:id/configuration-profiles/:profileId/hosted-configuration-versions/:versionNumber", method: "GET", - telemetry: { service: "cloud-proxy" }, }, ], [ @@ -922,7 +862,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/configuration/resources/:id/configuration-profiles/:profileId/hosted-configuration-versions", method: "POST", - telemetry: { service: "cloud-proxy" }, }, ], [ @@ -930,7 +869,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/configuration/resources/:id/configuration-profiles/:profileId/hosted-configuration-versions/:versionNumber", method: "DELETE", - telemetry: { service: "cloud-proxy" }, }, ], [ @@ -938,7 +876,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/configuration/deployment-strategies", method: "GET", - telemetry: { service: "cloud-proxy" }, }, ], [ @@ -946,7 +883,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/configuration/deployment-strategies", method: "POST", - telemetry: { service: "cloud-proxy" }, }, ], [ @@ -954,7 +890,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/configuration/deployment-strategies/:strategyId", method: "DELETE", - telemetry: { service: "cloud-proxy" }, }, ], @@ -963,7 +898,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/:service/resources/:id/collections", method: "GET", - telemetry: { service: "cloud-proxy" }, }, ], [ @@ -971,7 +905,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/:service/resources/:id/collections/:cid/items", method: "GET", - telemetry: { service: "cloud-proxy" }, }, ], [ @@ -979,7 +912,13 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/:service/resources/:id/items", method: "GET", - telemetry: { service: "cloud-proxy" }, + }, + ], + [ + apiEndpointKeys.clouds.flatItems.query, + { + path: "/clouds/:cloud/services/:service/resources/:id/query", + method: "POST", }, ], [ @@ -987,7 +926,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/:service/resources/:id/items", method: "POST", - telemetry: { service: "cloud-proxy" }, }, ], [ @@ -995,7 +933,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/clouds/:cloud/services/:service/resources/:id/items/:itemId", method: "DELETE", - telemetry: { service: "cloud-proxy" }, }, ], // AWS EKS @@ -1004,7 +941,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/eks/clusters", method: "GET", - telemetry: { provider: "aws", service: "eks" }, }, ], [ @@ -1012,7 +948,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/eks/clusters/:name", method: "GET", - telemetry: { provider: "aws", service: "eks" }, }, ], [ @@ -1020,7 +955,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/eks/clusters/:name/nodegroups", method: "GET", - telemetry: { provider: "aws", service: "eks" }, }, ], [ @@ -1028,7 +962,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/eks/clusters/:name/nodegroups/:nodegroup", method: "GET", - telemetry: { provider: "aws", service: "eks" }, }, ], [ @@ -1036,7 +969,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/eks/clusters/:name/nodegroups", method: "POST", - telemetry: { provider: "aws", service: "eks" }, }, ], [ @@ -1044,7 +976,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/eks/clusters/:name/nodegroups/:nodegroup", method: "DELETE", - telemetry: { provider: "aws", service: "eks" }, }, ], [ @@ -1052,7 +983,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/eks/clusters/:name/fargate-profiles", method: "GET", - telemetry: { provider: "aws", service: "eks" }, }, ], [ @@ -1060,7 +990,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/eks/clusters/:name/fargate-profiles/:profile", method: "GET", - telemetry: { provider: "aws", service: "eks" }, }, ], [ @@ -1068,7 +997,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/eks/clusters/:name/fargate-profiles", method: "POST", - telemetry: { provider: "aws", service: "eks" }, }, ], [ @@ -1076,7 +1004,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/eks/clusters/:name/fargate-profiles/:profile", method: "DELETE", - telemetry: { provider: "aws", service: "eks" }, }, ], @@ -1086,7 +1013,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/secretsmanager/secrets", method: "GET", - telemetry: { provider: "aws", service: "secretsmanager" }, }, ], [ @@ -1094,7 +1020,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/secretsmanager/secrets", method: "POST", - telemetry: { provider: "aws", service: "secretsmanager" }, }, ], [ @@ -1102,7 +1027,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/secretsmanager/secret", method: "GET", - telemetry: { provider: "aws", service: "secretsmanager" }, }, ], [ @@ -1110,7 +1034,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/secretsmanager/secret", method: "DELETE", - telemetry: { provider: "aws", service: "secretsmanager" }, }, ], [ @@ -1118,7 +1041,6 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/secretsmanager/secret/value", method: "GET", - telemetry: { provider: "aws", service: "secretsmanager" }, }, ], [ @@ -1126,65 +1048,64 @@ export const endpointRegistry: EndpointRegistry = new Map([ { path: "/secretsmanager/secret/value", method: "PUT", - telemetry: { provider: "aws", service: "secretsmanager" }, }, ], // AWS EC2 - [apiEndpointKeys.aws.ec2.instances.list, { path: "/ec2/instances", method: "GET", telemetry: { provider: "aws", service: "ec2" } }], - [apiEndpointKeys.aws.ec2.instances.describe, { path: "/ec2/instances/:instanceId", method: "GET", telemetry: { provider: "aws", service: "ec2" } }], - [apiEndpointKeys.aws.ec2.instances.create, { path: "/ec2/instances", method: "POST", telemetry: { provider: "aws", service: "ec2" } }], - [apiEndpointKeys.aws.ec2.instances.start, { path: "/ec2/instances/:instanceId/start", method: "POST", telemetry: { provider: "aws", service: "ec2" } }], - [apiEndpointKeys.aws.ec2.instances.stop, { path: "/ec2/instances/:instanceId/stop", method: "POST", telemetry: { provider: "aws", service: "ec2" } }], - [apiEndpointKeys.aws.ec2.instances.reboot, { path: "/ec2/instances/:instanceId/reboot", method: "POST", telemetry: { provider: "aws", service: "ec2" } }], - [apiEndpointKeys.aws.ec2.instances.terminate,{ path: "/ec2/instances/:instanceId/terminate", method: "POST", telemetry: { provider: "aws", service: "ec2" } }], - [apiEndpointKeys.aws.ec2.instances.tags, { path: "/ec2/instances/:instanceId/tags", method: "PUT", telemetry: { provider: "aws", service: "ec2" } }], - [apiEndpointKeys.aws.ec2.instances.createImage, { path: "/ec2/instances/:instanceId/image", method: "POST", telemetry: { provider: "aws", service: "ec2" } }], - [apiEndpointKeys.aws.ec2.instances.console, { path: "/ec2/instances/:instanceId/console", method: "GET", telemetry: { provider: "aws", service: "ec2" } }], - [apiEndpointKeys.aws.ec2.amis.list, { path: "/ec2/amis", method: "GET", telemetry: { provider: "aws", service: "ec2" } }], - [apiEndpointKeys.aws.ec2.amis.deregister, { path: "/ec2/amis/:imageId/deregister", method: "POST", telemetry: { provider: "aws", service: "ec2" } }], - [apiEndpointKeys.aws.ec2.keyPairs.list, { path: "/ec2/key-pairs", method: "GET", telemetry: { provider: "aws", service: "ec2" } }], - [apiEndpointKeys.aws.ec2.keyPairs.create, { path: "/ec2/key-pairs", method: "POST", telemetry: { provider: "aws", service: "ec2" } }], - [apiEndpointKeys.aws.ec2.keyPairs.delete, { path: "/ec2/key-pairs/:name", method: "DELETE", telemetry: { provider: "aws", service: "ec2" } }], - [apiEndpointKeys.aws.ec2.securityGroups.list, { path: "/ec2/security-groups", method: "GET", telemetry: { provider: "aws", service: "ec2" } }], - [apiEndpointKeys.aws.ec2.securityGroups.create, { path: "/ec2/security-groups", method: "POST", telemetry: { provider: "aws", service: "ec2" } }], - [apiEndpointKeys.aws.ec2.securityGroups.delete, { path: "/ec2/security-groups/:groupId", method: "DELETE", telemetry: { provider: "aws", service: "ec2" } }], - [apiEndpointKeys.aws.ec2.securityGroups.authorize, { path: "/ec2/security-groups/:groupId/ingress", method: "POST", telemetry: { provider: "aws", service: "ec2" } }], - [apiEndpointKeys.aws.ec2.securityGroups.revoke, { path: "/ec2/security-groups/:groupId/ingress", method: "DELETE", telemetry: { provider: "aws", service: "ec2" } }], - [apiEndpointKeys.aws.ec2.securityGroups.authorizeEgress, { path: "/ec2/security-groups/:groupId/egress", method: "POST", telemetry: { provider: "aws", service: "ec2" } }], - [apiEndpointKeys.aws.ec2.securityGroups.revokeEgress, { path: "/ec2/security-groups/:groupId/egress", method: "DELETE", telemetry: { provider: "aws", service: "ec2" } }], - [apiEndpointKeys.aws.ec2.vpcs.list, { path: "/ec2/vpcs", method: "GET", telemetry: { provider: "aws", service: "ec2" } }], - [apiEndpointKeys.aws.ec2.vpcs.create, { path: "/ec2/vpcs", method: "POST", telemetry: { provider: "aws", service: "ec2" } }], - [apiEndpointKeys.aws.ec2.vpcs.delete, { path: "/ec2/vpcs/:vpcId", method: "DELETE", telemetry: { provider: "aws", service: "ec2" } }], - [apiEndpointKeys.aws.ec2.vpcs.getAttributes, { path: "/ec2/vpcs/:vpcId/attributes", method: "GET", telemetry: { provider: "aws", service: "ec2" } }], - [apiEndpointKeys.aws.ec2.vpcs.modifyAttribute, { path: "/ec2/vpcs/:vpcId/attributes", method: "PUT", telemetry: { provider: "aws", service: "ec2" } }], - [apiEndpointKeys.aws.ec2.subnets.list, { path: "/ec2/subnets", method: "GET", telemetry: { provider: "aws", service: "ec2" } }], - [apiEndpointKeys.aws.ec2.subnets.create, { path: "/ec2/subnets", method: "POST", telemetry: { provider: "aws", service: "ec2" } }], - [apiEndpointKeys.aws.ec2.subnets.delete, { path: "/ec2/subnets/:subnetId", method: "DELETE", telemetry: { provider: "aws", service: "ec2" } }], - [apiEndpointKeys.aws.ec2.subnets.modifyAttribute, { path: "/ec2/subnets/:subnetId/attributes", method: "PUT", telemetry: { provider: "aws", service: "ec2" } }], - [apiEndpointKeys.aws.ec2.internetGateways.list, { path: "/ec2/internet-gateways", method: "GET", telemetry: { provider: "aws", service: "ec2" } }], - [apiEndpointKeys.aws.ec2.internetGateways.create, { path: "/ec2/internet-gateways", method: "POST", telemetry: { provider: "aws", service: "ec2" } }], - [apiEndpointKeys.aws.ec2.internetGateways.attach, { path: "/ec2/internet-gateways/:igwId/attach", method: "POST", telemetry: { provider: "aws", service: "ec2" } }], - [apiEndpointKeys.aws.ec2.internetGateways.detach, { path: "/ec2/internet-gateways/:igwId/detach", method: "POST", telemetry: { provider: "aws", service: "ec2" } }], - [apiEndpointKeys.aws.ec2.internetGateways.delete, { path: "/ec2/internet-gateways/:igwId", method: "DELETE", telemetry: { provider: "aws", service: "ec2" } }], - [apiEndpointKeys.aws.ec2.natGateways.list, { path: "/ec2/nat-gateways", method: "GET", telemetry: { provider: "aws", service: "ec2" } }], - [apiEndpointKeys.aws.ec2.natGateways.create, { path: "/ec2/nat-gateways", method: "POST", telemetry: { provider: "aws", service: "ec2" } }], - [apiEndpointKeys.aws.ec2.natGateways.delete, { path: "/ec2/nat-gateways/:natId", method: "DELETE", telemetry: { provider: "aws", service: "ec2" } }], - [apiEndpointKeys.aws.ec2.routeTables.list, { path: "/ec2/route-tables", method: "GET", telemetry: { provider: "aws", service: "ec2" } }], - [apiEndpointKeys.aws.ec2.routeTables.create, { path: "/ec2/route-tables", method: "POST", telemetry: { provider: "aws", service: "ec2" } }], - [apiEndpointKeys.aws.ec2.routeTables.delete, { path: "/ec2/route-tables/:rtbId", method: "DELETE", telemetry: { provider: "aws", service: "ec2" } }], - [apiEndpointKeys.aws.ec2.routeTables.createRoute, { path: "/ec2/route-tables/:rtbId/routes", method: "POST", telemetry: { provider: "aws", service: "ec2" } }], - [apiEndpointKeys.aws.ec2.routeTables.deleteRoute, { path: "/ec2/route-tables/:rtbId/routes", method: "DELETE", telemetry: { provider: "aws", service: "ec2" } }], - [apiEndpointKeys.aws.ec2.routeTables.associate, { path: "/ec2/route-tables/:rtbId/associations", method: "POST", telemetry: { provider: "aws", service: "ec2" } }], - [apiEndpointKeys.aws.ec2.routeTables.disassociate,{ path: "/ec2/route-table-associations/:associationId", method: "DELETE", telemetry: { provider: "aws", service: "ec2" } }], - [apiEndpointKeys.aws.ec2.elasticIps.list, { path: "/ec2/elastic-ips", method: "GET", telemetry: { provider: "aws", service: "ec2" } }], - [apiEndpointKeys.aws.ec2.elasticIps.create, { path: "/ec2/elastic-ips", method: "POST", telemetry: { provider: "aws", service: "ec2" } }], - [apiEndpointKeys.aws.ec2.elasticIps.release, { path: "/ec2/elastic-ips/:allocationId/release", method: "POST", telemetry: { provider: "aws", service: "ec2" } }], - [apiEndpointKeys.aws.ec2.elasticIps.associate, { path: "/ec2/elastic-ips/:allocationId/associate", method: "POST", telemetry: { provider: "aws", service: "ec2" } }], - [apiEndpointKeys.aws.ec2.elasticIps.disassociate, { path: "/ec2/elastic-ips/:allocationId/disassociate", method: "POST", telemetry: { provider: "aws", service: "ec2" } }], - [apiEndpointKeys.aws.ec2.availabilityZones, { path: "/ec2/availability-zones", method: "GET", telemetry: { provider: "aws", service: "ec2" } }], - [apiEndpointKeys.aws.ec2.instanceTypes, { path: "/ec2/instance-types", method: "GET", telemetry: { provider: "aws", service: "ec2" } }], - [apiEndpointKeys.aws.ec2.vpcWizard, { path: "/ec2/vpc-wizard", method: "POST", telemetry: { provider: "aws", service: "ec2" } }], + [apiEndpointKeys.aws.ec2.instances.list, { path: "/ec2/instances", method: "GET" }], + [apiEndpointKeys.aws.ec2.instances.describe, { path: "/ec2/instances/:instanceId", method: "GET" }], + [apiEndpointKeys.aws.ec2.instances.create, { path: "/ec2/instances", method: "POST" }], + [apiEndpointKeys.aws.ec2.instances.start, { path: "/ec2/instances/:instanceId/start", method: "POST" }], + [apiEndpointKeys.aws.ec2.instances.stop, { path: "/ec2/instances/:instanceId/stop", method: "POST" }], + [apiEndpointKeys.aws.ec2.instances.reboot, { path: "/ec2/instances/:instanceId/reboot", method: "POST" }], + [apiEndpointKeys.aws.ec2.instances.terminate,{ path: "/ec2/instances/:instanceId/terminate", method: "POST" }], + [apiEndpointKeys.aws.ec2.instances.tags, { path: "/ec2/instances/:instanceId/tags", method: "PUT" }], + [apiEndpointKeys.aws.ec2.instances.createImage, { path: "/ec2/instances/:instanceId/image", method: "POST" }], + [apiEndpointKeys.aws.ec2.instances.console, { path: "/ec2/instances/:instanceId/console", method: "GET" }], + [apiEndpointKeys.aws.ec2.amis.list, { path: "/ec2/amis", method: "GET" }], + [apiEndpointKeys.aws.ec2.amis.deregister, { path: "/ec2/amis/:imageId/deregister", method: "POST" }], + [apiEndpointKeys.aws.ec2.keyPairs.list, { path: "/ec2/key-pairs", method: "GET" }], + [apiEndpointKeys.aws.ec2.keyPairs.create, { path: "/ec2/key-pairs", method: "POST" }], + [apiEndpointKeys.aws.ec2.keyPairs.delete, { path: "/ec2/key-pairs/:name", method: "DELETE" }], + [apiEndpointKeys.aws.ec2.securityGroups.list, { path: "/ec2/security-groups", method: "GET" }], + [apiEndpointKeys.aws.ec2.securityGroups.create, { path: "/ec2/security-groups", method: "POST" }], + [apiEndpointKeys.aws.ec2.securityGroups.delete, { path: "/ec2/security-groups/:groupId", method: "DELETE" }], + [apiEndpointKeys.aws.ec2.securityGroups.authorize, { path: "/ec2/security-groups/:groupId/ingress", method: "POST" }], + [apiEndpointKeys.aws.ec2.securityGroups.revoke, { path: "/ec2/security-groups/:groupId/ingress", method: "DELETE" }], + [apiEndpointKeys.aws.ec2.securityGroups.authorizeEgress, { path: "/ec2/security-groups/:groupId/egress", method: "POST" }], + [apiEndpointKeys.aws.ec2.securityGroups.revokeEgress, { path: "/ec2/security-groups/:groupId/egress", method: "DELETE" }], + [apiEndpointKeys.aws.ec2.vpcs.list, { path: "/ec2/vpcs", method: "GET" }], + [apiEndpointKeys.aws.ec2.vpcs.create, { path: "/ec2/vpcs", method: "POST" }], + [apiEndpointKeys.aws.ec2.vpcs.delete, { path: "/ec2/vpcs/:vpcId", method: "DELETE" }], + [apiEndpointKeys.aws.ec2.vpcs.getAttributes, { path: "/ec2/vpcs/:vpcId/attributes", method: "GET" }], + [apiEndpointKeys.aws.ec2.vpcs.modifyAttribute, { path: "/ec2/vpcs/:vpcId/attributes", method: "PUT" }], + [apiEndpointKeys.aws.ec2.subnets.list, { path: "/ec2/subnets", method: "GET" }], + [apiEndpointKeys.aws.ec2.subnets.create, { path: "/ec2/subnets", method: "POST" }], + [apiEndpointKeys.aws.ec2.subnets.delete, { path: "/ec2/subnets/:subnetId", method: "DELETE" }], + [apiEndpointKeys.aws.ec2.subnets.modifyAttribute, { path: "/ec2/subnets/:subnetId/attributes", method: "PUT" }], + [apiEndpointKeys.aws.ec2.internetGateways.list, { path: "/ec2/internet-gateways", method: "GET" }], + [apiEndpointKeys.aws.ec2.internetGateways.create, { path: "/ec2/internet-gateways", method: "POST" }], + [apiEndpointKeys.aws.ec2.internetGateways.attach, { path: "/ec2/internet-gateways/:igwId/attach", method: "POST" }], + [apiEndpointKeys.aws.ec2.internetGateways.detach, { path: "/ec2/internet-gateways/:igwId/detach", method: "POST" }], + [apiEndpointKeys.aws.ec2.internetGateways.delete, { path: "/ec2/internet-gateways/:igwId", method: "DELETE" }], + [apiEndpointKeys.aws.ec2.natGateways.list, { path: "/ec2/nat-gateways", method: "GET" }], + [apiEndpointKeys.aws.ec2.natGateways.create, { path: "/ec2/nat-gateways", method: "POST" }], + [apiEndpointKeys.aws.ec2.natGateways.delete, { path: "/ec2/nat-gateways/:natId", method: "DELETE" }], + [apiEndpointKeys.aws.ec2.routeTables.list, { path: "/ec2/route-tables", method: "GET" }], + [apiEndpointKeys.aws.ec2.routeTables.create, { path: "/ec2/route-tables", method: "POST" }], + [apiEndpointKeys.aws.ec2.routeTables.delete, { path: "/ec2/route-tables/:rtbId", method: "DELETE" }], + [apiEndpointKeys.aws.ec2.routeTables.createRoute, { path: "/ec2/route-tables/:rtbId/routes", method: "POST" }], + [apiEndpointKeys.aws.ec2.routeTables.deleteRoute, { path: "/ec2/route-tables/:rtbId/routes", method: "DELETE" }], + [apiEndpointKeys.aws.ec2.routeTables.associate, { path: "/ec2/route-tables/:rtbId/associations", method: "POST" }], + [apiEndpointKeys.aws.ec2.routeTables.disassociate,{ path: "/ec2/route-table-associations/:associationId", method: "DELETE" }], + [apiEndpointKeys.aws.ec2.elasticIps.list, { path: "/ec2/elastic-ips", method: "GET" }], + [apiEndpointKeys.aws.ec2.elasticIps.create, { path: "/ec2/elastic-ips", method: "POST" }], + [apiEndpointKeys.aws.ec2.elasticIps.release, { path: "/ec2/elastic-ips/:allocationId/release", method: "POST" }], + [apiEndpointKeys.aws.ec2.elasticIps.associate, { path: "/ec2/elastic-ips/:allocationId/associate", method: "POST" }], + [apiEndpointKeys.aws.ec2.elasticIps.disassociate, { path: "/ec2/elastic-ips/:allocationId/disassociate", method: "POST" }], + [apiEndpointKeys.aws.ec2.availabilityZones, { path: "/ec2/availability-zones", method: "GET" }], + [apiEndpointKeys.aws.ec2.instanceTypes, { path: "/ec2/instance-types", method: "GET" }], + [apiEndpointKeys.aws.ec2.vpcWizard, { path: "/ec2/vpc-wizard", method: "POST" }], ]); diff --git a/packages/frontend/src/api/cloudProxyClient.ts b/packages/frontend/src/api/cloudProxyClient.ts index 6cfe543..7e5084d 100644 --- a/packages/frontend/src/api/cloudProxyClient.ts +++ b/packages/frontend/src/api/cloudProxyClient.ts @@ -28,7 +28,9 @@ import type { KubernetesNodegroup, LogsInsightsQueryInput, LogsInsightsQueryResult, - NoSqlItem, + LogsFilterInput, + SqsMessage, + SqsSendMessageInput, SqlCredentials, SqlDatabase, SqlEngine, @@ -257,6 +259,8 @@ export interface WorkflowExecutionHistoryPage { export interface CreateApiGatewayLambdaRouteInput { path: string; + parentResourceId?: string; + pathPart?: string; method: string; stage: string; functionName: string; @@ -311,7 +315,16 @@ export interface ApiGatewayRouteOverview { integrationType: string | null; } +export interface ApiGatewayResourceOverview { + resourceId: string; + parentId: string | null; + path: string; + pathPart: string | null; + methods: ApiGatewayRouteOverview[]; +} + export interface ApiGatewayRoutesResult { + resources: ApiGatewayResourceOverview[]; routes: ApiGatewayRouteOverview[]; stages: string[]; } @@ -401,6 +414,20 @@ export interface EventBridgeRuleOverview extends CreateEventBridgeRuleInput { state: "ENABLED" | "DISABLED"; } +export interface PutEventBridgeEventInput { + source: string; + detailType: string; + detail: string; + resources?: string[]; +} + +export interface PutEventBridgeEventResult { + eventId: string | null; + errorCode: string | null; + errorMessage: string | null; +} +export interface CloudFormationChangeSet { name: string; id: string; status: string | null; executionStatus: string | null; changes: Array<{action: string | null; logicalResourceId: string | null; resourceType: string | null}> } + export interface CreateSqsLambdaEventSourceMappingInput { queueArn: string; batchSize?: number; @@ -697,6 +724,26 @@ export async function deleteEventBridgeRule( ); } +export async function putEventBridgeEvent( + cloud: CloudProvider, + eventBusId: string, + input: PutEventBridgeEventInput, + signal?: AbortSignal, +): Promise { + const res = await apiClient.call( + apiEndpointKeys.clouds.resources.putEventBridgeEvent, + requestOptions(cloud, "events", {signal, body: input}), + {cloud, id: eventBusId}, + ); + return res.data; +} + +export async function listCloudFormationChangeSets(cloud: CloudProvider, stackName: string, signal?: AbortSignal): Promise { const res = await apiClient.call(apiEndpointKeys.clouds.resources.listCloudFormationChangeSets, requestOptions(cloud, 'iac', {signal}), {cloud, id: stackName}); return res.data } +export async function createCloudFormationChangeSet(cloud: CloudProvider, stackName: string, input: {name: string; templateBody: string; parameters?: Record; capabilities?: string[]}): Promise { const res = await apiClient.call(apiEndpointKeys.clouds.resources.createCloudFormationChangeSet, requestOptions(cloud, 'iac', {body: input}), {cloud, id: stackName}); return res.data } +export async function executeCloudFormationChangeSet(cloud: CloudProvider, stackName: string, name: string): Promise { await apiClient.call(apiEndpointKeys.clouds.resources.executeCloudFormationChangeSet, requestOptions(cloud, 'iac', {}), {cloud, id: stackName, name}) } +export async function deleteCloudFormationChangeSet(cloud: CloudProvider, stackName: string, name: string): Promise { await apiClient.call(apiEndpointKeys.clouds.resources.deleteCloudFormationChangeSet, requestOptions(cloud, 'iac', {}), {cloud, id: stackName, name}) } +export async function updateCloudFormationTerminationProtection(cloud: CloudProvider, stackName: string, enabled: boolean): Promise { await apiClient.call(apiEndpointKeys.clouds.resources.updateCloudFormationTerminationProtection, requestOptions(cloud, 'iac', {body: {enabled}}), {cloud, id: stackName}) } + export async function createSqsLambdaEventSourceMapping( cloud: CloudProvider, functionName: string, @@ -1107,6 +1154,23 @@ export async function listFlatItems( return res.data; } +export async function queryFlatItems( + cloud: CloudProvider, + service: CloudServiceType, + resourceId: string, + query: Record, + cursor?: string, + limit = 100, + signal?: AbortSignal, +): Promise> { + const res = await apiClient.call, {query: Record}>( + apiEndpointKeys.clouds.flatItems.query, + requestOptions(cloud, service, {signal, params: {cursor, limit}, body: {query}}), + {cloud, service, id: resourceId}, + ); + return res.data; +} + export async function deleteFlatItem( cloud: CloudProvider, service: CloudServiceType, @@ -1215,6 +1279,44 @@ export async function queryLogs( return res.data; } +export async function filterLogEvents( + cloud: CloudProvider, + logGroupName: string, + input: LogsFilterInput, + cursor?: string, + signal?: AbortSignal, +): Promise> { + const res = await apiClient.call, LogsFilterInput>( + apiEndpointKeys.clouds.logs.filter, + requestOptions(cloud, "logs", { signal, body: input, params: cursor ? { cursor } : undefined }), + { cloud, id: logGroupName }, + ); + return res.data; +} + +export async function sendSqsMessage(cloud: CloudProvider, queueId: string, input: SqsSendMessageInput): Promise<{messageId: string}> { + const res = await apiClient.call<{messageId: string}, SqsSendMessageInput>(apiEndpointKeys.clouds.messaging.messages.send, requestOptions(cloud, 'messaging', {body: input}), {cloud, id: queueId}) + return res.data +} + +export async function receiveSqsMessages(cloud: CloudProvider, queueId: string, maxNumberOfMessages: number): Promise { + const res = await apiClient.call(apiEndpointKeys.clouds.messaging.messages.receive, requestOptions(cloud, 'messaging', {body: {maxNumberOfMessages}}), {cloud, id: queueId}) + return res.data +} + +export async function deleteSqsMessage(cloud: CloudProvider, queueId: string, receiptHandle: string): Promise { + await apiClient.call(apiEndpointKeys.clouds.messaging.messages.delete, requestOptions(cloud, 'messaging', {body: {receiptHandle}}), {cloud, id: queueId}) +} + +export async function purgeSqsQueue(cloud: CloudProvider, queueId: string): Promise { + await apiClient.call(apiEndpointKeys.clouds.messaging.messages.purge, requestOptions(cloud, 'messaging'), {cloud, id: queueId}) +} + +export async function startSqsMoveTask(cloud: CloudProvider, queueId: string, destinationArn?: string): Promise<{taskHandle: string}> { + const res = await apiClient.call<{taskHandle: string}, {destinationArn?: string}>(apiEndpointKeys.clouds.messaging.moveTasks, requestOptions(cloud, 'messaging', {body: {destinationArn}}), {cloud, id: queueId}) + return res.data +} + export async function listDatabaseSnapshots( cloud: CloudProvider, instanceIdentifier?: string, @@ -1260,33 +1362,6 @@ export async function listDatabaseOrderableClasses( return res.data; } -export async function listNoSqlItems( - cloud: CloudProvider, - resourceId: string, - signal?: AbortSignal, -): Promise { - const res = await apiClient.call( - apiEndpointKeys.clouds.nosql.items.list, - requestOptions(cloud, "nosql", { signal }), - { cloud, id: resourceId }, - ); - return res.data; -} - -export async function putNoSqlItem( - cloud: CloudProvider, - resourceId: string, - document: Record, - signal?: AbortSignal, -): Promise { - const res = await apiClient.call>( - apiEndpointKeys.clouds.nosql.items.put, - requestOptions(cloud, "nosql", { signal, body: document }), - { cloud, id: resourceId }, - ); - return res.data; -} - export async function listKubernetesNodegroups( cloud: CloudProvider, clusterId: string, @@ -1560,8 +1635,8 @@ export async function getAppConfigDeployment( } function requestOptions( - cloud: CloudProvider, - service: string, + _cloud: CloudProvider, + _service: string, options: { signal?: AbortSignal; params?: Record; @@ -1574,7 +1649,6 @@ function requestOptions( ) { return { ...options, - telemetry: { provider: cloud, service }, }; } diff --git a/packages/frontend/src/api/floci-client.ts b/packages/frontend/src/api/floci-client.ts deleted file mode 100644 index 67fe1dc..0000000 --- a/packages/frontend/src/api/floci-client.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { apiClient } from "./api"; -import type { HttpMethod } from "./HttpClient"; -import { - subscribeApiRequests, - type ApiRequestEvent, -} from "@/telemetry"; - -export type FlociRequestEvent = ApiRequestEvent; - -export function subscribeRequests( - cb: (event: FlociRequestEvent) => void, -): () => void { - return subscribeApiRequests(cb); -} - -export function apiGet( - path: string, - service: string, - signal?: AbortSignal, -): Promise { - return apiRequest("GET", path, service, undefined, signal); -} - -export function apiPost( - path: string, - service: string, - body: unknown, - signal?: AbortSignal, -): Promise { - return apiRequest("POST", path, service, body, signal); -} - -export function apiPut( - path: string, - service: string, - body: unknown, - signal?: AbortSignal, -): Promise { - return apiRequest("PUT", path, service, body, signal); -} - -export function apiDelete( - path: string, - service: string, - signal?: AbortSignal, -): Promise { - return apiRequest("DELETE", path, service, undefined, signal); -} - -async function apiRequest( - method: HttpMethod, - path: string, - service: string, - body?: unknown, - signal?: AbortSignal, -): Promise { - const res = await apiClient.request(method, path, { - signal, - ...(body === undefined ? {} : { body }), - telemetry: { provider: "aws", service }, - }); - return res.data; -} diff --git a/packages/frontend/src/api/requestEvents.ts b/packages/frontend/src/api/requestEvents.ts deleted file mode 100644 index 8f60112..0000000 --- a/packages/frontend/src/api/requestEvents.ts +++ /dev/null @@ -1,6 +0,0 @@ -export { - apiRequestEventBus, - emitApiRequest, - subscribeApiRequests, -} from "@/telemetry/requestEventBus"; -export type { ApiRequestEvent, CloudProvider } from "@/telemetry/types"; diff --git a/packages/frontend/src/assets/aws-services/NOTICE.md b/packages/frontend/src/assets/aws-services/NOTICE.md new file mode 100644 index 0000000..34df444 --- /dev/null +++ b/packages/frontend/src/assets/aws-services/NOTICE.md @@ -0,0 +1,17 @@ +# AWS Architecture Icons + +The SVG files in this directory are AWS Architecture Icons provided by Amazon +Web Services. They are used to identify AWS services in the Floci UI. + +Source and usage guidelines: +https://aws.amazon.com/architecture/icons/ + +AWS and the AWS service names and icons are trademarks of Amazon.com, Inc. or +its affiliates. Their inclusion does not imply sponsorship or endorsement. + +The source pack did not include a license or notice file. Only the service icons +used by Floci UI were copied here, with normalized filenames. The generic Elastic +Load Balancing, CloudWatch, and Systems Manager icons represent Application Load +Balancer, CloudWatch Logs, and Parameter Store where the pack has no dedicated +service icon. Services without matching artwork continue to use the documented +Lucide fallback. diff --git a/packages/frontend/src/assets/aws-services/api-gateway.svg b/packages/frontend/src/assets/aws-services/api-gateway.svg new file mode 100644 index 0000000..19247ff --- /dev/null +++ b/packages/frontend/src/assets/aws-services/api-gateway.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_ Amazon-API-Gateway_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/frontend/src/assets/aws-services/appconfig.svg b/packages/frontend/src/assets/aws-services/appconfig.svg new file mode 100644 index 0000000..b30a6c3 --- /dev/null +++ b/packages/frontend/src/assets/aws-services/appconfig.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-App-Config_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/frontend/src/assets/aws-services/cloudformation.svg b/packages/frontend/src/assets/aws-services/cloudformation.svg new file mode 100644 index 0000000..c2271fc --- /dev/null +++ b/packages/frontend/src/assets/aws-services/cloudformation.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-CloudFormation_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/frontend/src/assets/aws-services/cloudwatch-logs.svg b/packages/frontend/src/assets/aws-services/cloudwatch-logs.svg new file mode 100644 index 0000000..103369a --- /dev/null +++ b/packages/frontend/src/assets/aws-services/cloudwatch-logs.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-CloudWatch_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/frontend/src/assets/aws-services/dynamodb.svg b/packages/frontend/src/assets/aws-services/dynamodb.svg new file mode 100644 index 0000000..bd4f2c3 --- /dev/null +++ b/packages/frontend/src/assets/aws-services/dynamodb.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-DynamoDB_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/frontend/src/assets/aws-services/ec2.svg b/packages/frontend/src/assets/aws-services/ec2.svg new file mode 100644 index 0000000..14f083f --- /dev/null +++ b/packages/frontend/src/assets/aws-services/ec2.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-EC2_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/frontend/src/assets/aws-services/ecr.svg b/packages/frontend/src/assets/aws-services/ecr.svg new file mode 100644 index 0000000..2283358 --- /dev/null +++ b/packages/frontend/src/assets/aws-services/ecr.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-Elastic-Container-Registry_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/frontend/src/assets/aws-services/ecs.svg b/packages/frontend/src/assets/aws-services/ecs.svg new file mode 100644 index 0000000..c2ef4c2 --- /dev/null +++ b/packages/frontend/src/assets/aws-services/ecs.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-Elastic-Container-Service_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/frontend/src/assets/aws-services/eks.svg b/packages/frontend/src/assets/aws-services/eks.svg new file mode 100644 index 0000000..8fa8530 --- /dev/null +++ b/packages/frontend/src/assets/aws-services/eks.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-Elastic-Container-Kubernetes_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/frontend/src/assets/aws-services/elastic-load-balancing.svg b/packages/frontend/src/assets/aws-services/elastic-load-balancing.svg new file mode 100644 index 0000000..b79b920 --- /dev/null +++ b/packages/frontend/src/assets/aws-services/elastic-load-balancing.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Elastic-Load-Balancing_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/frontend/src/assets/aws-services/eventbridge.svg b/packages/frontend/src/assets/aws-services/eventbridge.svg new file mode 100644 index 0000000..469a4d3 --- /dev/null +++ b/packages/frontend/src/assets/aws-services/eventbridge.svg @@ -0,0 +1,16 @@ + + + Icon-Architecture/64/Arch_Amazon-EventBridge_64 + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/frontend/src/assets/aws-services/iam.svg b/packages/frontend/src/assets/aws-services/iam.svg new file mode 100644 index 0000000..618d0b4 --- /dev/null +++ b/packages/frontend/src/assets/aws-services/iam.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Identity-and-Access-Management_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/frontend/src/assets/aws-services/kms.svg b/packages/frontend/src/assets/aws-services/kms.svg new file mode 100644 index 0000000..01a811a --- /dev/null +++ b/packages/frontend/src/assets/aws-services/kms.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Key-Management-Services_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/frontend/src/assets/aws-services/lambda.svg b/packages/frontend/src/assets/aws-services/lambda.svg new file mode 100644 index 0000000..496ef0e --- /dev/null +++ b/packages/frontend/src/assets/aws-services/lambda.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Lambda_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/frontend/src/assets/aws-services/parameter-store.svg b/packages/frontend/src/assets/aws-services/parameter-store.svg new file mode 100644 index 0000000..70f9e8c --- /dev/null +++ b/packages/frontend/src/assets/aws-services/parameter-store.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Systems-Manager_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/frontend/src/assets/aws-services/rds.svg b/packages/frontend/src/assets/aws-services/rds.svg new file mode 100644 index 0000000..245d237 --- /dev/null +++ b/packages/frontend/src/assets/aws-services/rds.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-RDS_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/frontend/src/assets/aws-services/s3.svg b/packages/frontend/src/assets/aws-services/s3.svg new file mode 100644 index 0000000..425fe62 --- /dev/null +++ b/packages/frontend/src/assets/aws-services/s3.svg @@ -0,0 +1,18 @@ + + + Icon-Architecture/64/Arch_Amazon-Simple-Storage-Service_64 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/frontend/src/assets/aws-services/secrets-manager.svg b/packages/frontend/src/assets/aws-services/secrets-manager.svg new file mode 100644 index 0000000..558b227 --- /dev/null +++ b/packages/frontend/src/assets/aws-services/secrets-manager.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Secrets-Manager_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/frontend/src/assets/aws-services/ses.svg b/packages/frontend/src/assets/aws-services/ses.svg new file mode 100644 index 0000000..c907bb3 --- /dev/null +++ b/packages/frontend/src/assets/aws-services/ses.svg @@ -0,0 +1,16 @@ + + + Icon-Architecture/64/Arch_Amazon-Simple-Email-Service_64 + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/frontend/src/assets/aws-services/sns.svg b/packages/frontend/src/assets/aws-services/sns.svg new file mode 100644 index 0000000..6cb54ad --- /dev/null +++ b/packages/frontend/src/assets/aws-services/sns.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Simple-Notification-Service_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/frontend/src/assets/aws-services/sqs.svg b/packages/frontend/src/assets/aws-services/sqs.svg new file mode 100644 index 0000000..20726b5 --- /dev/null +++ b/packages/frontend/src/assets/aws-services/sqs.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Simple-Queue-Service_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/frontend/src/assets/aws-services/step-functions.svg b/packages/frontend/src/assets/aws-services/step-functions.svg new file mode 100644 index 0000000..db87650 --- /dev/null +++ b/packages/frontend/src/assets/aws-services/step-functions.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Step-Functions_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/frontend/src/assets/aws-services/vpc.svg b/packages/frontend/src/assets/aws-services/vpc.svg new file mode 100644 index 0000000..0568d19 --- /dev/null +++ b/packages/frontend/src/assets/aws-services/vpc.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-Virtual-Private-Cloud_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/frontend/src/components/ApiGatewayRoutePanel.test.tsx b/packages/frontend/src/components/ApiGatewayRoutePanel.test.tsx index 8463a3d..a95bb0a 100644 --- a/packages/frontend/src/components/ApiGatewayRoutePanel.test.tsx +++ b/packages/frontend/src/components/ApiGatewayRoutePanel.test.tsx @@ -1,7 +1,7 @@ import {QueryClient, QueryClientProvider} from '@tanstack/react-query' import {fireEvent, render, screen, waitFor} from '@testing-library/react' import userEvent from '@testing-library/user-event' -import {expect, test, vi} from 'vitest' +import {beforeEach, expect, test, vi} from 'vitest' import {ApiGatewayRoutePanel} from './ApiGatewayRoutePanel' import type {CloudResource} from '@/types/resource' @@ -18,6 +18,10 @@ const apiMocks = vi.hoisted(() => ({ vi.mock('@/api/cloudProxyClient', () => apiMocks) +beforeEach(() => { + vi.clearAllMocks() +}) + const api: CloudResource = { id: 'api-123', name: 'orders', cloud: 'aws', service: 'apigateway', type: 'rest-api', region: null, createdAt: null, metadata: {}, @@ -28,9 +32,27 @@ const lambda: CloudResource = { region: null, createdAt: null, metadata: {}, } +const rootResource = {resourceId: 'root-123', parentId: null, path: '/', pathPart: null, methods: []} + +function routesResult(routes: Array<{resourceId: string; path: string; method: string; functionName: string | null; integrationType?: string | null}> = [], stages = ['dev']) { + const byResource = new Map() + for (const route of routes) byResource.set(route.resourceId, [...(byResource.get(route.resourceId) ?? []), route]) + return { + resources: [rootResource, ...[...byResource.entries()].map(([resourceId, methods]) => ({ + resourceId, + parentId: 'root-123', + path: methods[0]!.path, + pathPart: methods[0]!.path.slice(1), + methods: methods.map((method) => ({...method, integrationType: method.integrationType ?? null})), + }))], + routes: routes.map((route) => ({...route, integrationType: route.integrationType ?? null})), + stages, + } +} + test('creates and displays a deployed Lambda proxy route', async () => { apiMocks.listCloudResources.mockResolvedValue([lambda]) - apiMocks.listApiGatewayRoutes.mockResolvedValue({routes: [], stages: ['dev']}) + apiMocks.listApiGatewayRoutes.mockResolvedValue(routesResult()) apiMocks.createApiGatewayLambdaRoute.mockResolvedValue({ resourceId: 'resource-123', deploymentId: 'deployment-123', invokeUrl: 'http://localhost:4566/execute-api/api-123/dev/orders', }) @@ -43,22 +65,88 @@ test('creates and displays a deployed Lambda proxy route', async () => { , ) - await user.clear(screen.getByPlaceholderText('/orders')) - await user.type(screen.getByPlaceholderText('/orders'), '/orders') - await user.selectOptions(screen.getAllByRole('combobox')[1]!, 'orders-handler') + await user.clear(screen.getByLabelText('Path part')) + await user.type(screen.getByLabelText('Path part'), 'orders') + await user.selectOptions(screen.getByLabelText('Lambda function'), 'orders-handler') await user.click(screen.getByRole('button', {name: 'Create and deploy route'})) await waitFor(() => { expect(apiMocks.createApiGatewayLambdaRoute).toHaveBeenCalledWith('aws', 'api-123', { - path: '/orders', method: 'GET', stage: 'dev', functionName: 'orders-handler', + path: '/orders', parentResourceId: 'root-123', pathPart: 'orders', method: 'GET', stage: 'dev', functionName: 'orders-handler', }) }) expect(await screen.findByText('Route deployed')).toBeInTheDocument() }) +test('renders root, empty resources, nested resources, and methods as a tree', async () => { + apiMocks.listCloudResources.mockResolvedValue([lambda]) + apiMocks.listApiGatewayRoutes.mockResolvedValue({ + resources: [ + rootResource, + {resourceId: 'orders-123', parentId: 'root-123', path: '/orders', pathPart: 'orders', methods: []}, + {resourceId: 'order-123', parentId: 'orders-123', path: '/orders/{id}', pathPart: '{id}', methods: [ + {resourceId: 'order-123', path: '/orders/{id}', method: 'PATCH', functionName: 'orders-handler', integrationType: 'AWS_PROXY'}, + ]}, + ], + routes: [{resourceId: 'order-123', path: '/orders/{id}', method: 'PATCH', functionName: 'orders-handler', integrationType: 'AWS_PROXY'}], + stages: ['dev'], + }) + const queryClient = new QueryClient({defaultOptions: {queries: {retry: false}}}) + + render() + + const nodes = await screen.findAllByRole('treeitem') + expect(nodes).toHaveLength(3) + expect(nodes.map((node) => node.getAttribute('aria-level'))).toEqual(['1', '2', '3']) + expect(screen.getByRole('button', {name: '/ordersNo methods'})).toBeInTheDocument() + expect(screen.getByRole('button', {name: /PATCH.*\/orders\/\{id}.*orders-handler/})).toBeInTheDocument() +}) + +test('creates a path parameter under the selected parent resource', async () => { + apiMocks.listCloudResources.mockResolvedValue([lambda]) + apiMocks.listApiGatewayRoutes.mockResolvedValue({ + resources: [rootResource, {resourceId: 'orders-123', parentId: 'root-123', path: '/orders', pathPart: 'orders', methods: []}], + routes: [], + stages: ['dev'], + }) + apiMocks.createApiGatewayLambdaRoute.mockResolvedValue({resourceId: 'order-123', deploymentId: 'deployment-123', invokeUrl: '/orders/{id}'}) + const user = userEvent.setup() + const queryClient = new QueryClient({defaultOptions: {queries: {retry: false}}}) + render() + + await screen.findByRole('option', {name: '/orders'}) + await user.selectOptions(screen.getByLabelText('Parent resource'), 'orders-123') + fireEvent.change(screen.getByLabelText('Path part'), {target: {value: '{id}'}}) + await user.selectOptions(screen.getByLabelText('Method'), 'PATCH') + await user.selectOptions(screen.getByLabelText('Lambda function'), 'orders-handler') + await user.click(screen.getByRole('button', {name: 'Create and deploy route'})) + + await waitFor(() => expect(apiMocks.createApiGatewayLambdaRoute).toHaveBeenCalledWith('aws', 'api-123', { + path: '/orders/{id}', parentResourceId: 'orders-123', pathPart: '{id}', method: 'PATCH', stage: 'dev', functionName: 'orders-handler', + })) +}) + +test('uses a concrete HTTP verb when deployed-testing an ANY method', async () => { + apiMocks.listCloudResources.mockResolvedValue([lambda]) + apiMocks.listApiGatewayRoutes.mockResolvedValue(routesResult([ + {resourceId: 'resource-123', path: '/orders', method: 'ANY', functionName: 'orders-handler'}, + ])) + apiMocks.testApiGatewayRoute.mockResolvedValue({statusCode: 200, headers: {}, body: '', durationMs: 1}) + const user = userEvent.setup() + const queryClient = new QueryClient({defaultOptions: {queries: {retry: false}}}) + render() + + await user.click(await screen.findByRole('button', {name: /ANY.*\/orders.*orders-handler/})) + expect(screen.getByText('ANY is API Gateway configuration, not an HTTP request verb.')).toBeInTheDocument() + await user.selectOptions(screen.getByLabelText('HTTP verb for deployed request'), 'HEAD') + await user.click(screen.getByRole('button', {name: 'Send deployed HTTP request'})) + + await waitFor(() => expect(apiMocks.testApiGatewayRoute).toHaveBeenCalledWith('aws', 'api-123', expect.objectContaining({method: 'HEAD'}))) +}) + test('displays and copies the selected API stage base invoke URL', async () => { apiMocks.listCloudResources.mockResolvedValue([lambda]) - apiMocks.listApiGatewayRoutes.mockResolvedValue({routes: [], stages: ['dev']}) + apiMocks.listApiGatewayRoutes.mockResolvedValue(routesResult()) const user = userEvent.setup() const writeText = vi.spyOn(navigator.clipboard, 'writeText').mockResolvedValue() const queryClient = new QueryClient({defaultOptions: {queries: {retry: false}}}) @@ -77,7 +165,7 @@ test('displays and copies the selected API stage base invoke URL', async () => { test('uses the browser host for a Docker bridge runtime endpoint', () => { apiMocks.listCloudResources.mockResolvedValue([lambda]) - apiMocks.listApiGatewayRoutes.mockResolvedValue({routes: [], stages: ['dev']}) + apiMocks.listApiGatewayRoutes.mockResolvedValue(routesResult()) const queryClient = new QueryClient({defaultOptions: {queries: {retry: false}}}) render( @@ -91,7 +179,7 @@ test('uses the browser host for a Docker bridge runtime endpoint', () => { test('sends a route test request and displays its response', async () => { apiMocks.listCloudResources.mockResolvedValue([lambda]) - apiMocks.listApiGatewayRoutes.mockResolvedValue({routes: [], stages: ['dev']}) + apiMocks.listApiGatewayRoutes.mockResolvedValue(routesResult()) apiMocks.testApiGatewayRoute.mockResolvedValue({ statusCode: 200, headers: {'content-type': 'application/json'}, @@ -123,9 +211,9 @@ test('sends a route test request and displays its response', async () => { test('tests a selected method through TestInvokeMethod without a deployed stage', async () => { apiMocks.listCloudResources.mockResolvedValue([lambda]) - apiMocks.listApiGatewayRoutes.mockResolvedValue({ - routes: [{resourceId: 'resource-123', path: '/orders', method: 'POST', functionName: 'orders-handler'}], stages: [], - }) + apiMocks.listApiGatewayRoutes.mockResolvedValue(routesResult([ + {resourceId: 'resource-123', path: '/orders', method: 'POST', functionName: 'orders-handler'}, + ], [])) apiMocks.testApiGatewayMethod.mockResolvedValue({ statusCode: 200, body: '{"message":"Hello"}', headers: {'content-type': 'application/json'}, multiValueHeaders: {'set-cookie': ['session=1']}, log: 'Execution completed', latency: 8, @@ -150,7 +238,7 @@ test('tests a selected method through TestInvokeMethod without a deployed stage' test('formats the request body as JSON', async () => { apiMocks.listCloudResources.mockResolvedValue([lambda]) - apiMocks.listApiGatewayRoutes.mockResolvedValue({routes: [], stages: ['dev']}) + apiMocks.listApiGatewayRoutes.mockResolvedValue(routesResult()) const user = userEvent.setup() const queryClient = new QueryClient({defaultOptions: {queries: {retry: false}}}) @@ -169,10 +257,9 @@ test('formats the request body as JSON', async () => { test('selects a deployed route for testing', async () => { apiMocks.listCloudResources.mockResolvedValue([lambda]) - apiMocks.listApiGatewayRoutes.mockResolvedValue({ - routes: [{resourceId: 'resource-123', path: '/orders', method: 'POST', functionName: 'orders-handler'}], - stages: ['dev'], - }) + apiMocks.listApiGatewayRoutes.mockResolvedValue(routesResult([ + {resourceId: 'resource-123', path: '/orders', method: 'POST', functionName: 'orders-handler'}, + ])) const user = userEvent.setup() const queryClient = new QueryClient({defaultOptions: {queries: {retry: false}}}) @@ -184,16 +271,17 @@ test('selects a deployed route for testing', async () => { await user.click(await screen.findByRole('button', {name: /POST.*\/orders.*orders-handler/})) - expect(screen.getByPlaceholderText('/orders')).toHaveValue('/orders') - expect(screen.getAllByRole('combobox')[0]).toHaveValue('POST') - expect(screen.getAllByRole('combobox')[1]).toHaveValue('orders-handler') + expect(screen.getByText(/POST \/dev\/orders/)).toBeInTheDocument() + expect(screen.getByLabelText('Method')).toHaveValue('POST') + expect(screen.getByLabelText('Lambda function')).toHaveValue('orders-handler') + expect(screen.getByRole('link', {name: 'Open Lambda logs'})).toHaveAttribute('href', '/cloud-explorer/aws/logs?resource=%2Faws%2Flambda%2Forders-handler') }) test('confirms before deleting a deployed route', async () => { apiMocks.listCloudResources.mockResolvedValue([lambda]) - apiMocks.listApiGatewayRoutes.mockResolvedValue({ - routes: [{resourceId: 'resource-123', path: '/orders', method: 'POST', functionName: 'orders-handler'}], stages: ['dev'], - }) + apiMocks.listApiGatewayRoutes.mockResolvedValue(routesResult([ + {resourceId: 'resource-123', path: '/orders', method: 'POST', functionName: 'orders-handler'}, + ])) apiMocks.deleteApiGatewayRoute.mockResolvedValue(undefined) const user = userEvent.setup() const queryClient = new QueryClient({defaultOptions: {queries: {retry: false}}}) @@ -209,12 +297,10 @@ test('confirms before deleting a deployed route', async () => { test('enables CORS for one resource and shows AWS proxy guidance', async () => { apiMocks.listCloudResources.mockResolvedValue([lambda]) - apiMocks.listApiGatewayRoutes.mockResolvedValue({ - routes: [ + apiMocks.listApiGatewayRoutes.mockResolvedValue(routesResult([ {resourceId: 'resource-123', path: '/orders', method: 'GET', functionName: null, integrationType: 'HTTP'}, {resourceId: 'resource-123', path: '/orders', method: 'POST', functionName: 'orders-handler', integrationType: 'AWS_PROXY'}, - ], stages: ['dev'], - }) + ])) apiMocks.enableApiGatewayCors.mockResolvedValue({configuredMethods: ['GET'], proxyMethods: ['POST']}) const user = userEvent.setup() const queryClient = new QueryClient({defaultOptions: {queries: {retry: false}}}) diff --git a/packages/frontend/src/components/ApiGatewayRoutePanel.tsx b/packages/frontend/src/components/ApiGatewayRoutePanel.tsx index b83630e..ca297bd 100644 --- a/packages/frontend/src/components/ApiGatewayRoutePanel.tsx +++ b/packages/frontend/src/components/ApiGatewayRoutePanel.tsx @@ -1,12 +1,13 @@ -import {useEffect, useState} from 'react' -import {ArrowRight, Braces, Copy, Link2, ListTree, Loader2, Rocket, Send, Trash2, Waypoints, X} from 'lucide-react' +import {useEffect, useState, type CSSProperties} from 'react' +import {ArrowRight, Copy, Link2, ListTree, Loader2, Rocket, ScrollText, Send, Trash2, X} from 'lucide-react' import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query' import {createApiGatewayLambdaRoute, deleteApiGatewayRoute, enableApiGatewayCors, listApiGatewayRoutes, listCloudResources, testApiGatewayMethod, testApiGatewayRoute} from '@/api/cloudProxyClient' -import type {ApiGatewayMethodTestResult} from '@/api/cloudProxyClient' +import type {ApiGatewayMethodTestResult, ApiGatewayResourceOverview} from '@/api/cloudProxyClient' import type {CloudProvider} from '@/types/cloud' import type {CloudResource} from '@/types/resource' import {CopyValueButton} from './CopyValueButton' import {LogsQueryPanel} from './LogsQueryPanel' +import {ServiceIcon} from '@/components/serviceIcons' interface ApiGatewayRoutePanelProps { cloud: CloudProvider @@ -17,7 +18,10 @@ interface ApiGatewayRoutePanelProps { export function ApiGatewayRoutePanel({cloud, resource, runtimeReachable, runtimeEndpoint}: ApiGatewayRoutePanelProps) { const [path, setPath] = useState('/hello') + const [parentResourceId, setParentResourceId] = useState('') + const [pathPart, setPathPart] = useState('hello') const [method, setMethod] = useState('GET') + const [anyTestMethod, setAnyTestMethod] = useState('GET') const [stage, setStage] = useState('dev') const [functionName, setFunctionName] = useState('') const [testBody, setTestBody] = useState('{}') @@ -48,7 +52,10 @@ export function ApiGatewayRoutePanel({cloud, resource, runtimeReachable, runtime useEffect(() => { setPath('/hello') + setParentResourceId('') + setPathPart('hello') setMethod('GET') + setAnyTestMethod('GET') setStage('dev') setFunctionName('') setTestBody('{}') @@ -68,13 +75,20 @@ export function ApiGatewayRoutePanel({cloud, resource, runtimeReachable, runtime }, [resource?.id]) const routeMutation = useMutation({ - mutationFn: () => createApiGatewayLambdaRoute(cloud, resource!.id, {path, method, stage, functionName}), + mutationFn: () => createApiGatewayLambdaRoute(cloud, resource!.id, { + path: createPath, + parentResourceId: selectedParent?.resourceId, + pathPart, + method, + stage, + functionName, + }), onSuccess: () => queryClient.invalidateQueries({queryKey: ['api-gateway-routes', cloud, resource!.id]}), }) const testMutation = useMutation({ mutationFn: () => testApiGatewayRoute(cloud, resource!.id, { path, - method, + method: deployedHttpMethod, stage, body: testBody, headers: parseHeaders(testHeaders), @@ -132,9 +146,14 @@ export function ApiGatewayRoutePanel({cloud, resource, runtimeReachable, runtime } const functions = functionsQuery.data?.filter((item) => item.type === 'lambda') ?? [] - const canCreate = cloud === 'aws' && runtimeReachable && Boolean(functionName) && !routeMutation.isPending - const existingPath = routesQuery.data?.routes.some((route) => route.path === path) ?? false - const curlCommand = buildCurlCommand(resource.id, stage, path, method, testHeaders, testQuery, testBody) + const apiResources = routesQuery.data?.resources ?? [] + const rootResource = apiResources.find((item) => item.path === '/') + const selectedParent = apiResources.find((item) => item.resourceId === parentResourceId) ?? rootResource + const createPath = selectedParent ? childResourcePath(selectedParent.path, pathPart) : `/${pathPart}` + const deployedHttpMethod = method === 'ANY' ? anyTestMethod : method + const canCreate = cloud === 'aws' && runtimeReachable && Boolean(functionName && selectedParent && pathPart) && !routeMutation.isPending + const existingPath = apiResources.some((item) => item.path === createPath) + const curlCommand = buildCurlCommand(resource.id, stage, path, deployedHttpMethod, testHeaders, testQuery, testBody) const endpoint = invokeEndpoint(runtimeEndpoint) const baseInvokeUrl = endpoint ? `${endpoint}/execute-api/${resource.id}/${stage}` : null @@ -154,6 +173,7 @@ export function ApiGatewayRoutePanel({cloud, resource, runtimeReachable, runtime autoRunKey={logsRefreshKey} /> )} + {functionName && Open Lambda logs}
{baseInvokeUrl &&
@@ -168,20 +188,27 @@ export function ApiGatewayRoutePanel({cloud, resource, runtimeReachable, runtime if (canCreate) routeMutation.mutate() }}> +
{routesQuery.isLoading &&

Loading configured methods...

} {routesQuery.isError &&

{routesQuery.error instanceof Error ? routesQuery.error.message : 'Unable to load API routes.'}

} - {routesQuery.data?.routes.length === 0 &&

No configured methods yet. Create a route above.

} - {routesQuery.data && routesQuery.data.routes.length > 0 && ( -
- {routesQuery.data.routes.map((route) => { - const selected = route.path === path && route.method === method - const key = `${route.resourceId}-${route.method}` - const resourceMethods = routesQuery.data.routes - .filter((item) => item.resourceId === route.resourceId && CORS_METHODS.includes(item.method)) - .map((item) => item.method) - const isFirstForResource = routesQuery.data.routes.findIndex((item) => item.resourceId === route.resourceId) === routesQuery.data.routes.indexOf(route) - return
- + {resourceMethods.length > 0 && } +
+ {treeResource.methods.map((route) => { + const selected = route.path === path && route.method === method + const key = `${route.resourceId}-${route.method}` + return
+ - {isFirstForResource && } - {deleteConfirmation === key ?
+ {deleteConfirmation === key ?
Delete {route.method}?
: } - {corsConfirmation?.resourceId === route.resourceId && isFirstForResource &&
- Enable CORS for {corsConfirmation.path} - - -
Allowed methods{resourceMethods.map((value) => )}
- Updates this resource's OPTIONS preflight and non-proxy method responses, then deploys {stage}. - - - {corsMutation.isError &&

{corsMutation.error instanceof Error ? corsMutation.error.message : 'Unable to enable CORS.'}

} -
} -
- })} +
+ })} + {corsConfirmation?.resourceId === treeResource.resourceId &&
+ Enable CORS for {corsConfirmation.path} + + +
Allowed methods{resourceMethods.map((value) => )}
+ Updates this resource's OPTIONS preflight and non-proxy method responses, then deploys {stage}. + + + {corsMutation.isError &&

{corsMutation.error instanceof Error ? corsMutation.error.message : 'Unable to enable CORS.'}

} +
} + {children.length > 0 && {children.length} child resources} +
+ })}
)} {corsProxyMethods.length > 0 && } @@ -274,11 +309,16 @@ export function ApiGatewayRoutePanel({cloud, resource, runtimeReachable, runtime
-
{ - event.preventDefault() - if (selectedResourceId && !methodTestMutation.isPending) methodTestMutation.mutate() - }}> -