fufu-cloud-cli 0.9.6__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.6/src/fufu_cloud_cli.egg-info → fufu_cloud_cli-0.9.8}/PKG-INFO +40 -4
  2. {fufu_cloud_cli-0.9.6 → fufu_cloud_cli-0.9.8}/README.md +39 -3
  3. {fufu_cloud_cli-0.9.6 → fufu_cloud_cli-0.9.8}/pyproject.toml +1 -1
  4. {fufu_cloud_cli-0.9.6 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/__init__.py +1 -1
  5. {fufu_cloud_cli-0.9.6 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/api_backend.py +8 -0
  6. {fufu_cloud_cli-0.9.6 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/aws_errors.py +1 -1
  7. fufu_cloud_cli-0.9.8/src/fufu_cloud_cli/aws_lambda.py +125 -0
  8. {fufu_cloud_cli-0.9.6 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/aws_output.py +9 -1
  9. {fufu_cloud_cli-0.9.6 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/capabilities.json +55 -19
  10. {fufu_cloud_cli-0.9.6 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/cli.py +59 -14
  11. {fufu_cloud_cli-0.9.6 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/cloud_data.py +1 -1
  12. {fufu_cloud_cli-0.9.6 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/control.py +2 -2
  13. {fufu_cloud_cli-0.9.6 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/docker_backend.py +23 -6
  14. {fufu_cloud_cli-0.9.6 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/gcp_infra.py +42 -9
  15. {fufu_cloud_cli-0.9.6 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/gcs.py +95 -6
  16. {fufu_cloud_cli-0.9.6 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/lambda_adapter.py +17 -0
  17. {fufu_cloud_cli-0.9.6 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/limits.py +1 -0
  18. {fufu_cloud_cli-0.9.6 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/object_transfer.py +11 -6
  19. fufu_cloud_cli-0.9.8/src/fufu_cloud_cli/observability.py +127 -0
  20. {fufu_cloud_cli-0.9.6 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/remote_compute.py +2 -1
  21. {fufu_cloud_cli-0.9.6 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/runtime_sdk.py +1 -1
  22. {fufu_cloud_cli-0.9.6 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/terraform_bridge/runner.py +15 -1
  23. {fufu_cloud_cli-0.9.6 → fufu_cloud_cli-0.9.8/src/fufu_cloud_cli.egg-info}/PKG-INFO +40 -4
  24. {fufu_cloud_cli-0.9.6 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli.egg-info/SOURCES.txt +2 -0
  25. {fufu_cloud_cli-0.9.6 → fufu_cloud_cli-0.9.8}/LICENSE +0 -0
  26. {fufu_cloud_cli-0.9.6 → fufu_cloud_cli-0.9.8}/MANIFEST.in +0 -0
  27. {fufu_cloud_cli-0.9.6 → fufu_cloud_cli-0.9.8}/setup.cfg +0 -0
  28. {fufu_cloud_cli-0.9.6 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/__main__.py +0 -0
  29. {fufu_cloud_cli-0.9.6 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/bigquery.py +0 -0
  30. {fufu_cloud_cli-0.9.6 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/capacity.py +0 -0
  31. {fufu_cloud_cli-0.9.6 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/cloudflare.py +0 -0
  32. {fufu_cloud_cli-0.9.6 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/errors.py +0 -0
  33. {fufu_cloud_cli-0.9.6 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/gcloud_environment.py +0 -0
  34. {fufu_cloud_cli-0.9.6 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/gcloud_output.py +0 -0
  35. {fufu_cloud_cli-0.9.6 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/gcp_sdk.py +0 -0
  36. {fufu_cloud_cli-0.9.6 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/http_runtime.py +0 -0
  37. {fufu_cloud_cli-0.9.6 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/iac_state.py +0 -0
  38. {fufu_cloud_cli-0.9.6 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/manifest.py +0 -0
  39. {fufu_cloud_cli-0.9.6 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/public_access.py +0 -0
  40. {fufu_cloud_cli-0.9.6 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/pubsub.py +0 -0
  41. {fufu_cloud_cli-0.9.6 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/pulumi-endpoints.json +0 -0
  42. {fufu_cloud_cli-0.9.6 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/pulumi_bridge.py +0 -0
  43. {fufu_cloud_cli-0.9.6 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/runtime/functions/Dockerfile +0 -0
  44. {fufu_cloud_cli-0.9.6 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/runtime/ingress/Dockerfile +0 -0
  45. {fufu_cloud_cli-0.9.6 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/runtime/ingress/proxy.py +0 -0
  46. {fufu_cloud_cli-0.9.6 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/secrets.py +0 -0
  47. {fufu_cloud_cli-0.9.6 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/serverless.py +0 -0
  48. {fufu_cloud_cli-0.9.6 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/terraform_bridge/__init__.py +0 -0
  49. {fufu_cloud_cli-0.9.6 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/terraform_bridge/aws.py +0 -0
  50. {fufu_cloud_cli-0.9.6 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/terraform_bridge/aws_data.py +0 -0
  51. {fufu_cloud_cli-0.9.6 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/terraform_bridge/common.py +0 -0
  52. {fufu_cloud_cli-0.9.6 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/terraform_bridge/google.py +0 -0
  53. {fufu_cloud_cli-0.9.6 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/terraform_bridge/google_pubsub.py +0 -0
  54. {fufu_cloud_cli-0.9.6 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/terraform_bridge/remote_state.py +0 -0
  55. {fufu_cloud_cli-0.9.6 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/vendor/AWS-CLI-LICENSE.txt +0 -0
  56. {fufu_cloud_cli-0.9.6 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/vendor/__init__.py +0 -0
  57. {fufu_cloud_cli-0.9.6 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/vendor/aws_text.py +0 -0
  58. {fufu_cloud_cli-0.9.6 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli.egg-info/dependency_links.txt +0 -0
  59. {fufu_cloud_cli-0.9.6 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli.egg-info/entry_points.txt +0 -0
  60. {fufu_cloud_cli-0.9.6 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli.egg-info/requires.txt +0 -0
  61. {fufu_cloud_cli-0.9.6 → 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.6
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,41 @@ 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
+
56
+ 0.9.7 補齊 Lambda 五種 `wait` 指令、設定/程式碼更新的 `--revision-id`,
57
+ 以及常用設定的 `--cli-input-json` 和 `Variables={KEY=value}` shorthand。
58
+ 過期 RevisionId 回 `PreconditionFailedException`,保留原部署;CLI 仍等待
59
+ 部署完成才返回,waiter 有有限次數與失敗退出碼 255。程式碼雜湊、大小、
60
+ 最新修改時間與執行架構來自實際部署;預設記憶體為 128 MiB、期限 3 秒。
61
+ 需要配套 Console/compute worker 更新。JSON 只接受明列欄位,ZIP 另以
62
+ `--zip-file fileb://...` 傳入;不支援 Code.S3Bucket、Layers、Image、版本
63
+ 或 aliases。Function URL 的無效 AWS 簽名與匿名呼叫回 403。
64
+
30
65
  0.9.6 新增 AWS Lambda Event/DryRun、configuration 更新、JMESPath `--query`、
31
66
  json/text/yaml 輸出、SQS FIFO/batch、SSM path/history、DynamoDB Binary
32
67
  及 CloudWatch Logs。需要配套後端;Event 202 只表示持久化接受,並非 handler
@@ -53,8 +88,9 @@ fufu infrastructure catalog
53
88
  ```
54
89
 
55
90
  BigQuery 是 tenant emulator 的 GoogleSQL 子集,無 DML/DDL、load/export、
56
- 外部表與效能/費用等價。Logging 保留每租戶最新 10,000 筆,不自動收集所有
57
- 容器 stdout。Tasks 每 queue 每秒最多 1 次、每租戶 2 個同時投遞;耗盡 task
91
+ 外部表與效能/費用等價。配套新版 Logging 預設每租戶 2 GiB;GCP 保留
92
+ 30 天,AWS 依 log group retention 設定,未設定時不自動到期。只收集已
93
+ 驗證歸屬的 FUFU 工作負載容器。Tasks 每 queue 每秒最多 1 次、每租戶 2 個同時投遞;耗盡 task
58
94
  留待檢查,與官方刪除策略不同。新的 dependency flow 不代表完整應用都能遷移。
59
95
 
60
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 格式可讀舊資料,回滾須還原升級前備份。
@@ -71,7 +107,7 @@ Python 預設識別而遇到 Cloudflare HTTP 403/1010。識別標頭不取代
71
107
  ## Cloudflare 前端部署與 R2
72
108
 
73
109
  ```bash
74
- pip install 'fufu-cloud-cli==0.9.6'
110
+ pip install 'fufu-cloud-cli==0.9.8'
75
111
  export FUFU_CLOUDFLARE_CREDENTIAL_FILE=/run/secrets/fufu-cloudflare.json
76
112
  wrangler-fufu capabilities
77
113
  wrangler-fufu pages project create frontend --production-branch main
@@ -4,6 +4,41 @@
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
+
33
+ 0.9.7 補齊 Lambda 五種 `wait` 指令、設定/程式碼更新的 `--revision-id`,
34
+ 以及常用設定的 `--cli-input-json` 和 `Variables={KEY=value}` shorthand。
35
+ 過期 RevisionId 回 `PreconditionFailedException`,保留原部署;CLI 仍等待
36
+ 部署完成才返回,waiter 有有限次數與失敗退出碼 255。程式碼雜湊、大小、
37
+ 最新修改時間與執行架構來自實際部署;預設記憶體為 128 MiB、期限 3 秒。
38
+ 需要配套 Console/compute worker 更新。JSON 只接受明列欄位,ZIP 另以
39
+ `--zip-file fileb://...` 傳入;不支援 Code.S3Bucket、Layers、Image、版本
40
+ 或 aliases。Function URL 的無效 AWS 簽名與匿名呼叫回 403。
41
+
7
42
  0.9.6 新增 AWS Lambda Event/DryRun、configuration 更新、JMESPath `--query`、
8
43
  json/text/yaml 輸出、SQS FIFO/batch、SSM path/history、DynamoDB Binary
9
44
  及 CloudWatch Logs。需要配套後端;Event 202 只表示持久化接受,並非 handler
@@ -30,8 +65,9 @@ fufu infrastructure catalog
30
65
  ```
31
66
 
32
67
  BigQuery 是 tenant emulator 的 GoogleSQL 子集,無 DML/DDL、load/export、
33
- 外部表與效能/費用等價。Logging 保留每租戶最新 10,000 筆,不自動收集所有
34
- 容器 stdout。Tasks 每 queue 每秒最多 1 次、每租戶 2 個同時投遞;耗盡 task
68
+ 外部表與效能/費用等價。配套新版 Logging 預設每租戶 2 GiB;GCP 保留
69
+ 30 天,AWS 依 log group retention 設定,未設定時不自動到期。只收集已
70
+ 驗證歸屬的 FUFU 工作負載容器。Tasks 每 queue 每秒最多 1 次、每租戶 2 個同時投遞;耗盡 task
35
71
  留待檢查,與官方刪除策略不同。新的 dependency flow 不代表完整應用都能遷移。
36
72
 
37
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 格式可讀舊資料,回滾須還原升級前備份。
@@ -48,7 +84,7 @@ Python 預設識別而遇到 Cloudflare HTTP 403/1010。識別標頭不取代
48
84
  ## Cloudflare 前端部署與 R2
49
85
 
50
86
  ```bash
51
- pip install 'fufu-cloud-cli==0.9.6'
87
+ pip install 'fufu-cloud-cli==0.9.8'
52
88
  export FUFU_CLOUDFLARE_CREDENTIAL_FILE=/run/secrets/fufu-cloudflare.json
53
89
  wrangler-fufu capabilities
54
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.6"
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.6"
3
+ __version__ = "0.9.8"
@@ -184,6 +184,14 @@ class ApiClient:
184
184
  return json.loads(raw)
185
185
  except HTTPError as exc:
186
186
  status = exc.code
187
+ if self.provider == 'aws' and '/compute/' in path and status in {400, 412}:
188
+ try:
189
+ value = json.loads(exc.read(4096))
190
+ code = value.get('error', {}).get('code') if isinstance(value, dict) and isinstance(value.get('error'), dict) else None
191
+ if code in {'PreconditionFailedException', 'InvalidRequestContentException'}:
192
+ raise FufuError(code, 'Lambda 操作遭拒;未輸出原始設定或錯誤內容。') from None
193
+ except (ValueError, OSError): pass
194
+ finally: exc.close()
187
195
  if self.provider == 'aws' and '/control/native' in path:
188
196
  from .aws_errors import AWS_ERRORS
189
197
  try:
@@ -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
@@ -0,0 +1,125 @@
1
+ """Lambda command inputs and bounded AWS waiter semantics for the supported subset."""
2
+ import json
3
+ import re
4
+ import time
5
+
6
+ from .aws_output import configuration
7
+ from .errors import FufuError, require
8
+
9
+
10
+ WAITERS = {
11
+ 'function-exists': ('describe', None, None, 1, 20),
12
+ 'function-active': ('configuration', 'State', 'Active', 5, 60),
13
+ 'function-active-v2': ('describe', 'State', 'Active', 1, 300),
14
+ 'function-updated': ('configuration', 'LastUpdateStatus', 'Successful', 5, 60),
15
+ 'function-updated-v2': ('describe', 'LastUpdateStatus', 'Successful', 1, 300),
16
+ }
17
+
18
+
19
+ def wait(backend, waiter, name):
20
+ method, field, success, delay, attempts = WAITERS[waiter]
21
+ deadline = time.monotonic() + delay * attempts
22
+ for attempt in range(attempts):
23
+ try:
24
+ value = configuration(getattr(backend, method)('aws-lambda-sync', name))
25
+ except FufuError as error:
26
+ if waiter != 'function-exists' or error.code not in {'NOT_FOUND', 'RESOURCE_NOT_FOUND', 'ResourceNotFoundException'}:
27
+ raise FufuError('WAITER_FAILED', '等待服務時查詢失敗;請確認權限與服務狀態。', 255) from None
28
+ else:
29
+ if field is None or value.get(field) == success: return
30
+ if value.get(field) == 'Failed':
31
+ raise FufuError('WAITER_FAILED', '服務回報失敗狀態;未達到等待條件。', 255)
32
+ remaining = deadline - time.monotonic()
33
+ if attempt + 1 == attempts or remaining <= 0: break
34
+ time.sleep(min(delay, remaining))
35
+ raise FufuError('WAITER_FAILED', '等待超過期限;服務操作可能仍在執行,請重新查詢。', 255)
36
+
37
+
38
+ def environment(value):
39
+ """Native JSON or Variables={KEY=value,...}; malformed/ambiguous input fails closed."""
40
+ from .control import read_json
41
+ if value.startswith(('{', 'file://')):
42
+ return read_json(value)
43
+ require(value.startswith('Variables={') and value.endswith('}'), 'INVALID_INPUT',
44
+ 'Environment 請使用 JSON、file:// JSON 或 Variables={KEY=value,...}。')
45
+ text = value[len('Variables={'):-1]
46
+ values = {}; position = 0
47
+ # Quotes and escaped commas are supported without shell execution or expansion.
48
+ pattern = re.compile(r'\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*')
49
+ while position < len(text):
50
+ matched = pattern.match(text, position)
51
+ require(matched is not None, 'INVALID_INPUT', 'Environment shorthand 格式無效。')
52
+ key = matched[1]; position = matched.end(); chars = []; quote = None
53
+ require(key not in values, 'INVALID_INPUT', 'Environment 不可重複指定相同變數。')
54
+ if position < len(text) and text[position] in {'"', "'"}:
55
+ quote = text[position]; position += 1
56
+ closed = quote is None
57
+ while position < len(text):
58
+ character = text[position]; position += 1
59
+ if character == '\\':
60
+ require(position < len(text), 'INVALID_INPUT', 'Environment 跳脫序列未完成。')
61
+ chars.append(text[position]); position += 1
62
+ elif quote and character == quote:
63
+ closed = True
64
+ while position < len(text) and text[position].isspace(): position += 1
65
+ require(position == len(text) or text[position] == ',', 'INVALID_INPUT', 'Environment 引號後有多餘內容。')
66
+ if position < len(text): position += 1
67
+ break
68
+ elif not quote and character == ',': break
69
+ else:
70
+ require(quote is not None or character not in {'{', '}', '[', ']', '"', "'"},
71
+ 'UNSUPPORTED_ARGUMENT', '複雜 Environment 值請使用 JSON 或引號。')
72
+ chars.append(character)
73
+ require(closed, 'INVALID_INPUT', 'Environment 引號未關閉。')
74
+ values[key] = ''.join(chars) if quote else ''.join(chars).rstrip()
75
+ require(position != len(text) or not text.endswith(','), 'INVALID_INPUT', 'Environment 不可有結尾逗號。')
76
+ return {'Variables': values}
77
+
78
+
79
+ FIELDS = {
80
+ 'FunctionName': ('--function-name', str), 'Runtime': ('--runtime', str),
81
+ 'Handler': ('--handler', str), 'Role': ('--role', str),
82
+ 'Environment': ('--environment', dict), 'MemorySize': ('--memory-size', int),
83
+ 'Timeout': ('--timeout', int), 'EphemeralStorage': ('--ephemeral-storage', dict),
84
+ 'RevisionId': ('--revision-id', str), 'InvocationType': ('--invocation-type', str),
85
+ 'Payload': ('--payload', str),
86
+ }
87
+ INPUTS = {
88
+ 'create-function': {'FunctionName', 'Runtime', 'Handler', 'Role', 'Environment', 'MemorySize', 'Timeout', 'EphemeralStorage'},
89
+ 'update-function-code': {'FunctionName', 'RevisionId'},
90
+ 'update-function-configuration': {'FunctionName', 'Role', 'Environment', 'MemorySize', 'Timeout', 'EphemeralStorage', 'RevisionId'},
91
+ 'get-function': {'FunctionName'}, 'get-function-configuration': {'FunctionName'},
92
+ 'delete-function': {'FunctionName'}, 'invoke': {'FunctionName', 'InvocationType', 'Payload'},
93
+ 'list-functions': set(),
94
+ }
95
+
96
+
97
+ def input_json(argv):
98
+ """Expand validated native JSON before argparse; explicit flags take precedence."""
99
+ from .control import read_json
100
+ if len(argv) < 2: return argv
101
+ rest = []; documents = []; index = 2
102
+ while index < len(argv):
103
+ flag, separator, value = argv[index].partition('=')
104
+ if flag == '--cli-input-json':
105
+ if not separator:
106
+ index += 1
107
+ require(index < len(argv), 'INVALID_INPUT', 'cli-input-json 缺少值。')
108
+ value = argv[index]
109
+ documents.append(value)
110
+ else: rest.append(argv[index])
111
+ index += 1
112
+ if not documents: return argv
113
+ require(len(documents) == 1 and argv[1] in INPUTS, 'UNSUPPORTED_ARGUMENT', '此操作不支援指定的 cli-input-json。')
114
+ value = read_json(documents[0])
115
+ require(isinstance(value, dict) and not value.keys() - INPUTS[argv[1]], 'UNSUPPORTED_ARGUMENT',
116
+ 'Lambda JSON 含未支援的欄位;ZIP 請另用 --zip-file fileb:// 傳入,未執行操作。')
117
+ explicit = {arg.partition('=')[0] for arg in rest if arg.startswith('--')}
118
+ # FUFU memory alias selects the same official field.
119
+ if '--memory' in explicit: explicit.add('--memory-size')
120
+ expanded = []
121
+ for key, item in value.items():
122
+ flag, expected = FIELDS[key]
123
+ require(type(item) is expected, 'INVALID_INPUT', 'Lambda JSON 欄位型別無效。')
124
+ if flag not in explicit: expanded.extend([flag + '=' + (json.dumps(item) if expected is dict else str(item))])
125
+ return argv[:2] + expanded + rest
@@ -1,6 +1,7 @@
1
1
  """AWS JMESPath and native text formatting over the supported response subset."""
2
2
  import io
3
3
  import json
4
+ import base64
4
5
 
5
6
  import jmespath
6
7
  from jmespath.exceptions import JMESPathError
@@ -25,10 +26,17 @@ def configuration(value):
25
26
  fields = {'FunctionName': value.get('name'), 'FunctionArn': value.get('functionArn'),
26
27
  'Runtime': value.get('runtime'), 'Handler': value.get('handler'), 'Role': value.get('role'),
27
28
  'MemorySize': resources.get('memoryMiB'), 'Timeout': resources.get('timeoutSeconds'),
28
- 'RevisionId': value.get('revision'), 'LastModified': value.get('createdAt')}
29
+ 'RevisionId': value.get('revision'), 'LastModified': value.get('lastModified', value.get('createdAt'))}
29
30
  result = {key: item for key, item in fields.items() if item is not None}
30
31
  if resources.get('tmpMiB') is not None: result['EphemeralStorage'] = {'Size': resources['tmpMiB']}
31
32
  if value.get('status') == 'running': result.update(State='Active', LastUpdateStatus='Successful')
33
+ if value.get('state'): result['State'] = value['state']
34
+ if value.get('lastUpdateStatus'): result['LastUpdateStatus'] = value['lastUpdateStatus']
35
+ result.update(Version='$LATEST', PackageType='Zip')
36
+ if value.get('architecture'): result['Architectures'] = [value['architecture']]
37
+ source = value.get('source', {})
38
+ if source.get('sha256'): result['CodeSha256'] = base64.b64encode(bytes.fromhex(source['sha256'])).decode()
39
+ if source.get('size') is not None: result['CodeSize'] = source['size']
32
40
  if 'env' in value: result['Environment'] = {'Variables': value['env']}
33
41
  result['Fufu'] = {key: value[key] for key in ['mode', 'backend', 'readiness', 'serviceId', 'publicAccess', 'iamEnforced'] if key in value}
34
42
  result['Fufu'].setdefault('mode', 'simplified')
@@ -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",