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.
- {fufu_cloud_cli-0.9.7/src/fufu_cloud_cli.egg-info → fufu_cloud_cli-0.9.8}/PKG-INFO +31 -4
- {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/README.md +30 -3
- {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/pyproject.toml +1 -1
- {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/__init__.py +1 -1
- {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/aws_errors.py +1 -1
- {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/capabilities.json +55 -19
- {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/cli.py +32 -8
- {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/cloud_data.py +1 -1
- {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/control.py +2 -2
- {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/docker_backend.py +15 -3
- {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/gcp_infra.py +42 -9
- {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/gcs.py +95 -6
- {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/lambda_adapter.py +17 -0
- {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/object_transfer.py +11 -6
- fufu_cloud_cli-0.9.8/src/fufu_cloud_cli/observability.py +127 -0
- {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/runtime_sdk.py +1 -1
- {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/terraform_bridge/runner.py +15 -1
- {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8/src/fufu_cloud_cli.egg-info}/PKG-INFO +31 -4
- {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli.egg-info/SOURCES.txt +1 -0
- {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/LICENSE +0 -0
- {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/MANIFEST.in +0 -0
- {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/setup.cfg +0 -0
- {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/__main__.py +0 -0
- {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/api_backend.py +0 -0
- {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/aws_lambda.py +0 -0
- {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/aws_output.py +0 -0
- {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/bigquery.py +0 -0
- {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/capacity.py +0 -0
- {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/cloudflare.py +0 -0
- {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/errors.py +0 -0
- {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/gcloud_environment.py +0 -0
- {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/gcloud_output.py +0 -0
- {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/gcp_sdk.py +0 -0
- {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/http_runtime.py +0 -0
- {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/iac_state.py +0 -0
- {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/limits.py +0 -0
- {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/manifest.py +0 -0
- {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/public_access.py +0 -0
- {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/pubsub.py +0 -0
- {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/pulumi-endpoints.json +0 -0
- {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/pulumi_bridge.py +0 -0
- {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/remote_compute.py +0 -0
- {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/runtime/functions/Dockerfile +0 -0
- {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/runtime/ingress/Dockerfile +0 -0
- {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/runtime/ingress/proxy.py +0 -0
- {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/secrets.py +0 -0
- {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/serverless.py +0 -0
- {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/terraform_bridge/__init__.py +0 -0
- {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/terraform_bridge/aws.py +0 -0
- {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/terraform_bridge/aws_data.py +0 -0
- {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/terraform_bridge/common.py +0 -0
- {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/terraform_bridge/google.py +0 -0
- {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/terraform_bridge/google_pubsub.py +0 -0
- {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/terraform_bridge/remote_state.py +0 -0
- {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/vendor/AWS-CLI-LICENSE.txt +0 -0
- {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/vendor/__init__.py +0 -0
- {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli/vendor/aws_text.py +0 -0
- {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli.egg-info/dependency_links.txt +0 -0
- {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli.egg-info/entry_points.txt +0 -0
- {fufu_cloud_cli-0.9.7 → fufu_cloud_cli-0.9.8}/src/fufu_cloud_cli.egg-info/requires.txt +0 -0
- {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.
|
|
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
|
-
|
|
66
|
-
|
|
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.
|
|
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
|
-
|
|
43
|
-
|
|
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.
|
|
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
|
|
@@ -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": ["
|
|
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-
|
|
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
|
-
"
|
|
356
|
-
"
|
|
357
|
-
"
|
|
358
|
-
"no-sinks-
|
|
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": "
|
|
496
|
-
"
|
|
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": "
|
|
526
|
-
"
|
|
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": "
|
|
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": "
|
|
600
|
-
"
|
|
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": "
|
|
668
|
-
"reason": "
|
|
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
|
-
|
|
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:
|
|
554
|
-
|
|
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
|
|
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.
|
|
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=
|
|
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
|
-
|
|
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='
|
|
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
|
-
|
|
150
|
-
return
|
|
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
|
|
181
|
-
|
|
182
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|