fufu-cloud-cli 0.9.7__tar.gz → 0.9.8__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. {fufu_cloud_cli-0.9.7/src/fufu_cloud_cli.egg-info → fufu_cloud_cli-0.9.8}/PKG-INFO +31 -4
  2. {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/README.md +30 -3
  3. {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/pyproject.toml +1 -1
  4. {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/__init__.py +1 -1
  5. {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/aws_errors.py +1 -1
  6. {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/capabilities.json +55 -19
  7. {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/cli.py +32 -8
  8. {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/cloud_data.py +1 -1
  9. {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/control.py +2 -2
  10. {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/docker_backend.py +15 -3
  11. {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/gcp_infra.py +42 -9
  12. {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/gcs.py +95 -6
  13. {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/lambda_adapter.py +17 -0
  14. {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/object_transfer.py +11 -6
  15. fufu_cloud_cli-0.9.8/src/fufu_cloud_cli/observability.py +127 -0
  16. {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/runtime_sdk.py +1 -1
  17. {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/terraform_bridge/runner.py +15 -1
  18. {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8/src/fufu_cloud_cli.egg-info}/PKG-INFO +31 -4
  19. {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli.egg-info/SOURCES.txt +1 -0
  20. {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/LICENSE +0 -0
  21. {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/MANIFEST.in +0 -0
  22. {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/setup.cfg +0 -0
  23. {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/__main__.py +0 -0
  24. {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/api_backend.py +0 -0
  25. {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/aws_lambda.py +0 -0
  26. {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/aws_output.py +0 -0
  27. {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/bigquery.py +0 -0
  28. {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/capacity.py +0 -0
  29. {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/cloudflare.py +0 -0
  30. {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/errors.py +0 -0
  31. {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/gcloud_environment.py +0 -0
  32. {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/gcloud_output.py +0 -0
  33. {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/gcp_sdk.py +0 -0
  34. {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/http_runtime.py +0 -0
  35. {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/iac_state.py +0 -0
  36. {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/limits.py +0 -0
  37. {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/manifest.py +0 -0
  38. {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/public_access.py +0 -0
  39. {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/pubsub.py +0 -0
  40. {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/pulumi-endpoints.json +0 -0
  41. {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/pulumi_bridge.py +0 -0
  42. {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/remote_compute.py +0 -0
  43. {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/runtime/functions/Dockerfile +0 -0
  44. {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/runtime/ingress/Dockerfile +0 -0
  45. {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/runtime/ingress/proxy.py +0 -0
  46. {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/secrets.py +0 -0
  47. {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/serverless.py +0 -0
  48. {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/terraform_bridge/__init__.py +0 -0
  49. {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/terraform_bridge/aws.py +0 -0
  50. {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/terraform_bridge/aws_data.py +0 -0
  51. {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/terraform_bridge/common.py +0 -0
  52. {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/terraform_bridge/google.py +0 -0
  53. {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/terraform_bridge/google_pubsub.py +0 -0
  54. {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/terraform_bridge/remote_state.py +0 -0
  55. {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/vendor/AWS-CLI-LICENSE.txt +0 -0
  56. {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/vendor/__init__.py +0 -0
  57. {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/vendor/aws_text.py +0 -0
  58. {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli.egg-info/dependency_links.txt +0 -0
  59. {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli.egg-info/entry_points.txt +0 -0
  60. {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli.egg-info/requires.txt +0 -0
  61. {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fufu-cloud-cli
3
- Version: 0.9.7
3
+ Version: 0.9.8
4
4
  Summary: FUFU Cloud CLI for tenant-scoped mock cloud development and deployment
5
5
  License-Expression: LicenseRef-Proprietary
6
6
  Requires-Python: >=3.10
@@ -27,6 +27,32 @@ Dynamic: license-file
27
27
  `gcloud-fufu`、`az-fufu`、`wrangler-fufu`、`bq-fufu`。這是 AWS、Google Cloud、Azure 與 Cloudflare 的**明確子集**,
28
28
  只供 FUFU mock 環境使用;不會把部署轉送到正式雲端。
29
29
 
30
+ 2026-09-10 後續原始碼與 Cloud Shell 新增 GKE、firewall、五類 global LB 與 Redis
31
+ 配置命令,typed configuration 合計 22 類;都有 `MOCK ONLY` 提示、引用存在性與
32
+ 相依刪除檢查,不建立 Kubernetes、VM、Redis 或網路資料面,也未增加 IaC 資源。
33
+ 這批包含在 0.9.8;需要配套的 FUFU Console 與運算服務。
34
+
35
+ 0.9.8 提供 Cloud Run/Functions 的原生形式日誌查詢、CloudWatch Logs
36
+ tail/follow,以及 `fufu observability` 的日誌、請求指標與追蹤查詢。
37
+ 公開套件包含完整的日誌指令模組;重複建立 CloudWatch log group 的
38
+ `ResourceAlreadyExistsException` 會保留至 CLI 與 boto3 例外處理。
39
+ 配套服務端持久化收集 Cloud Run、Functions Gen 1/Gen 2、Lambda 的
40
+ stdout/stderr 與例外堆疊。支援租戶隔離、IAM、敏感值遮罩、查詢分頁、
41
+ request ID/trace 關聯及重啟恢復。GCP log names 清單、dry run、欄位存在
42
+ 查詢,以及 AWS 10,000 筆/1 MiB 寫入批次均有契約驗收。
43
+
44
+ ```bash
45
+ gcloud-fufu run services logs read hello --limit=100 --log-filter='severity>=ERROR'
46
+ gcloud-fufu functions logs read hello --gen2 --limit=100
47
+ aws-fufu logs tail /aws/lambda/hello --since 1h --follow --fufu-duration 300
48
+ fufu observability status --provider gcp
49
+ fufu observability logs --provider aws --request-id REQUEST_ID --since 1h
50
+ ```
51
+
52
+ 請求 metrics/traces 是明示的 FUFU 擴充,沒有實作完整 Cloud Monitoring、
53
+ CloudWatch Metrics、Trace 或 X-Ray。Log Router sinks、Logs Insights、
54
+ subscriptions、alerts 及完整 OpenTelemetry collector 尚未支援。
55
+
30
56
  0.9.7 補齊 Lambda 五種 `wait` 指令、設定/程式碼更新的 `--revision-id`,
31
57
  以及常用設定的 `--cli-input-json` 和 `Variables={KEY=value}` shorthand。
32
58
  過期 RevisionId 回 `PreconditionFailedException`,保留原部署;CLI 仍等待
@@ -62,8 +88,9 @@ fufu infrastructure catalog
62
88
  ```
63
89
 
64
90
  BigQuery 是 tenant emulator 的 GoogleSQL 子集,無 DML/DDL、load/export、
65
- 外部表與效能/費用等價。Logging 保留每租戶最新 10,000 筆,不自動收集所有
66
- 容器 stdout。Tasks 每 queue 每秒最多 1 次、每租戶 2 個同時投遞;耗盡 task
91
+ 外部表與效能/費用等價。配套新版 Logging 預設每租戶 2 GiB;GCP 保留
92
+ 30 天,AWS 依 log group retention 設定,未設定時不自動到期。只收集已
93
+ 驗證歸屬的 FUFU 工作負載容器。Tasks 每 queue 每秒最多 1 次、每租戶 2 個同時投遞;耗盡 task
67
94
  留待檢查,與官方刪除策略不同。新的 dependency flow 不代表完整應用都能遷移。
68
95
 
69
96
  0.9.4 新增 GCS generation/metageneration 條件、`storage objects describe/update/list`、8 KiB 自訂 metadata 與二進位 `cat`。競爭寫入、過期條件、CLI metadata 和官方 gcloud 輸出欄位已對照驗收;不相符條件回傳 `PRECONDITION_FAILED`/HTTP 412。新功能需要同步更新 FUFU Console、GCS 儲存映像和 Cloud Shell。儲存升級前須停寫並備份完整 volume;新 metadata 格式可讀舊資料,回滾須還原升級前備份。
@@ -80,7 +107,7 @@ Python 預設識別而遇到 Cloudflare HTTP 403/1010。識別標頭不取代
80
107
  ## Cloudflare 前端部署與 R2
81
108
 
82
109
  ```bash
83
- pip install 'fufu-cloud-cli==0.9.7'
110
+ pip install 'fufu-cloud-cli==0.9.8'
84
111
  export FUFU_CLOUDFLARE_CREDENTIAL_FILE=/run/secrets/fufu-cloudflare.json
85
112
  wrangler-fufu capabilities
86
113
  wrangler-fufu pages project create frontend --production-branch main
@@ -4,6 +4,32 @@
4
4
  `gcloud-fufu`、`az-fufu`、`wrangler-fufu`、`bq-fufu`。這是 AWS、Google Cloud、Azure 與 Cloudflare 的**明確子集**,
5
5
  只供 FUFU mock 環境使用;不會把部署轉送到正式雲端。
6
6
 
7
+ 2026-09-10 後續原始碼與 Cloud Shell 新增 GKE、firewall、五類 global LB 與 Redis
8
+ 配置命令,typed configuration 合計 22 類;都有 `MOCK ONLY` 提示、引用存在性與
9
+ 相依刪除檢查,不建立 Kubernetes、VM、Redis 或網路資料面,也未增加 IaC 資源。
10
+ 這批包含在 0.9.8;需要配套的 FUFU Console 與運算服務。
11
+
12
+ 0.9.8 提供 Cloud Run/Functions 的原生形式日誌查詢、CloudWatch Logs
13
+ tail/follow,以及 `fufu observability` 的日誌、請求指標與追蹤查詢。
14
+ 公開套件包含完整的日誌指令模組;重複建立 CloudWatch log group 的
15
+ `ResourceAlreadyExistsException` 會保留至 CLI 與 boto3 例外處理。
16
+ 配套服務端持久化收集 Cloud Run、Functions Gen 1/Gen 2、Lambda 的
17
+ stdout/stderr 與例外堆疊。支援租戶隔離、IAM、敏感值遮罩、查詢分頁、
18
+ request ID/trace 關聯及重啟恢復。GCP log names 清單、dry run、欄位存在
19
+ 查詢,以及 AWS 10,000 筆/1 MiB 寫入批次均有契約驗收。
20
+
21
+ ```bash
22
+ gcloud-fufu run services logs read hello --limit=100 --log-filter='severity>=ERROR'
23
+ gcloud-fufu functions logs read hello --gen2 --limit=100
24
+ aws-fufu logs tail /aws/lambda/hello --since 1h --follow --fufu-duration 300
25
+ fufu observability status --provider gcp
26
+ fufu observability logs --provider aws --request-id REQUEST_ID --since 1h
27
+ ```
28
+
29
+ 請求 metrics/traces 是明示的 FUFU 擴充,沒有實作完整 Cloud Monitoring、
30
+ CloudWatch Metrics、Trace 或 X-Ray。Log Router sinks、Logs Insights、
31
+ subscriptions、alerts 及完整 OpenTelemetry collector 尚未支援。
32
+
7
33
  0.9.7 補齊 Lambda 五種 `wait` 指令、設定/程式碼更新的 `--revision-id`,
8
34
  以及常用設定的 `--cli-input-json` 和 `Variables={KEY=value}` shorthand。
9
35
  過期 RevisionId 回 `PreconditionFailedException`,保留原部署;CLI 仍等待
@@ -39,8 +65,9 @@ fufu infrastructure catalog
39
65
  ```
40
66
 
41
67
  BigQuery 是 tenant emulator 的 GoogleSQL 子集,無 DML/DDL、load/export、
42
- 外部表與效能/費用等價。Logging 保留每租戶最新 10,000 筆,不自動收集所有
43
- 容器 stdout。Tasks 每 queue 每秒最多 1 次、每租戶 2 個同時投遞;耗盡 task
68
+ 外部表與效能/費用等價。配套新版 Logging 預設每租戶 2 GiB;GCP 保留
69
+ 30 天,AWS 依 log group retention 設定,未設定時不自動到期。只收集已
70
+ 驗證歸屬的 FUFU 工作負載容器。Tasks 每 queue 每秒最多 1 次、每租戶 2 個同時投遞;耗盡 task
44
71
  留待檢查,與官方刪除策略不同。新的 dependency flow 不代表完整應用都能遷移。
45
72
 
46
73
  0.9.4 新增 GCS generation/metageneration 條件、`storage objects describe/update/list`、8 KiB 自訂 metadata 與二進位 `cat`。競爭寫入、過期條件、CLI metadata 和官方 gcloud 輸出欄位已對照驗收;不相符條件回傳 `PRECONDITION_FAILED`/HTTP 412。新功能需要同步更新 FUFU Console、GCS 儲存映像和 Cloud Shell。儲存升級前須停寫並備份完整 volume;新 metadata 格式可讀舊資料,回滾須還原升級前備份。
@@ -57,7 +84,7 @@ Python 預設識別而遇到 Cloudflare HTTP 403/1010。識別標頭不取代
57
84
  ## Cloudflare 前端部署與 R2
58
85
 
59
86
  ```bash
60
- pip install 'fufu-cloud-cli==0.9.7'
87
+ pip install 'fufu-cloud-cli==0.9.8'
61
88
  export FUFU_CLOUDFLARE_CREDENTIAL_FILE=/run/secrets/fufu-cloudflare.json
62
89
  wrangler-fufu capabilities
63
90
  wrangler-fufu pages project create frontend --production-branch main
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "fufu-cloud-cli"
7
- version = "0.9.7"
7
+ version = "0.9.8"
8
8
  description = "FUFU Cloud CLI for tenant-scoped mock cloud development and deployment"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1,3 +1,3 @@
1
1
  """FUFU CLI 與本機部署工具;相容範圍見 capabilities.json。"""
2
2
 
3
- __version__ = "0.9.7"
3
+ __version__ = "0.9.8"
@@ -1,5 +1,5 @@
1
1
  """Only documented provider error codes may cross the public transport boundary."""
2
- AWS_ERRORS = frozenset('''ConditionalCheckFailedException ResourceNotFoundException ResourceInUseException ValidationException
2
+ AWS_ERRORS = frozenset('''ConditionalCheckFailedException ResourceNotFoundException ResourceInUseException ResourceAlreadyExistsException ValidationException
3
3
  NoSuchKey NoSuchBucket NotFound NotModified ParameterNotFound ParameterAlreadyExists ParameterVersionNotFound QueueDoesNotExist
4
4
  PreconditionFailed InvalidRange InvalidParameterException InvalidParameterValue InvalidAttributeName InvalidAttributeValue QueueNameExists
5
5
  PurgeQueueInProgress ReceiptHandleIsInvalid BatchEntryIdsNotDistinct EmptyBatchRequest BatchRequestTooLong InvalidBatchEntryId
@@ -226,7 +226,8 @@
226
226
  "no-Entra-STS"
227
227
  ]
228
228
  },
229
- "aws-cloudwatch-logs": {"status": "implemented", "backend": "api", "mode": "simplified", "operations": ["log-group-crud", "log-stream-crud", "put-get-filter-events", "retention-policy", "CLI-UI-boto3"], "limitations": ["no-automatic-stdout-collection", "simple-text-filters-only", "no-Insights-subscriptions-or-KMS", "no-guaranteed-retention-deletion", "service-page-tokens-no-CLI-auto-pagination"]},
229
+ "aws-cloudwatch-logs": {"status": "implemented", "backend": "api", "mode": "simplified", "operations": ["log-group-crud", "log-stream-crud", "put-get-filter-events", "retention-policy-expiry", "CLI-UI-boto3", "owned-Lambda-stdout-stderr", "logs-tail-follow", "SQLite-persistence", "signed-pagination", "credential-redaction"], "limitations": ["single-host-configurable-storage-capacity", "simple-text-filters-only", "no-Insights-subscriptions-or-KMS", "follow-default-300s-max-3600s", "service-page-tokens-except-tail-auto-pagination"]},
230
+ "fufu-observability": {"status": "implemented", "backend": "api", "mode": "simplified", "operations": ["logs", "request-metrics", "request-traces", "collector-status", "GCP-AWS-IAM", "structured-trace-links"], "limitations": ["explicit-FUFU-extension-not-native-Monitoring-CloudWatch-Trace-XRay-API", "gateway-request-duration-not-handler-or-billing-metrics", "no-automatic-SDK-spans-alerts-or-cross-service-trace-assembly", "plain-stdout-correlates-by-service-revision", "async-Lambda-logs-without-request-metrics", "single-host-no-HA"]},
230
231
  "aws-lambda-async": {"status": "implemented", "backend": "api", "mode": "simplified", "operations": ["invoke-Event", "invoke-DryRun", "CLI-UI-boto3-Invoke"], "limitations": ["1MiB-payload", "single-host-SQLite-two-dispatchers", "at-least-once", "two-handler-retries-60s-120s", "system-retries-up-to-6h", "no-destinations-or-DLQ", "no-AWS-scaling-or-managed-timeout-equivalence", "10000-events-or-512MiB-encoded-queue-per-tenant"]},
231
232
  "aws-cli-output": {"status": "implemented", "mode": "simplified", "operations": ["JMESPath-query", "json", "text", "yaml"], "limitations": ["no-table-or-yaml-stream", "no-full-CLI-auto-pagination", "Lambda-response-subset-no-Code-download"]},
232
233
  "aws-data-services": {
@@ -345,17 +346,21 @@
345
346
  "mode": "simplified",
346
347
  "operations": [
347
348
  "write-read",
348
- "filter-AND-severity-timestamp-logName-resourceType",
349
+ "filter-AND-OR-NOT-structured-fields-severity-timestamp-trace-text",
349
350
  "logs-list-delete",
350
351
  "partialSuccess",
351
352
  "explicit-official-Python-REST-SDK",
352
- "setup-logging"
353
+ "setup-logging",
354
+ "owned-CloudRun-Functions-stdout-stderr",
355
+ "run-services-logs-read",
356
+ "functions-logs-read-gen1-gen2",
357
+ "SQLite-persistence-retention-redaction"
353
358
  ],
354
359
  "limitations": [
355
- "last-10000-entries-per-tenant",
356
- "single-writer-128MiB-state-capacity",
357
- "no-automatic-container-stdout",
358
- "no-sinks-retention-metrics-alerts",
360
+ "single-host-configurable-storage-capacity-default-2GiB-per-tenant",
361
+ "default-30-day-log-retention",
362
+ "bounded-durable-spool-and-Docker-log-rotation",
363
+ "no-sinks-alerts-or-native-Monitoring-Trace-API",
359
364
  "no-new-IaC-resources"
360
365
  ]
361
366
  },
@@ -378,16 +383,30 @@
378
383
  "builds",
379
384
  "gemini-models",
380
385
  "vm-policies",
381
- "eventarc-triggers"
386
+ "eventarc-triggers",
387
+ "gke-clusters",
388
+ "firewall-rules",
389
+ "health-checks",
390
+ "backend-services",
391
+ "url-maps",
392
+ "target-http-proxies",
393
+ "forwarding-rules",
394
+ "redis-instances"
382
395
  ],
383
396
  "operations": [
384
397
  "typed-crud",
385
398
  "three-locale-mobile-console",
386
- "fufu-infrastructure-cli"
399
+ "fufu-infrastructure-cli",
400
+ "native-gcloud-command-subsets",
401
+ "same-tenant-reference-validation",
402
+ "referenced-resource-delete-conflict"
387
403
  ],
388
404
  "limitations": [
389
405
  "configuration-only",
390
- "no-VM-SQL-network-DNS-build-registry-inference-runtime",
406
+ "no-VM-SQL-network-DNS-build-registry-inference-Kubernetes-Redis-LB-runtime",
407
+ "configuration-envelope-not-official-resource-JSON",
408
+ "LB-global-subset-regional-flags-rejected",
409
+ "no-complete-reference-use-permission-model",
391
410
  "no-new-IaC-resources"
392
411
  ]
393
412
  },
@@ -492,8 +511,11 @@
492
511
  },
493
512
  "gcp-memorystore": {
494
513
  "name": "Memorystore",
495
- "status": "partial",
496
- "reason": "工作區有 Redis 引擎;不提供 Memorystore 控制面。"
514
+ "status": "mock-only",
515
+ "backend": "api",
516
+ "mode": "mock-only",
517
+ "operations": ["typed-redis-configuration-crud", "network-reference-validation"],
518
+ "reason": "僅保存 Memorystore 配置;不建立 Redis process、endpoint、replication 或 persistence。工作區既有 Redis 引擎是獨立服務。"
497
519
  },
498
520
  "aws-elasticache": {
499
521
  "name": "ElastiCache",
@@ -522,8 +544,19 @@
522
544
  },
523
545
  "gcp-gke": {
524
546
  "name": "GKE",
525
- "status": "unsupported",
526
- "reason": "尚未實作可供 CLI 自動部署與驗證的服務契約。"
547
+ "status": "mock-only",
548
+ "backend": "api",
549
+ "mode": "mock-only",
550
+ "operations": ["typed-cluster-configuration-crud", "network-reference-validation"],
551
+ "reason": "僅保存叢集配置;無 Kubernetes API、nodes、kubectl、scheduler 或 workloads。"
552
+ },
553
+ "gcp-vpc": {
554
+ "name": "VPC / Firewall",
555
+ "status": "mock-only",
556
+ "backend": "api",
557
+ "mode": "mock-only",
558
+ "operations": ["network-subnet-connector-firewall-configuration-crud"],
559
+ "reason": "僅保存配置;規則不會過濾封包,沒有路由、位址配置或真實 VPC。"
527
560
  },
528
561
  "gcp-cloud-run-jobs": {
529
562
  "name": "Cloud Run Jobs",
@@ -592,12 +625,15 @@
592
625
  "gcp-cloud-logging-monitoring": {
593
626
  "name": "Cloud Logging / Monitoring",
594
627
  "status": "partial",
595
- "reason": "gcp-logging 已提供可執行日誌子集;Monitoring、alerts、sinks 與完整 stdout 收集未支援。"
628
+ "reason": "Cloud Logging 已接持久化、工作負載 stdout/stderr、CLI 與 UI;請求 metrics/traces 為明示 FUFU 擴充。未實作官方 Monitoring/Cloud Trace API、alerts 或 sinks。"
596
629
  },
597
630
  "gcp-load-balancing-nat": {
598
631
  "name": "Cloud Load Balancing / NAT",
599
- "status": "unsupported",
600
- "reason": "尚未實作可供 CLI 自動部署與驗證的服務契約。"
632
+ "status": "mock-only",
633
+ "backend": "api",
634
+ "mode": "mock-only",
635
+ "operations": ["global-health-check-backend-url-map-http-proxy-forwarding-rule-configuration-crud"],
636
+ "reason": "LB 僅保存 global 配置相依鏈;沒有探測、IP、listener、TLS 或轉送。NAT 尚未支援。"
601
637
  },
602
638
  "aws-ec2": {
603
639
  "name": "EC2",
@@ -664,8 +700,8 @@
664
700
  },
665
701
  "aws-cloudwatch": {
666
702
  "name": "CloudWatch",
667
- "status": "unsupported",
668
- "reason": "尚未實作可供 CLI 自動部署與驗證的服務契約。"
703
+ "status": "partial",
704
+ "reason": "CloudWatch Logs 明列 API 子集、Lambda stdout/stderr、CLI tail、持久化與 retention 已接入;請求 metrics/traces 使用 FUFU 擴充,未實作官方 Metrics/X-Ray API 或 alarms。"
669
705
  },
670
706
  "aws-cloudfront": {
671
707
  "name": "CloudFront",
@@ -104,6 +104,7 @@ def fufu_parser():
104
104
  p = Parser(prog="fufu", description="FUFU 租戶認證、遠端部署與本機 Docker;所有輸出均為 FUFU 子集契約。")
105
105
  p.add_argument("--version", action="version", version=__version__)
106
106
  sub = p.add_subparsers(dest="action", required=True)
107
+ sub.add_parser('observability', help='FUFU 擴充:日誌、請求指標、trace 與收集狀態;使用 observability --help')
107
108
  auth = sub.add_parser("auth", help="驗證 CLI credential,顯示目前租戶與 scope").add_subparsers(dest="auth_action", required=True)
108
109
  identity = auth.add_parser("whoami")
109
110
  identity.add_argument("--provider", choices=["aws", "gcp", "azure", "cloudflare"])
@@ -495,7 +496,7 @@ def entry(provider, argv=None):
495
496
  p.add_argument('--quiet', '-q', action='store_true', help='非互動執行')
496
497
  p.add_argument('--version', action='store_true', help='CLI 版本')
497
498
  groups = p.add_subparsers(title='服務')
498
- for group in ['run', 'functions', 'iam', 'projects', 'pubsub', 'secrets', 'storage', 'auth', 'scheduler', 'tasks', 'logging', 'compute', 'sql', 'artifacts', 'dns', 'eventarc']: groups.add_parser(group)
499
+ for group in ['run', 'functions', 'iam', 'projects', 'pubsub', 'secrets', 'storage', 'auth', 'scheduler', 'tasks', 'logging', 'compute', 'sql', 'artifacts', 'dns', 'eventarc', 'container', 'redis']: groups.add_parser(group)
499
500
  p.print_help(); return
500
501
  if provider == 'bq-fufu':
501
502
  from .bigquery import command
@@ -519,10 +520,15 @@ def entry(provider, argv=None):
519
520
  emit(configuration_command(argv[1:], Parser)); return
520
521
  # Native global selectors are checked against the authenticated tenant scope.
521
522
  if provider in {'aws-fufu', 'gcloud-fufu'}:
522
- rest = []; i = 0
523
+ rest = []; i = 0; explicit_region = False
524
+ native_tail=provider=='aws-fufu' and any(argv[j:j+2]==['logs','tail'] for j in range(len(argv)))
525
+ native_s3=provider=='aws-fufu' and 's3' in argv
526
+ native_storage_ls=provider=='gcloud-fufu' and any(argv[j:j+2]==['storage','ls'] for j in range(len(argv)))
527
+ native_function_logs=provider=='gcloud-fufu' and any(argv[j:j+3]==['functions','logs','read'] for j in range(len(argv)))
523
528
  while i < len(argv):
524
529
  flag, _, inline = argv[i].partition('=')
525
- if flag in {'--region', '--project', '--output', '--format', '--impersonate-service-account', '--filter', '--sort-by', '--query'} or flag == '--limit' and provider == 'gcloud-fufu' and 'list' in argv:
530
+ native_option=flag=='--format' and (native_tail or native_storage_ls) or flag=='--filter' and native_function_logs
531
+ if not native_option and (flag in {'--region', '--project', '--output', '--format', '--impersonate-service-account', '--filter', '--sort-by', '--query'} or flag == '--limit' and provider == 'gcloud-fufu' and 'list' in argv):
526
532
  value = inline if '=' in argv[i] else (argv[i + 1] if i + 1 < len(argv) else '')
527
533
  i += 1 if '=' in argv[i] else 2
528
534
  require(bool(value) and not value.startswith('--'), 'UNSUPPORTED_ARGUMENT', '全域旗標缺少值。')
@@ -531,7 +537,7 @@ def entry(provider, argv=None):
531
537
  from .aws_output import expression
532
538
  aws_query = expression(value)
533
539
  elif flag == '--output' and provider == 'aws-fufu':
534
- require(value in {'json', 'text', 'yaml', 'off'}, 'UNSUPPORTED_FORMAT', 'AWS 輸出支援 json、text、yaml、off。')
540
+ require(value in ({'json', 'text', 'yaml', 'table', 'yaml-stream', 'off'} if native_s3 else {'json', 'text', 'yaml', 'off'}), 'UNSUPPORTED_FORMAT', 'AWS 輸出支援 json、text、yaml、off。')
535
541
  aws_format = value
536
542
  elif flag == '--impersonate-service-account':
537
543
  require(provider == 'gcloud-fufu', 'UNSUPPORTED_ARGUMENT', '此旗標只適用於 gcloud-fufu。')
@@ -550,12 +556,16 @@ def entry(provider, argv=None):
550
556
  if flag == '--limit':
551
557
  require(value.isdigit(), 'UNSUPPORTED_ARGUMENT', 'limit 必須為非負整數。'); output_limit = int(value)
552
558
  elif flag in {'--output', '--format'}: require(value == 'json', 'UNSUPPORTED_ARGUMENT', '此入口目前輸出格式支援 json。')
553
- else: os.environ['FUFU_SELECTED_REGION' if flag == '--region' else 'FUFU_SELECTED_PROJECT'] = value
554
- elif flag in {'--quiet', '-q', '--no-cli-pager'}:
559
+ else:
560
+ if flag == '--region': explicit_region = True
561
+ os.environ['FUFU_SELECTED_REGION' if flag == '--region' else 'FUFU_SELECTED_PROJECT'] = value
562
+ elif flag in {'--quiet', '-q', '--no-cli-pager'} and not (native_s3 and flag == '--quiet'):
555
563
  require('=' not in argv[i], 'UNSUPPORTED_ARGUMENT', '布林旗標不接受額外值。'); i += 1
556
564
  else: rest.append(argv[i]); i += 1
557
565
  argv = rest
558
566
  if provider == 'gcloud-fufu':
567
+ if explicit_region and argv[:2] in [['compute', name] for name in ['health-checks', 'backend-services', 'url-maps', 'target-http-proxies', 'forwarding-rules']]:
568
+ require(False, 'CAPABILITY_NOT_SUPPORTED', '目前 LB 配置僅支援 global 子集;不能以 --region 建立或查詢區域資源。')
559
569
  if output_filter or output_sort or output_limit is not None:
560
570
  list_commands = [('run', 'services', 'list'), ('functions', 'list'), ('iam', 'service-accounts', 'list'),
561
571
  ('pubsub', 'topics', 'list'), ('pubsub', 'subscriptions', 'list'), ('secrets', 'list'),
@@ -564,11 +574,13 @@ def entry(provider, argv=None):
564
574
  ('compute', 'instances', 'list'), ('sql', 'instances', 'list'), ('sql', 'databases', 'list'),
565
575
  ('artifacts', 'repositories', 'list'), ('dns', 'managed-zones', 'list'), ('eventarc', 'triggers', 'list'),
566
576
  ('compute', 'networks', 'list'), ('compute', 'networks', 'subnets', 'list'), ('compute', 'networks', 'vpc-access', 'connectors', 'list')]
577
+ from .gcp_infra import MOCK_GROUPS
578
+ list_commands.extend((*command, 'list') for command in MOCK_GROUPS)
567
579
  require(any(tuple(argv[:len(command)]) == command for command in list_commands), 'UNSUPPORTED_ARGUMENT', '清單選取旗標只適用於資源 list;未執行操作。')
568
580
  if output_format and argv[:2] == ['functions', 'call']:
569
581
  require(output_format == 'json', 'UNSUPPORTED_FORMAT', 'Functions call 目前只支援 JSON 回應。')
570
582
  from . import public_access
571
- if provider == 'gcloud-fufu' and argv and argv[0] in {'scheduler', 'tasks', 'logging', 'compute', 'sql', 'artifacts', 'dns', 'eventarc'}:
583
+ if provider == 'gcloud-fufu' and argv and argv[0] in {'scheduler', 'tasks', 'logging', 'compute', 'sql', 'artifacts', 'dns', 'eventarc', 'container', 'redis'}:
572
584
  from .gcp_infra import command
573
585
  emit(command(argv, Parser)); return
574
586
  if provider == 'aws-fufu' and argv[:1] == ['secretsmanager'] or provider == 'gcloud-fufu' and argv[:1] == ['secrets']:
@@ -588,6 +600,15 @@ def entry(provider, argv=None):
588
600
  if provider == 'aws-fufu' and argv and argv[0] == 'serverless':
589
601
  from .serverless import command
590
602
  emit(command(argv[1:], Parser)); return
603
+ if provider=='fufu' and argv and argv[0]=='observability':
604
+ from .observability import command
605
+ emit(command(argv[1:],Parser));return
606
+ if provider=='aws-fufu' and argv[:2]==['logs','tail']:
607
+ from .observability import aws_tail
608
+ aws_tail(argv[2:],Parser);return
609
+ if provider=='gcloud-fufu' and (argv[:4]==['run','services','logs','read'] or argv[:3]==['functions','logs','read']):
610
+ from .observability import service_logs
611
+ emit(service_logs(argv,Parser));return
591
612
  if provider == 'fufu' and argv and argv[0] in {'gateway', 'events', 'mock'}:
592
613
  from .control import fufu_control
593
614
  emit(fufu_control(argv, Parser)); return
@@ -609,7 +630,10 @@ def entry(provider, argv=None):
609
630
  if provider == 'az-fufu' and argv and argv[0] == 'functionapp':
610
631
  raise FufuError('CAPABILITY_NOT_SUPPORTED', 'Azure Functions runtime 尚未實作。', 2)
611
632
  remote = {"aws-fufu": "aws", "gcloud-fufu": "gcp", "az-fufu": "azure"}
612
- if provider == 'aws-fufu' and argv and argv[0] in {'s3', 'sqs', 'ssm'}:
633
+ if provider == 'aws-fufu' and argv[:1] == ['s3']:
634
+ from .s3_highlevel import command
635
+ command(argv[1:], Parser); return
636
+ if provider == 'aws-fufu' and argv and argv[0] in {'sqs', 'ssm'}:
613
637
  from .cloud_data import aws_data_command
614
638
  emit(aws_data_command(argv, Parser))
615
639
  return
@@ -28,7 +28,7 @@ def s3_path(value):
28
28
  p = urlsplit(value)
29
29
  require(p.scheme == 's3' and p.netloc and not p.query and not p.fragment and not p.username and not p.password,
30
30
  'INVALID_RESOURCE', '請使用 s3://bucket 或 s3://bucket/key。')
31
- return '/s3/buckets/' + segment(p.netloc), p.path.lstrip('/')
31
+ return '/s3/buckets/' + segment(p.netloc), p.path.removeprefix('/')
32
32
 
33
33
 
34
34
  def aws_data_command(argv, Parser):
@@ -27,7 +27,7 @@ AWS_OPERATIONS = {
27
27
  'BatchGetItem': 'RequestItems', 'BatchWriteItem': 'RequestItems',
28
28
  },
29
29
  's3api': {
30
- 'CreateBucket': 'Bucket CreateBucketConfiguration', 'DeleteBucket': 'Bucket', 'ListBuckets': '', 'HeadBucket': 'Bucket',
30
+ 'CreateBucket': 'Bucket CreateBucketConfiguration', 'DeleteBucket': 'Bucket', 'ListBuckets': '', 'HeadBucket': 'Bucket', 'GetBucketLocation': 'Bucket',
31
31
  'ListObjectsV2': 'Bucket Prefix MaxKeys ContinuationToken Delimiter StartAfter EncodingType', 'PutObject': 'Bucket Key Body ContentType Metadata',
32
32
  'GetObject': 'Bucket Key ChecksumMode Range IfMatch IfNoneMatch', 'HeadObject': 'Bucket Key ChecksumMode IfMatch IfNoneMatch', 'DeleteObject': 'Bucket Key',
33
33
  },
@@ -134,7 +134,7 @@ def command(argv, Parser):
134
134
  params.setdefault('Type', 'String')
135
135
  if a.service == 'sqs' and 'QueueUrl' in params:
136
136
  require(isinstance(params['QueueUrl'], str) and re.fullmatch(r'(?:fufu://sqs/|https://sqs\.[a-z0-9-]+\.fufu\.invalid/[0-9]{12}/)[A-Za-z0-9_-]+(?:\.fifo)?', params['QueueUrl']) and len(params['QueueUrl'].rsplit('/', 1)[-1]) <= 80, 'INVALID_RESOURCE', 'QueueUrl 必須是 FUFU 租戶資源識別。')
137
- with ApiClient('aws', read_only=bool(re.match(r'^(Get|List|Describe|Query|Scan|BatchGet|Simulate|Head)', a.native_operation))) as api:
137
+ with ApiClient('aws', read_only=bool(re.match(r'^(Get|List|Describe|Query|Scan|BatchGet|Simulate|Head|FilterLogEvents)', a.native_operation))) as api:
138
138
  if a.service == 's3api' and a.native_operation == 'PutObject' and isinstance(params.get('Body'), Path):
139
139
  from .object_transfer import upload_file
140
140
  return upload_file(api, params['Body'], params.get('Bucket'), params.get('Key'), content_type=params.get('ContentType'), metadata=params.get('Metadata'))
@@ -147,6 +147,7 @@ class Backend:
147
147
  self.network = f"fufu-cli-{self.scope}"
148
148
  self.ingress_network = f"{self.network}-ingress"
149
149
  self.state_file = self.path / "state.json"
150
+ self.before_remove = None
150
151
 
151
152
  @contextlib.contextmanager
152
153
  def locked(self):
@@ -294,6 +295,7 @@ class Backend:
294
295
  supplied.update(secrets)
295
296
  supplied.update(runtime_env or {})
296
297
  supplied["AWS_EC2_METADATA_DISABLED"] = "true"
298
+ supplied['PYTHONUNBUFFERED'] = '1'
297
299
  if kind == "aws-lambda-sync":
298
300
  supplied.update(AWS_LAMBDA_FUNCTION_NAME=service, AWS_REGION=(runtime_env or {}).get('AWS_REGION', 'us-east-1'),
299
301
  AWS_DEFAULT_REGION=(runtime_env or {}).get('AWS_REGION', 'us-east-1'))
@@ -306,12 +308,13 @@ class Backend:
306
308
  "--cap-drop", "ALL", "--security-opt", "no-new-privileges:true",
307
309
  "--pids-limit", "1024", "--ulimit", "nofile=4096:4096", "--memory", str(allocation['memoryMiB'])+'m',
308
310
  "--memory-swap", str(allocation['memoryMiB'])+'m', "--cpus", str(allocation['cpu']),
309
- "--log-opt", "max-size=5m", "--log-opt", "max-file=1"]
311
+ "--log-driver", "json-file", "--log-opt", "max-size=50m", "--log-opt", "max-file=3"]
310
312
  for k in sorted(supplied):
311
313
  args.extend(["--env", k])
312
314
  args.append(image_id)
313
315
  args.extend(command or [])
314
316
  candidate = None
317
+ record = None
315
318
  ingress_candidate = None
316
319
  committed = False
317
320
  try:
@@ -376,6 +379,7 @@ class Backend:
376
379
  try:
377
380
  self.owned(old["container_id"])
378
381
  self.owned(old["ingress_id"])
382
+ if self.before_remove: self.before_remove(old)
379
383
  docker("container", "rm", "--force", old["ingress_id"], old["container_id"])
380
384
  self.cleanup_workload_network(old)
381
385
  except FufuError:
@@ -392,6 +396,9 @@ class Backend:
392
396
  self.owned(ingress_candidate)
393
397
  docker("container", "rm", "--force", ingress_candidate)
394
398
  self.owned(candidate)
399
+ # Startup failures must retain their final output for debugging.
400
+ # If the spool is full, the owned candidate remains available for retry.
401
+ if self.before_remove and record: self.before_remove(record)
395
402
  docker("container", "rm", "--force", candidate)
396
403
  if gateway: self.cleanup_workload_network({'network': network, 'gateway_id': gateway['id']})
397
404
 
@@ -458,6 +465,7 @@ class Backend:
458
465
  require(r is not None, "NOT_FOUND", "此 workspace 沒有該服務。")
459
466
  self.owned(r["container_id"])
460
467
  self.owned(r["ingress_id"])
468
+ if self.before_remove: self.before_remove(r)
461
469
  docker("container", "rm", "--force", r["ingress_id"], r["container_id"])
462
470
  del state["services"][key]
463
471
  self.save_state(state)
@@ -481,7 +489,7 @@ class Backend:
481
489
  docker("network", "rm", network)
482
490
  return {"workspace": self.workspace, "removed_containers": len(ids)}
483
491
 
484
- def invoke(self, kind, service, payload, *, path="/", timeout=None, expected_service_id=None):
492
+ def invoke(self, kind, service, payload, *, path="/", timeout=None, expected_service_id=None, trace_headers=None):
485
493
  limits = compute_limits(kind)
486
494
  if timeout is None: timeout = limits['timeoutSeconds']
487
495
  require(1 <= timeout <= limits["timeoutSeconds"], "INVALID_TIMEOUT", "請求期限超過此服務上限。")
@@ -499,7 +507,11 @@ class Backend:
499
507
  return None
500
508
 
501
509
  opener = request.build_opener(request.ProxyHandler({}), NoRedirect())
502
- req = request.Request(data["url"] + route, data=payload, headers={"Content-Type": "application/json"})
510
+ telemetry = dict(trace_headers or {})
511
+ require(set(telemetry) <= {'traceparent','x-cloud-trace-context','x-amzn-trace-id','x-request-id'}
512
+ and all(isinstance(v,str) and len(v)<256 and not re.search(r'[\r\n\x00]',v) for v in telemetry.values()),
513
+ 'INVALID_INPUT','Invalid trace context.')
514
+ req = request.Request(data["url"] + route, data=payload, headers={"Content-Type": "application/json", **telemetry})
503
515
  try:
504
516
  try:
505
517
  response = opener.open(req, timeout=timeout)
@@ -135,7 +135,7 @@ def jobs_command(argv, Parser):
135
135
 
136
136
 
137
137
  def logging_command(argv, Parser):
138
- p = Parser(prog='gcloud-fufu logging', description='持久化本機日誌;保留最近 10,000 筆,不含正式 Log Router、retention 或全容器 stdout。')
138
+ p = Parser(prog='gcloud-fufu logging', description='持久化日誌、容器 stdout/stderr、查詢及保留政策;Log Router sinks 與 alerts 尚未支援。')
139
139
  actions = p.add_subparsers(dest='action', required=True)
140
140
  q = actions.add_parser('write'); q.add_argument('log'); q.add_argument('message'); q.add_argument('--payload-type', choices=['text', 'json'], default='text'); q.add_argument('--severity', default='DEFAULT')
141
141
  q = actions.add_parser('read'); q.add_argument('filter', nargs='?', default=''); q.add_argument('--limit', type=int, default=50); q.add_argument('--order', choices=['asc', 'desc'], default='desc')
@@ -146,8 +146,8 @@ def logging_command(argv, Parser):
146
146
  project = 'projects/' + api.whoami()['projectId']
147
147
  if a.action == 'logs' and a.logs_action == 'list': return all_pages(api, 'logging', 'logs-list', {'parent': project}, 'logNames')
148
148
  if a.action == 'read':
149
- require(1 <= a.limit <= 10000, 'INVALID_LIMIT', '日誌 limit 必須介於 1–10000。')
150
- return all_pages(api, 'logging', 'read', {'resourceNames': [project], 'filter': a.filter, 'orderBy': 'timestamp ' + a.order, 'pageSize': min(a.limit, 1000)}, 'entries')[:a.limit]
149
+ from .observability import read_gcp_logs
150
+ return read_gcp_logs(api, {'resourceNames': [project], 'filter': a.filter, 'orderBy': 'timestamp ' + a.order}, a.limit)
151
151
  name = a.log if a.log.startswith('projects/') else project + '/logs/' + a.log
152
152
  if a.action == 'logs': return call(api, 'logging', 'logs-delete', {'logName': name})
153
153
  entry = {'severity': a.severity, 'jsonPayload' if a.payload_type == 'json' else 'textPayload': json.loads(a.message) if a.payload_type == 'json' else a.message}
@@ -166,6 +166,14 @@ MOCK_GROUPS = {
166
166
  ('compute', 'os-config', 'os-policy-assignments'): ('vm-policies', {'zone': 'zone', 'description': 'description'}),
167
167
  ('compute', 'networks', 'vpc-access', 'connectors'): ('vpc-connectors', {'network': 'network', 'range': 'range', 'min-instances': 'minInstances', 'max-instances': 'maxInstances'}),
168
168
  ('eventarc', 'triggers'): ('eventarc-triggers', {'location': 'location', 'destination-run-service': 'destination', 'event-filters': 'eventType', 'service-account': 'serviceAccount'}),
169
+ ('container', 'clusters'): ('gke-clusters', {'location': 'location', 'network': 'network', 'subnetwork': 'subnetwork', 'machine-type': 'machineType', 'num-nodes': 'numNodes', 'release-channel': 'releaseChannel', 'enable-private-nodes': 'enablePrivateNodes'}),
170
+ ('compute', 'firewall-rules'): ('firewall-rules', {'network': 'network', 'direction': 'direction', 'priority': 'priority', 'allow': 'allow', 'deny': 'deny', 'source-ranges': 'sourceRanges', 'destination-ranges': 'destinationRanges', 'target-tags': 'targetTags', 'disabled': 'disabled', 'description': 'description'}),
171
+ ('compute', 'health-checks'): ('health-checks', {'port': 'port', 'request-path': 'requestPath', 'check-interval': 'checkIntervalSec', 'timeout': 'timeoutSec', 'healthy-threshold': 'healthyThreshold', 'unhealthy-threshold': 'unhealthyThreshold', 'description': 'description'}),
172
+ ('compute', 'backend-services'): ('backend-services', {'protocol': 'protocol', 'health-checks': 'healthChecks', 'port-name': 'portName', 'timeout': 'timeoutSec', 'load-balancing-scheme': 'loadBalancingScheme', 'description': 'description'}),
173
+ ('compute', 'url-maps'): ('url-maps', {'default-service': 'defaultService', 'description': 'description'}),
174
+ ('compute', 'target-http-proxies'): ('target-http-proxies', {'url-map': 'urlMap', 'description': 'description'}),
175
+ ('compute', 'forwarding-rules'): ('forwarding-rules', {'target-http-proxy': 'targetHttpProxy', 'ports': 'ports', 'address': 'address', 'ip-protocol': 'ipProtocol', 'load-balancing-scheme': 'loadBalancingScheme', 'description': 'description'}),
176
+ ('redis', 'instances'): ('redis-instances', {'network': 'network', 'size': 'sizeGb', 'tier': 'tier', 'redis-version': 'redisVersion', 'display-name': 'displayName'}),
169
177
  }
170
178
 
171
179
 
@@ -176,18 +184,43 @@ def mock_command(argv, Parser):
176
184
  p = Parser(prog='gcloud-fufu ' + ' '.join(match), description='MOCK ONLY:只儲存配置,不建立或啟動真正資源。')
177
185
  actions = p.add_subparsers(dest='action', required=True)
178
186
  for action in ['create', 'update', 'describe', 'list', 'delete']:
187
+ if action == 'update' and service in {'url-maps', 'forwarding-rules'}: continue
179
188
  q = actions.add_parser(action)
180
- if action != 'list': q.add_argument('name')
181
- if action in {'create', 'update'}:
182
- for flag, field in flags.items(): q.add_argument('--' + flag, dest=field, type=int if field in {'bootDiskSizeGb', 'storageSizeGb', 'mtu', 'minInstances', 'maxInstances'} else str)
189
+ if service == 'health-checks' and action in {'create', 'update'}:
190
+ protocols = q.add_subparsers(dest='protocol', required=True)
191
+ targets = [protocols.add_parser(protocol) for protocol in ['http', 'https', 'tcp']]
192
+ else: targets = [q]
193
+ for q in targets:
194
+ if service in {'health-checks', 'backend-services', 'url-maps', 'target-http-proxies', 'forwarding-rules'}:
195
+ q.add_argument('--global', dest='global_scope', action='store_true')
196
+ if action != 'list': q.add_argument('name')
197
+ if action in {'create', 'update'}:
198
+ for flag, field in flags.items():
199
+ if service == 'health-checks' and field == 'requestPath' and q.prog.endswith(' tcp'): continue
200
+ if field in {'disabled', 'enablePrivateNodes'}:
201
+ group = q.add_mutually_exclusive_group()
202
+ group.add_argument('--' + flag, dest=field, action='store_true', default=None)
203
+ group.add_argument('--no-' + flag, dest=field, action='store_false')
204
+ else: q.add_argument('--' + flag, dest=field, type=int if field in {'bootDiskSizeGb', 'storageSizeGb', 'mtu', 'minInstances', 'maxInstances', 'numNodes', 'priority', 'port', 'healthyThreshold', 'unhealthyThreshold', 'sizeGb'} else str)
183
205
  a = p.parse_args(argv[len(match):])
184
- sys.stderr.write('FUFU MOCK ONLY: configuration is persisted; no VM, database, registry, DNS or network is provisioned.\n')
206
+ sys.stderr.write('FUFU MOCK ONLY: configuration is persisted; no infrastructure is provisioned or executed.\n')
185
207
  with ApiClient('gcp', read_only=a.action in {'list', 'describe'}) as api:
186
208
  if a.action == 'list': return all_pages(api, service, 'list', {}, 'items')
187
209
  params = {'name': a.name}
188
210
  if a.action in {'create', 'update'}:
189
- config = {field: getattr(a, field) for field in flags.values() if getattr(a, field) is not None}
190
- if service in {'sql-instances', 'subnets', 'vpc-connectors'}: config['region'] = os.environ.get('FUFU_SELECTED_REGION') or api.whoami()['region']
211
+ config = {field: getattr(a, field) for field in flags.values() if getattr(a, field, None) is not None}
212
+ for field in ['timeoutSec', 'checkIntervalSec']:
213
+ if field in config:
214
+ import re
215
+ value = str(config[field])
216
+ require(re.fullmatch(r'[1-9][0-9]*(?:s)?', value), 'UNSUPPORTED_ARGUMENT', '配置子集的期限使用整數秒或 s。')
217
+ config[field] = int(value.removesuffix('s'))
218
+ if service == 'health-checks': config['protocol'] = a.protocol.upper()
219
+ if service == 'redis-instances' and 'tier' in config:
220
+ require(config['tier'].lower() in {'basic', 'standard'}, 'UNSUPPORTED_ARGUMENT', 'Redis tier 使用 basic 或 standard。')
221
+ config['tier'] = 'BASIC' if config['tier'].lower() == 'basic' else 'STANDARD_HA'
222
+ if service in {'sql-instances', 'subnets', 'vpc-connectors', 'redis-instances'}: config['region'] = os.environ.get('FUFU_SELECTED_REGION') or api.whoami()['region']
223
+ if service == 'gke-clusters' and a.action == 'create': config.setdefault('location', os.environ.get('FUFU_SELECTED_REGION') or api.whoami()['region'])
191
224
  params['config'] = config
192
225
  return call(api, service, 'get' if a.action == 'describe' else a.action, params)
193
226