fufu-cloud-cli 0.9.4__tar.gz → 0.9.5__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 (54) hide show
  1. {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.5}/PKG-INFO +40 -10
  2. fufu_cloud_cli-0.9.4/src/fufu_cloud_cli.egg-info/PKG-INFO → fufu_cloud_cli-0.9.5/README.md +35 -27
  3. {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.5}/pyproject.toml +3 -1
  4. {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.5}/src/fufu_cloud_cli/__init__.py +1 -1
  5. {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.5}/src/fufu_cloud_cli/__main__.py +1 -1
  6. fufu_cloud_cli-0.9.5/src/fufu_cloud_cli/bigquery.py +92 -0
  7. {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.5}/src/fufu_cloud_cli/capabilities.json +159 -18
  8. {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.5}/src/fufu_cloud_cli/cli.py +25 -3
  9. fufu_cloud_cli-0.9.5/src/fufu_cloud_cli/gcp_infra.py +222 -0
  10. fufu_cloud_cli-0.9.5/src/fufu_cloud_cli/gcp_sdk.py +60 -0
  11. fufu_cloud_cli-0.9.4/README.md → fufu_cloud_cli-0.9.5/src/fufu_cloud_cli.egg-info/PKG-INFO +57 -9
  12. {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.5}/src/fufu_cloud_cli.egg-info/SOURCES.txt +3 -0
  13. {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.5}/src/fufu_cloud_cli.egg-info/entry_points.txt +1 -0
  14. {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.5}/src/fufu_cloud_cli.egg-info/requires.txt +5 -0
  15. {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.5}/LICENSE +0 -0
  16. {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.5}/MANIFEST.in +0 -0
  17. {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.5}/setup.cfg +0 -0
  18. {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.5}/src/fufu_cloud_cli/api_backend.py +0 -0
  19. {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.5}/src/fufu_cloud_cli/capacity.py +0 -0
  20. {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.5}/src/fufu_cloud_cli/cloud_data.py +0 -0
  21. {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.5}/src/fufu_cloud_cli/cloudflare.py +0 -0
  22. {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.5}/src/fufu_cloud_cli/control.py +0 -0
  23. {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.5}/src/fufu_cloud_cli/docker_backend.py +0 -0
  24. {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.5}/src/fufu_cloud_cli/errors.py +0 -0
  25. {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.5}/src/fufu_cloud_cli/gcloud_environment.py +0 -0
  26. {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.5}/src/fufu_cloud_cli/gcloud_output.py +0 -0
  27. {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.5}/src/fufu_cloud_cli/gcs.py +0 -0
  28. {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.5}/src/fufu_cloud_cli/http_runtime.py +0 -0
  29. {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.5}/src/fufu_cloud_cli/iac_state.py +0 -0
  30. {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.5}/src/fufu_cloud_cli/lambda_adapter.py +0 -0
  31. {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.5}/src/fufu_cloud_cli/limits.py +0 -0
  32. {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.5}/src/fufu_cloud_cli/manifest.py +0 -0
  33. {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.5}/src/fufu_cloud_cli/object_transfer.py +0 -0
  34. {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.5}/src/fufu_cloud_cli/public_access.py +0 -0
  35. {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.5}/src/fufu_cloud_cli/pubsub.py +0 -0
  36. {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.5}/src/fufu_cloud_cli/pulumi-endpoints.json +0 -0
  37. {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.5}/src/fufu_cloud_cli/pulumi_bridge.py +0 -0
  38. {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.5}/src/fufu_cloud_cli/remote_compute.py +0 -0
  39. {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.5}/src/fufu_cloud_cli/runtime/functions/Dockerfile +0 -0
  40. {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.5}/src/fufu_cloud_cli/runtime/ingress/Dockerfile +0 -0
  41. {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.5}/src/fufu_cloud_cli/runtime/ingress/proxy.py +0 -0
  42. {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.5}/src/fufu_cloud_cli/runtime_sdk.py +0 -0
  43. {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.5}/src/fufu_cloud_cli/secrets.py +0 -0
  44. {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.5}/src/fufu_cloud_cli/serverless.py +0 -0
  45. {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.5}/src/fufu_cloud_cli/terraform_bridge/__init__.py +0 -0
  46. {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.5}/src/fufu_cloud_cli/terraform_bridge/aws.py +0 -0
  47. {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.5}/src/fufu_cloud_cli/terraform_bridge/aws_data.py +0 -0
  48. {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.5}/src/fufu_cloud_cli/terraform_bridge/common.py +0 -0
  49. {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.5}/src/fufu_cloud_cli/terraform_bridge/google.py +0 -0
  50. {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.5}/src/fufu_cloud_cli/terraform_bridge/google_pubsub.py +0 -0
  51. {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.5}/src/fufu_cloud_cli/terraform_bridge/remote_state.py +0 -0
  52. {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.5}/src/fufu_cloud_cli/terraform_bridge/runner.py +0 -0
  53. {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.5}/src/fufu_cloud_cli.egg-info/dependency_links.txt +0 -0
  54. {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.5}/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.4
3
+ Version: 0.9.5
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
@@ -14,14 +14,43 @@ Provides-Extra: test
14
14
  Requires-Dist: boto3==1.40.66; extra == "test"
15
15
  Provides-Extra: terraform
16
16
  Requires-Dist: python-hcl2==7.3.1; extra == "terraform"
17
+ Provides-Extra: gcp
18
+ Requires-Dist: google-cloud-bigquery==3.38.0; extra == "gcp"
19
+ Requires-Dist: google-cloud-tasks==2.20.0; extra == "gcp"
20
+ Requires-Dist: google-cloud-logging==3.12.1; extra == "gcp"
17
21
  Dynamic: license-file
18
22
 
19
23
  # FUFU Cloud CLI
20
24
 
21
- 以五個指令操作 FUFU Cloud 的租戶隔離開發環境:`fufu`、`aws-fufu`、
22
- `gcloud-fufu`、`az-fufu`、`wrangler-fufu`。這是 AWS、Google Cloud、Azure 與 Cloudflare 的**明確子集**,
25
+ 以六個指令操作 FUFU Cloud 的租戶隔離開發環境:`fufu`、`aws-fufu`、
26
+ `gcloud-fufu`、`az-fufu`、`wrangler-fufu`、`bq-fufu`。這是 AWS、Google Cloud、Azure 與 Cloudflare 的**明確子集**,
23
27
  只供 FUFU mock 環境使用;不會把部署轉送到正式雲端。
24
28
 
29
+ 0.9.5 新增 `gcloud-fufu scheduler`、`tasks`、`logging` 與 `bq-fufu`。
30
+ 支援同租戶已登錄 HTTP 應用的排程/背景任務、BigQuery dataset/table、
31
+ insertAll/SELECT 與持久化日誌。`fufu_cloud_cli.gcp_sdk` 提供官方 Python
32
+ BigQuery、Cloud Tasks、Logging SDK 的明確 REST factory;可用 `[gcp]` extra
33
+ 安裝已驗收相依。需要對應的新後端,並非透明 ADC 或所有 Google SDK 替換。
34
+
35
+ `fufu infrastructure` 提供 14 類 typed configuration mock。Compute Engine、
36
+ Cloud SQL、App Engine、DNS、VPC、Artifact Registry、Cloud Build、Gemini、
37
+ VM Manager、Eventarc 等設定不會啟動 VM/SQL/registry/網路/推論。
38
+ UI 顯示僅設定,CLI stderr 保留 MOCK ONLY,不因輸出投影而隱藏。
39
+ 新服務尚未接入 Terraform/OpenTofu/Pulumi;單機背景投遞不是雲端 HA。
40
+
41
+ ```bash
42
+ gcloud-fufu scheduler jobs list
43
+ gcloud-fufu tasks queues list
44
+ bq-fufu query --use_legacy_sql=false 'SELECT 1 AS ready'
45
+ gcloud-fufu logging read 'severity>=ERROR'
46
+ fufu infrastructure catalog
47
+ ```
48
+
49
+ BigQuery 是 tenant emulator 的 GoogleSQL 子集,無 DML/DDL、load/export、
50
+ 外部表與效能/費用等價。Logging 保留每租戶最新 10,000 筆,不自動收集所有
51
+ 容器 stdout。Tasks 每 queue 每秒最多 1 次、每租戶 2 個同時投遞;耗盡 task
52
+ 留待檢查,與官方刪除策略不同。新的 dependency flow 不代表完整應用都能遷移。
53
+
25
54
  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 格式可讀舊資料,回滾須還原升級前備份。
26
55
 
27
56
  0.9.3 的常用輸出投影、清單篩選/排序/limit、Cloud Run 設定更新、Service Account 啟停及 Pub/Sub attributes/ACK deadline 持續保留。僅明列子集經過官方 gcloud、小用量真實 GCP 與隔離 Docker 驗收;不表示完整 Google IAM、SDK/ADC 或自動擴縮相容。
@@ -36,7 +65,7 @@ Python 預設識別而遇到 Cloudflare HTTP 403/1010。識別標頭不取代
36
65
  ## Cloudflare 前端部署與 R2
37
66
 
38
67
  ```bash
39
- pip install 'fufu-cloud-cli==0.9.4'
68
+ pip install 'fufu-cloud-cli==0.9.5'
40
69
  export FUFU_CLOUDFLARE_CREDENTIAL_FILE=/run/secrets/fufu-cloudflare.json
41
70
  wrangler-fufu capabilities
42
71
  wrangler-fufu pages project create frontend --production-branch main
@@ -80,7 +109,8 @@ pip install fufu-cloud-cli
80
109
  fufu --version
81
110
  ```
82
111
 
83
- npm 版本需要 Node.js 18 以上,以及已安裝的 Python 3.10 以上;各入口
112
+ npm registry 尚未發布;以下是 registry 可用後的安裝語法。目前可安裝經驗收
113
+ 的 npm tarball。npm 版本需要 Node.js 18 以上,以及已安裝的 Python 3.10 以上;各入口
84
114
  呼叫相同的 Python CLI。0.8.0 的 npm 建置包內附純 Python YAML/TOML parser,沒有安裝腳本,
85
115
  不會自行下載 Python 或執行 pip。
86
116
 
@@ -97,12 +127,12 @@ npx --no-install aws-fufu --help
97
127
 
98
128
  ## 憑證與 CI
99
129
 
100
- 目前工作樹另提供 `fufu tofu`、`fufu pulumi`、`fufu state`。
130
+ 目前版本另提供 `fufu tofu`、`fufu pulumi`、`fufu state`。
101
131
  Terraform/OpenTofu 可選擇租戶遠端 HTTP state,具備鎖、加密、版本與
102
132
  復原;需配套 Console API 和平台金鑰。Pulumi 使用固定官方 Node.js SDK,
103
133
  已驗收 SQS、SSM String、Pub/Sub topic/pull subscription,採獨立本機
104
- DIY backend,秘密欄位需外部 passphrase。這些新能力尚未包含在已發布
105
- 的 0.8.0,不能把套件安裝成功當成支援驗收。
134
+ DIY backend,秘密欄位需外部 passphrase。這些能力從 0.9.0 起納入 CLI,
135
+ 仍需要配套服務端;不能把套件安裝成功當成支援驗收。
106
136
 
107
137
  此工作樹也提供 `fufu terraform`:以官方 AWS/Google Terraform provider
108
138
  管理 Lambda ZIP、Cloud Run v2、IAM 與服務入口的明列子集,並使用獨立的
@@ -112,9 +142,9 @@ GCS/S3、Functions Gen 1/Gen 2 等一般 CLI 能力
112
142
  不表示官方 Terraform 資源已相容。正式環境使用正常 Terraform、正式認證
113
143
  與獨立 state,不可直接搬用 FUFU state。
114
144
 
115
- 後續開發版另接入 SQS queue、SSM String parameter、Pub/Sub topic/pull
145
+ 目前版本另接入 SQS queue、SSM String parameter、Pub/Sub topic/pull
116
146
  subscription;新增能力需要配套的 Console 與 Pub/Sub 後端更新,不能從
117
- 已發布的 0.8.0 安裝成功就推定已具備。`gcloud-fufu pubsub` 提供建立、
147
+ CLI 安裝成功就推定服務端已具備。`gcloud-fufu pubsub` 提供建立、
118
148
  查詢、更新、刪除、publish/pull/ack 子集;不支援 push、ordering、
119
149
  exactly-once、Function trigger 或保留期限保證。
120
150
 
@@ -1,27 +1,34 @@
1
- Metadata-Version: 2.4
2
- Name: fufu-cloud-cli
3
- Version: 0.9.4
4
- Summary: FUFU Cloud CLI for tenant-scoped mock cloud development and deployment
5
- License-Expression: LicenseRef-Proprietary
6
- Requires-Python: >=3.10
7
- Description-Content-Type: text/markdown
8
- License-File: LICENSE
9
- Requires-Dist: PyYAML==6.0.3
10
- Requires-Dist: tomli==2.2.1; python_version < "3.11"
11
- Provides-Extra: yaml
12
- Requires-Dist: PyYAML==6.0.3; extra == "yaml"
13
- Provides-Extra: test
14
- Requires-Dist: boto3==1.40.66; extra == "test"
15
- Provides-Extra: terraform
16
- Requires-Dist: python-hcl2==7.3.1; extra == "terraform"
17
- Dynamic: license-file
18
-
19
1
  # FUFU Cloud CLI
20
2
 
21
- 以五個指令操作 FUFU Cloud 的租戶隔離開發環境:`fufu`、`aws-fufu`、
22
- `gcloud-fufu`、`az-fufu`、`wrangler-fufu`。這是 AWS、Google Cloud、Azure 與 Cloudflare 的**明確子集**,
3
+ 以六個指令操作 FUFU Cloud 的租戶隔離開發環境:`fufu`、`aws-fufu`、
4
+ `gcloud-fufu`、`az-fufu`、`wrangler-fufu`、`bq-fufu`。這是 AWS、Google Cloud、Azure 與 Cloudflare 的**明確子集**,
23
5
  只供 FUFU mock 環境使用;不會把部署轉送到正式雲端。
24
6
 
7
+ 0.9.5 新增 `gcloud-fufu scheduler`、`tasks`、`logging` 與 `bq-fufu`。
8
+ 支援同租戶已登錄 HTTP 應用的排程/背景任務、BigQuery dataset/table、
9
+ insertAll/SELECT 與持久化日誌。`fufu_cloud_cli.gcp_sdk` 提供官方 Python
10
+ BigQuery、Cloud Tasks、Logging SDK 的明確 REST factory;可用 `[gcp]` extra
11
+ 安裝已驗收相依。需要對應的新後端,並非透明 ADC 或所有 Google SDK 替換。
12
+
13
+ `fufu infrastructure` 提供 14 類 typed configuration mock。Compute Engine、
14
+ Cloud SQL、App Engine、DNS、VPC、Artifact Registry、Cloud Build、Gemini、
15
+ VM Manager、Eventarc 等設定不會啟動 VM/SQL/registry/網路/推論。
16
+ UI 顯示僅設定,CLI stderr 保留 MOCK ONLY,不因輸出投影而隱藏。
17
+ 新服務尚未接入 Terraform/OpenTofu/Pulumi;單機背景投遞不是雲端 HA。
18
+
19
+ ```bash
20
+ gcloud-fufu scheduler jobs list
21
+ gcloud-fufu tasks queues list
22
+ bq-fufu query --use_legacy_sql=false 'SELECT 1 AS ready'
23
+ gcloud-fufu logging read 'severity>=ERROR'
24
+ fufu infrastructure catalog
25
+ ```
26
+
27
+ BigQuery 是 tenant emulator 的 GoogleSQL 子集,無 DML/DDL、load/export、
28
+ 外部表與效能/費用等價。Logging 保留每租戶最新 10,000 筆,不自動收集所有
29
+ 容器 stdout。Tasks 每 queue 每秒最多 1 次、每租戶 2 個同時投遞;耗盡 task
30
+ 留待檢查,與官方刪除策略不同。新的 dependency flow 不代表完整應用都能遷移。
31
+
25
32
  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 格式可讀舊資料,回滾須還原升級前備份。
26
33
 
27
34
  0.9.3 的常用輸出投影、清單篩選/排序/limit、Cloud Run 設定更新、Service Account 啟停及 Pub/Sub attributes/ACK deadline 持續保留。僅明列子集經過官方 gcloud、小用量真實 GCP 與隔離 Docker 驗收;不表示完整 Google IAM、SDK/ADC 或自動擴縮相容。
@@ -36,7 +43,7 @@ Python 預設識別而遇到 Cloudflare HTTP 403/1010。識別標頭不取代
36
43
  ## Cloudflare 前端部署與 R2
37
44
 
38
45
  ```bash
39
- pip install 'fufu-cloud-cli==0.9.4'
46
+ pip install 'fufu-cloud-cli==0.9.5'
40
47
  export FUFU_CLOUDFLARE_CREDENTIAL_FILE=/run/secrets/fufu-cloudflare.json
41
48
  wrangler-fufu capabilities
42
49
  wrangler-fufu pages project create frontend --production-branch main
@@ -80,7 +87,8 @@ pip install fufu-cloud-cli
80
87
  fufu --version
81
88
  ```
82
89
 
83
- npm 版本需要 Node.js 18 以上,以及已安裝的 Python 3.10 以上;各入口
90
+ npm registry 尚未發布;以下是 registry 可用後的安裝語法。目前可安裝經驗收
91
+ 的 npm tarball。npm 版本需要 Node.js 18 以上,以及已安裝的 Python 3.10 以上;各入口
84
92
  呼叫相同的 Python CLI。0.8.0 的 npm 建置包內附純 Python YAML/TOML parser,沒有安裝腳本,
85
93
  不會自行下載 Python 或執行 pip。
86
94
 
@@ -97,12 +105,12 @@ npx --no-install aws-fufu --help
97
105
 
98
106
  ## 憑證與 CI
99
107
 
100
- 目前工作樹另提供 `fufu tofu`、`fufu pulumi`、`fufu state`。
108
+ 目前版本另提供 `fufu tofu`、`fufu pulumi`、`fufu state`。
101
109
  Terraform/OpenTofu 可選擇租戶遠端 HTTP state,具備鎖、加密、版本與
102
110
  復原;需配套 Console API 和平台金鑰。Pulumi 使用固定官方 Node.js SDK,
103
111
  已驗收 SQS、SSM String、Pub/Sub topic/pull subscription,採獨立本機
104
- DIY backend,秘密欄位需外部 passphrase。這些新能力尚未包含在已發布
105
- 的 0.8.0,不能把套件安裝成功當成支援驗收。
112
+ DIY backend,秘密欄位需外部 passphrase。這些能力從 0.9.0 起納入 CLI,
113
+ 仍需要配套服務端;不能把套件安裝成功當成支援驗收。
106
114
 
107
115
  此工作樹也提供 `fufu terraform`:以官方 AWS/Google Terraform provider
108
116
  管理 Lambda ZIP、Cloud Run v2、IAM 與服務入口的明列子集,並使用獨立的
@@ -112,9 +120,9 @@ GCS/S3、Functions Gen 1/Gen 2 等一般 CLI 能力
112
120
  不表示官方 Terraform 資源已相容。正式環境使用正常 Terraform、正式認證
113
121
  與獨立 state,不可直接搬用 FUFU state。
114
122
 
115
- 後續開發版另接入 SQS queue、SSM String parameter、Pub/Sub topic/pull
123
+ 目前版本另接入 SQS queue、SSM String parameter、Pub/Sub topic/pull
116
124
  subscription;新增能力需要配套的 Console 與 Pub/Sub 後端更新,不能從
117
- 已發布的 0.8.0 安裝成功就推定已具備。`gcloud-fufu pubsub` 提供建立、
125
+ CLI 安裝成功就推定服務端已具備。`gcloud-fufu pubsub` 提供建立、
118
126
  查詢、更新、刪除、publish/pull/ack 子集;不支援 push、ordering、
119
127
  exactly-once、Function trigger 或保留期限保證。
120
128
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "fufu-cloud-cli"
7
- version = "0.9.4"
7
+ version = "0.9.5"
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"
@@ -16,6 +16,7 @@ license-files = ["LICENSE"]
16
16
  yaml = ["PyYAML==6.0.3"]
17
17
  test = ["boto3==1.40.66"]
18
18
  terraform = ["python-hcl2==7.3.1"]
19
+ gcp = ["google-cloud-bigquery==3.38.0", "google-cloud-tasks==2.20.0", "google-cloud-logging==3.12.1"]
19
20
 
20
21
  [project.scripts]
21
22
  fufu = "fufu_cloud_cli.cli:main"
@@ -23,6 +24,7 @@ aws-fufu = "fufu_cloud_cli.cli:aws_main"
23
24
  gcloud-fufu = "fufu_cloud_cli.cli:gcloud_main"
24
25
  az-fufu = "fufu_cloud_cli.cli:az_main"
25
26
  wrangler-fufu = "fufu_cloud_cli.cli:wrangler_main"
27
+ bq-fufu = "fufu_cloud_cli.cli:bq_main"
26
28
 
27
29
  [tool.setuptools.packages.find]
28
30
  where = ["src"]
@@ -1,3 +1,3 @@
1
1
  """FUFU CLI 與本機部署工具;相容範圍見 capabilities.json。"""
2
2
 
3
- __version__ = "0.9.4"
3
+ __version__ = "0.9.5"
@@ -3,7 +3,7 @@ import sys
3
3
  from .cli import entry
4
4
 
5
5
  if __name__ == "__main__":
6
- if len(sys.argv) > 1 and sys.argv[1] in {"fufu", "aws-fufu", "gcloud-fufu", "az-fufu"}:
6
+ if len(sys.argv) > 1 and sys.argv[1] in {"fufu", "aws-fufu", "gcloud-fufu", "az-fufu", "wrangler-fufu", "bq-fufu"}:
7
7
  entry(sys.argv[1], sys.argv[2:])
8
8
  else:
9
9
  entry("fufu", sys.argv[1:])
@@ -0,0 +1,92 @@
1
+ """bq command subset. Uses the same IAM-enforced BigQuery API as the console."""
2
+ import json
3
+ import sys
4
+ from pathlib import Path
5
+ from .api_backend import ApiClient
6
+ from .errors import require
7
+ from .gcp_infra import call
8
+
9
+
10
+ def reference(value, project):
11
+ if ':' in value: selected, value = value.split(':', 1)
12
+ else: selected = project
13
+ require(selected == project, 'ACCESS_DENIED', 'BigQuery project 必須符合 credential scope。')
14
+ parts = value.split('.')
15
+ require(len(parts) <= 2 and all(parts), 'INVALID_RESOURCE', '請使用 project:dataset.table。')
16
+ return {'projectId': project, 'datasetId': parts[0], **({'tableId': parts[1]} if len(parts) == 2 else {})}
17
+
18
+
19
+ def schema(value):
20
+ path = Path(value)
21
+ if path.is_file():
22
+ require(path.stat().st_size <= 1024**2, 'PAYLOAD_LIMIT', 'Schema 檔案超過 1 MiB。')
23
+ result = json.loads(path.read_text())
24
+ return result if isinstance(result, dict) else {'fields': result}
25
+ return {'fields': [{'name': name, 'type': kind.upper()} for name, kind in (field.split(':', 1) for field in value.split(','))]}
26
+
27
+
28
+ def command(argv, Parser):
29
+ # bq permits global flags before or after the command.
30
+ project = location = None; clean = []; i = 0
31
+ while i < len(argv):
32
+ flag, sep, inline = argv[i].partition('=')
33
+ if flag in {'--project_id', '--location', '--format'}:
34
+ value = inline if sep else (argv[i + 1] if i + 1 < len(argv) else '')
35
+ require(value and not value.startswith('--'), 'INVALID_ARGUMENT', '全域參數缺少值。')
36
+ if flag == '--project_id': project = value
37
+ elif flag == '--location': location = value
38
+ else: require(value in {'json', 'prettyjson'}, 'UNSUPPORTED_FORMAT', 'bq-fufu 輸出支援 json 與 prettyjson。')
39
+ i += 1 if sep else 2
40
+ elif flag in {'--quiet', '-q', '--headless'}: i += 1
41
+ else: clean.append(argv[i]); i += 1
42
+ p = Parser(prog='bq-fufu', description='BigQuery dataset/table、insertAll 與 SELECT 子集。未支援 load jobs、DML/DDL、external tables 或成本估算。')
43
+ actions = p.add_subparsers(dest='action', required=True)
44
+ for action in ['mk', 'ls', 'show', 'rm', 'head']:
45
+ q = actions.add_parser(action)
46
+ q.add_argument('resource', nargs='?' if action == 'ls' else None)
47
+ q.add_argument('--dataset', '-d', action='store_true'); q.add_argument('--table', '-t', action='store_true')
48
+ if action == 'mk': q.add_argument('schema', nargs='?'); q.add_argument('--description')
49
+ if action == 'rm': q.add_argument('--recursive', '-r', action='store_true'); q.add_argument('--force', '-f', action='store_true')
50
+ if action in {'head', 'ls'}: q.add_argument('--max_results', '-n', type=int, default=100); q.add_argument('--page_token')
51
+ q = actions.add_parser('query'); q.add_argument('query'); q.add_argument('--use_legacy_sql', choices=['false'], default='false'); q.add_argument('--nouse_legacy_sql', action='store_true'); q.add_argument('--parameter', action='append', default=[]); q.add_argument('--max_rows', type=int, default=100)
52
+ q = actions.add_parser('insert'); q.add_argument('resource'); q.add_argument('file')
53
+ a = p.parse_args(clean)
54
+ sys.stderr.write('FUFU BigQuery subset: tenant emulator; unsupported SQL and job types fail explicitly.\n')
55
+ with ApiClient('gcp', read_only=a.action in {'ls', 'show', 'head'}) as api:
56
+ selected = api.whoami()['projectId']
57
+ require(project is None or project == selected, 'ACCESS_DENIED', 'BigQuery project 必須符合 credential scope。')
58
+ if a.action == 'query':
59
+ params = {'projectId': selected, 'query': a.query, 'useLegacySql': False, 'maxResults': a.max_rows}
60
+ if location: params['location'] = location
61
+ if a.parameter:
62
+ params['parameterMode'] = 'NAMED'; params['queryParameters'] = []
63
+ for value in a.parameter:
64
+ parts = value.split(':', 2); require(len(parts) == 3, 'INVALID_ARGUMENT', 'parameter 請使用 name:type:value。')
65
+ name, kind, data = parts
66
+ params['queryParameters'].append({'name': name, 'parameterType': {'type': kind.upper() or 'STRING'}, 'parameterValue': {'value': data}})
67
+ return call(api, 'bigquery', 'queries.query', params)
68
+ ref = reference(a.resource, selected) if a.resource else {'projectId': selected}
69
+ table = 'tableId' in ref
70
+ if getattr(a, 'dataset', False): require(not table, 'INVALID_RESOURCE', 'Dataset 命令不可傳入 table。')
71
+ if getattr(a, 'table', False): require(table, 'INVALID_RESOURCE', 'Table 命令需要 dataset.table。')
72
+ group = 'tables' if table else 'datasets'
73
+ if a.action == 'mk':
74
+ require(not table or a.schema, 'INVALID_ARGUMENT', 'Table 建立需要 schema。')
75
+ value = {'tableReference' if table else 'datasetReference': ref}
76
+ if table: value['schema'] = schema(a.schema)
77
+ elif location: value['location'] = location
78
+ if a.description is not None: value['description'] = a.description
79
+ return call(api, 'bigquery', group + '.insert', {'projectId': selected, **({'datasetId': ref['datasetId']} if table else {}), 'table' if table else 'dataset': value})
80
+ if a.action == 'rm': return call(api, 'bigquery', group + '.delete', {**ref, **({'deleteContents': a.recursive} if not table else {})})
81
+ if a.action == 'show': return call(api, 'bigquery', group + '.get', ref)
82
+ if a.action in {'ls', 'head'}:
83
+ require(a.action != 'head' or table, 'INVALID_RESOURCE', 'head 需要 table。')
84
+ require(a.action != 'ls' or not table, 'INVALID_RESOURCE', 'ls 請指定 dataset。')
85
+ op = 'tabledata.list' if table else 'tables.list' if 'datasetId' in ref else 'datasets.list'
86
+ return call(api, 'bigquery', op, {**ref, 'maxResults': a.max_results, **({'pageToken': a.page_token} if a.page_token else {})})
87
+ require(table, 'INVALID_RESOURCE', 'insert 需要 table。')
88
+ file = Path(a.file); require(file.is_file() and file.stat().st_size <= 10 * 1024**2, 'PAYLOAD_LIMIT', 'NDJSON 檔案超過 10 MiB。')
89
+ rows = [{'json': json.loads(line)} for line in file.read_text().splitlines() if line.strip()]
90
+ result = call(api, 'bigquery', 'tabledata.insertAll', {**ref, 'rows': rows})
91
+ require(not result.get('insertErrors'), 'BIGQUERY_INSERT_FAILED', 'BigQuery 拒絕部分資料列;請檢查 schema。')
92
+ return result
@@ -323,10 +323,82 @@
323
323
  "profile-environmentFrom-SSM-only"
324
324
  ]
325
325
  },
326
+ "gcp-logging": {
327
+ "name": "Cloud Logging",
328
+ "status": "implemented",
329
+ "backend": "api",
330
+ "mode": "simplified",
331
+ "operations": [
332
+ "write-read",
333
+ "filter-AND-severity-timestamp-logName-resourceType",
334
+ "logs-list-delete",
335
+ "partialSuccess",
336
+ "explicit-official-Python-REST-SDK",
337
+ "setup-logging"
338
+ ],
339
+ "limitations": [
340
+ "last-10000-entries-per-tenant",
341
+ "single-writer-128MiB-state-capacity",
342
+ "no-automatic-container-stdout",
343
+ "no-sinks-retention-metrics-alerts",
344
+ "no-new-IaC-resources"
345
+ ]
346
+ },
347
+ "gcp-infrastructure-config": {
348
+ "name": "GCP infrastructure configuration",
349
+ "status": "mock-only",
350
+ "backend": "api",
351
+ "mode": "mock-only",
352
+ "services": [
353
+ "compute-instances",
354
+ "sql-instances",
355
+ "sql-databases",
356
+ "app-services",
357
+ "artifact-repositories",
358
+ "dns-zones",
359
+ "dns-records",
360
+ "networks",
361
+ "subnets",
362
+ "vpc-connectors",
363
+ "builds",
364
+ "gemini-models",
365
+ "vm-policies",
366
+ "eventarc-triggers"
367
+ ],
368
+ "operations": [
369
+ "typed-crud",
370
+ "three-locale-mobile-console",
371
+ "fufu-infrastructure-cli"
372
+ ],
373
+ "limitations": [
374
+ "configuration-only",
375
+ "no-VM-SQL-network-DNS-build-registry-inference-runtime",
376
+ "no-new-IaC-resources"
377
+ ]
378
+ },
326
379
  "gcp-bigquery": {
327
380
  "name": "BigQuery",
328
- "status": "partial",
329
- "reason": "Console 與 emulator 查詢子集;CLI、SQL 完整性與雲端最佳化未驗證。"
381
+ "status": "implemented",
382
+ "backend": "api",
383
+ "mode": "simplified",
384
+ "operations": [
385
+ "dataset-table-crud",
386
+ "insertAll",
387
+ "tabledata-list",
388
+ "SELECT-CTE",
389
+ "query-parameters",
390
+ "query-job-results",
391
+ "bq-fufu",
392
+ "explicit-official-Python-REST-SDK"
393
+ ],
394
+ "limitations": [
395
+ "tenant-emulator-required",
396
+ "no-DML-DDL-load-export-external-tables",
397
+ "GoogleSQL-subset",
398
+ "no-billing-or-performance-parity",
399
+ "last-1000-local-query-handles",
400
+ "no-new-Terraform-OpenTofu-Pulumi-resources"
401
+ ]
330
402
  },
331
403
  "gcp-firestore": {
332
404
  "name": "Firestore",
@@ -343,8 +415,28 @@
343
415
  },
344
416
  "gcp-cloud-tasks": {
345
417
  "name": "Cloud Tasks",
346
- "status": "partial",
347
- "reason": "已有 emulator 與 Console 檢視;未驗證 worker 事件投遞。"
418
+ "status": "implemented",
419
+ "backend": "api",
420
+ "mode": "simplified",
421
+ "operations": [
422
+ "queue-crud-pause-resume-purge",
423
+ "HTTP-task-create-list-get-delete-run",
424
+ "scheduled-background-delivery",
425
+ "retry",
426
+ "service-account-invoker-enforcement",
427
+ "explicit-official-Python-REST-SDK"
428
+ ],
429
+ "limitations": [
430
+ "same-tenant-registered-HTTP-targets-only",
431
+ "single-local-coordinator",
432
+ "one-task-per-queue-per-second",
433
+ "2-active-per-tenant-8-global",
434
+ "no-AppEngine-targets",
435
+ "no-Google-ID-token",
436
+ "24h-task-name-tombstones",
437
+ "exhausted-tasks-remain-visible",
438
+ "no-new-IaC-resources"
439
+ ]
348
440
  },
349
441
  "gcp-secret-manager": {
350
442
  "name": "Secret Manager",
@@ -370,8 +462,13 @@
370
462
  },
371
463
  "gcp-cloud-sql": {
372
464
  "name": "Cloud SQL",
373
- "status": "partial",
374
- "reason": "工作區有 PostgreSQL 引擎;不提供 Cloud SQL 控制面或每租戶 SQL 配置。"
465
+ "status": "mock-only",
466
+ "backend": "api",
467
+ "mode": "mock-only",
468
+ "operations": [
469
+ "typed-instance-database-configuration-crud"
470
+ ],
471
+ "reason": "僅保存設定;不建立 SQL runtime、connection endpoint、proxy、backup 或 HA。"
375
472
  },
376
473
  "aws-rds": {
377
474
  "name": "RDS / Aurora",
@@ -391,12 +488,22 @@
391
488
  "gcp-app-engine": {
392
489
  "name": "App Engine",
393
490
  "status": "mock-only",
394
- "reason": "明示 mock 目錄;App 未執行,不能列為成功部署。"
491
+ "backend": "api",
492
+ "mode": "mock-only",
493
+ "operations": [
494
+ "typed-service-configuration-crud"
495
+ ],
496
+ "reason": "僅保存設定;不執行 app.yaml、App Engine runtime 或 traffic。"
395
497
  },
396
498
  "gcp-compute-engine": {
397
499
  "name": "Compute Engine",
398
- "status": "unsupported",
399
- "reason": "尚未實作可供 CLI 自動部署與驗證的服務契約。"
500
+ "status": "mock-only",
501
+ "backend": "api",
502
+ "mode": "mock-only",
503
+ "operations": [
504
+ "typed-instance-configuration-crud"
505
+ ],
506
+ "reason": "僅保存設定;無 VM、disk、SSH、guest OS 或 hypervisor。"
400
507
  },
401
508
  "gcp-gke": {
402
509
  "name": "GKE",
@@ -410,23 +517,57 @@
410
517
  },
411
518
  "gcp-cloud-scheduler": {
412
519
  "name": "Cloud Scheduler",
413
- "status": "unsupported",
414
- "reason": "尚未實作可供 CLI 自動部署與驗證的服務契約。"
520
+ "status": "implemented",
521
+ "backend": "api",
522
+ "mode": "simplified",
523
+ "operations": [
524
+ "HTTP-job-crud",
525
+ "Unix-cron",
526
+ "IANA-timezone",
527
+ "pause-resume-run",
528
+ "automatic-dispatch",
529
+ "retry",
530
+ "service-account-invoker-enforcement"
531
+ ],
532
+ "limitations": [
533
+ "same-tenant-registered-HTTP-targets-only",
534
+ "no-PubSub-targets",
535
+ "no-Google-ID-token-or-service-agent",
536
+ "single-local-coordinator",
537
+ "missed-schedules-coalesce",
538
+ "no-HA-or-exactly-once",
539
+ "no-new-IaC-resources"
540
+ ]
415
541
  },
416
542
  "gcp-artifact-registry": {
417
543
  "name": "Artifact Registry",
418
- "status": "unsupported",
419
- "reason": "尚未實作可供 CLI 自動部署與驗證的服務契約。"
544
+ "status": "mock-only",
545
+ "backend": "api",
546
+ "mode": "mock-only",
547
+ "operations": [
548
+ "typed-repository-configuration-crud"
549
+ ],
550
+ "reason": "僅保存設定;無 OCI push/pull、package storage、scanning 或 registry authentication。"
420
551
  },
421
552
  "gcp-cloud-build": {
422
553
  "name": "Cloud Build",
423
- "status": "unsupported",
424
- "reason": "尚未實作可供 CLI 自動部署與驗證的服務契約。"
554
+ "status": "mock-only",
555
+ "backend": "api",
556
+ "mode": "mock-only",
557
+ "operations": [
558
+ "typed-build-configuration-crud"
559
+ ],
560
+ "reason": "僅保存設定;Cloud Run 本機 source build 是另一條可執行路徑,不是 Cloud Build。"
425
561
  },
426
562
  "gcp-eventarc": {
427
563
  "name": "Eventarc",
428
564
  "status": "mock-only",
429
- "reason": "明示 mock 目錄;事件路由未執行,不能列為成功部署。"
565
+ "backend": "api",
566
+ "mode": "mock-only",
567
+ "operations": [
568
+ "typed-trigger-configuration-crud"
569
+ ],
570
+ "reason": "僅保存設定;不投遞事件或執行 CloudEvents 轉換。"
430
571
  },
431
572
  "gcp-vertex-ai": {
432
573
  "name": "Vertex AI",
@@ -435,8 +576,8 @@
435
576
  },
436
577
  "gcp-cloud-logging-monitoring": {
437
578
  "name": "Cloud Logging / Monitoring",
438
- "status": "unsupported",
439
- "reason": "尚未實作可供 CLI 自動部署與驗證的服務契約。"
579
+ "status": "partial",
580
+ "reason": "gcp-logging 已提供可執行日誌子集;Monitoring、alerts、sinks 與完整 stdout 收集未支援。"
440
581
  },
441
582
  "gcp-load-balancing-nat": {
442
583
  "name": "Cloud Load Balancing / NAT",
@@ -125,6 +125,7 @@ def fufu_parser():
125
125
  sub.add_parser('tofu', help='官方 OpenTofu;支援本機或遠端 FUFU state')
126
126
  sub.add_parser('pulumi', help='官方 Pulumi Node.js SDK;使用獨立 FUFU 開發 state')
127
127
  sub.add_parser('state', help='管理 Terraform/OpenTofu 遠端 state、版本與鎖')
128
+ sub.add_parser('infrastructure', help='GCP 基礎設施配置 mock 的查詢與 CRUD;不啟動資源')
128
129
  for action in ("plan", "deploy"):
129
130
  q = sub.add_parser(action)
130
131
  q.add_argument("--file", required=True)
@@ -176,7 +177,11 @@ def fufu_command(argv):
176
177
  if a.remote:
177
178
  with ApiClient(a.provider, read_only=True) as api:
178
179
  d['remote'] = api.request('GET', '/capabilities' if a.provider == 'cloudflare' else '/compute/capabilities')
180
+ if a.provider == 'gcp': d['infrastructure'] = api.request('GET', '/services/capabilities')
179
181
  available = ['cloudflare-pages', 'cloudflare-workers-static', 'cloudflare-r2'] if a.provider == 'cloudflare' and d['remote'].get('executable') else d['remote'].get('kinds', []) + d['remote'].get('features', [])
182
+ if a.provider == 'gcp':
183
+ names = {'scheduler': 'gcp-cloud-scheduler', 'tasks': 'gcp-cloud-tasks', 'bigquery': 'gcp-bigquery', 'logging': 'gcp-logging'}
184
+ available += [names[service['id']] for service in d['infrastructure']['services'] if service['executable'] and service['id'] in names]
180
185
  require(all(k in available for k in a.require), 'CAPABILITY_NOT_SUPPORTED', '目前 provider 的服務端不支援必要能力。')
181
186
  d['selectedBackend'] = backend
182
187
  return d
@@ -414,7 +419,7 @@ def entry(provider, argv=None):
414
419
  try:
415
420
  argv = sys.argv[1:] if argv is None else argv
416
421
  argv = list(argv)
417
- require(provider in {"fufu", "aws-fufu", "gcloud-fufu", "az-fufu", "wrangler-fufu"}, "UNSUPPORTED_CLI", "未知 CLI 入口。")
422
+ require(provider in {"fufu", "aws-fufu", "gcloud-fufu", "az-fufu", "wrangler-fufu", "bq-fufu"}, "UNSUPPORTED_CLI", "未知 CLI 入口。")
418
423
  if argv == ['--version']:
419
424
  sys.stdout.write(('fufu-cloud-cli ' if provider == 'gcloud-fufu' else '') + __version__ + '\n'); return
420
425
  if provider == 'gcloud-fufu' and argv in [['--help'], ['-h']]:
@@ -426,8 +431,11 @@ def entry(provider, argv=None):
426
431
  p.add_argument('--quiet', '-q', action='store_true', help='非互動執行')
427
432
  p.add_argument('--version', action='store_true', help='CLI 版本')
428
433
  groups = p.add_subparsers(title='服務')
429
- for group in ['run', 'functions', 'iam', 'projects', 'pubsub', 'secrets', 'storage', 'auth']: groups.add_parser(group)
434
+ for group in ['run', 'functions', 'iam', 'projects', 'pubsub', 'secrets', 'storage', 'auth', 'scheduler', 'tasks', 'logging', 'compute', 'sql', 'artifacts', 'dns', 'eventarc']: groups.add_parser(group)
430
435
  p.print_help(); return
436
+ if provider == 'bq-fufu':
437
+ from .bigquery import command
438
+ emit(command(argv, Parser)); return
431
439
  if provider == 'wrangler-fufu':
432
440
  from .cloudflare import command
433
441
  result = command(argv, Parser)
@@ -442,6 +450,9 @@ def entry(provider, argv=None):
442
450
  if provider == 'fufu' and argv[:1] == ['state']:
443
451
  from .iac_state import command
444
452
  emit(command(argv[1:], Parser)); return
453
+ if provider == 'fufu' and argv[:1] == ['infrastructure']:
454
+ from .gcp_infra import configuration_command
455
+ emit(configuration_command(argv[1:], Parser)); return
445
456
  # Native global selectors are checked against the authenticated tenant scope.
446
457
  if provider in {'aws-fufu', 'gcloud-fufu'}:
447
458
  rest = []; i = 0
@@ -477,11 +488,18 @@ def entry(provider, argv=None):
477
488
  if output_filter or output_sort or output_limit is not None:
478
489
  list_commands = [('run', 'services', 'list'), ('functions', 'list'), ('iam', 'service-accounts', 'list'),
479
490
  ('pubsub', 'topics', 'list'), ('pubsub', 'subscriptions', 'list'), ('secrets', 'list'),
480
- ('secrets', 'versions', 'list'), ('storage', 'buckets', 'list'), ('storage', 'objects', 'list')]
491
+ ('secrets', 'versions', 'list'), ('storage', 'buckets', 'list'), ('storage', 'objects', 'list'),
492
+ ('scheduler', 'jobs', 'list'), ('tasks', 'queues', 'list'), ('tasks', 'list'),
493
+ ('compute', 'instances', 'list'), ('sql', 'instances', 'list'), ('sql', 'databases', 'list'),
494
+ ('artifacts', 'repositories', 'list'), ('dns', 'managed-zones', 'list'), ('eventarc', 'triggers', 'list'),
495
+ ('compute', 'networks', 'list'), ('compute', 'networks', 'subnets', 'list'), ('compute', 'networks', 'vpc-access', 'connectors', 'list')]
481
496
  require(any(tuple(argv[:len(command)]) == command for command in list_commands), 'UNSUPPORTED_ARGUMENT', '清單選取旗標只適用於資源 list;未執行操作。')
482
497
  if output_format and argv[:2] == ['functions', 'call']:
483
498
  require(output_format == 'json', 'UNSUPPORTED_FORMAT', 'Functions call 目前只支援 JSON 回應。')
484
499
  from . import public_access
500
+ if provider == 'gcloud-fufu' and argv and argv[0] in {'scheduler', 'tasks', 'logging', 'compute', 'sql', 'artifacts', 'dns', 'eventarc'}:
501
+ from .gcp_infra import command
502
+ emit(command(argv, Parser)); return
485
503
  if provider == 'aws-fufu' and argv[:1] == ['secretsmanager'] or provider == 'gcloud-fufu' and argv[:1] == ['secrets']:
486
504
  from .secrets import command
487
505
  emit(command('aws' if provider == 'aws-fufu' else 'gcp', argv[1:], Parser)); return
@@ -565,3 +583,7 @@ def az_main():
565
583
 
566
584
  def wrangler_main():
567
585
  entry("wrangler-fufu")
586
+
587
+
588
+ def bq_main():
589
+ entry("bq-fufu")
@@ -0,0 +1,222 @@
1
+ """Native command subsets for the shared, tenant-scoped GCP infrastructure API."""
2
+ import base64
3
+ import json
4
+ import os
5
+ import sys
6
+ import time
7
+ from pathlib import Path
8
+ from .api_backend import ApiClient
9
+ from .errors import require
10
+ from .gcloud_environment import dictionary
11
+
12
+
13
+ def call(api, service, operation, params):
14
+ return api._request('POST', '/api/cloud/gcp/services/' + service,
15
+ {'operation': operation, 'params': params}, large=True)
16
+
17
+
18
+ def all_pages(api, service, operation, params, key):
19
+ result, seen, deadline = [], set(), time.monotonic() + 60
20
+ for _ in range(1000):
21
+ require(time.monotonic() < deadline, 'PAGINATION_FAILED', '分頁讀取超過期限。')
22
+ response = call(api, service, operation, params)
23
+ result.extend(response.get(key, []))
24
+ token = response.get('nextPageToken')
25
+ if not token: return result
26
+ require(token not in seen, 'PAGINATION_FAILED', '後端重複回傳分頁。')
27
+ seen.add(token); params = {**params, 'pageToken': token}
28
+ require(False, 'PAGINATION_FAILED', '分頁數超過上限。')
29
+
30
+
31
+ def native_name(parent, collection, value):
32
+ prefix = parent + '/' + collection + '/'
33
+ if value.startswith('projects/'):
34
+ require(value.startswith(prefix) and '/' not in value[len(prefix):], 'ACCESS_DENIED', '資源必須屬於目前 project、location 與 parent。')
35
+ return value
36
+ require('/' not in value and value, 'INVALID_RESOURCE', '資源名稱不合法。')
37
+ return prefix + value
38
+
39
+
40
+ def target_flags(parser, scheduler):
41
+ parser.add_argument('--uri' if scheduler else '--url', dest='uri')
42
+ parser.add_argument('--http-method' if scheduler else '--method', dest='method')
43
+ parser.add_argument('--headers')
44
+ body = parser.add_mutually_exclusive_group()
45
+ body.add_argument('--message-body' if scheduler else '--body-content', dest='body')
46
+ body.add_argument('--message-body-from-file' if scheduler else '--body-file', dest='body_file')
47
+ parser.add_argument('--oidc-service-account-email')
48
+ parser.add_argument('--oidc-token-audience')
49
+
50
+
51
+ def target(a, existing=None, tasks=False):
52
+ result = dict(existing or {})
53
+ if a.uri is not None: result['url' if tasks else 'uri'] = a.uri
54
+ if a.method is not None: result['httpMethod'] = a.method
55
+ if a.headers is not None: result['headers'] = dictionary(a.headers)
56
+ if a.body is not None: result['body'] = base64.b64encode(a.body.encode()).decode()
57
+ if a.body_file:
58
+ p = Path(a.body_file)
59
+ require(p.is_file() and p.stat().st_size <= 1024**2, 'PAYLOAD_LIMIT', '工作內容檔案不得超過 1 MiB。')
60
+ result['body'] = base64.b64encode(p.read_bytes()).decode()
61
+ if a.oidc_service_account_email is not None:
62
+ result['oidcToken'] = {'serviceAccountEmail': a.oidc_service_account_email}
63
+ if a.oidc_token_audience is not None:
64
+ result.setdefault('oidcToken', {})['audience'] = a.oidc_token_audience
65
+ return result
66
+
67
+
68
+ def jobs_command(argv, Parser):
69
+ scheduler = argv[0] == 'scheduler'
70
+ p = Parser(prog='gcloud-fufu ' + argv[0], description='HTTP 工作的可執行子集:同租戶已登錄端點、OIDC identity、cron/重試。無外部 HTTP 投遞。')
71
+ groups = p.add_subparsers(dest='group', required=True)
72
+ for group in (['jobs'] if scheduler else ['queues', 'tasks']):
73
+ actions = groups if group == 'tasks' else groups.add_parser(group).add_subparsers(dest='action', required=True)
74
+ verbs = ['create', 'describe', 'list', 'delete', 'run', 'pause', 'resume', 'update'] if scheduler else ['create', 'describe', 'list', 'delete', 'update', 'pause', 'resume', 'purge'] if group == 'queues' else ['create-http-task', 'describe', 'list', 'delete', 'run']
75
+ for action in verbs:
76
+ q = actions.add_parser(action)
77
+ if group == 'tasks': q.set_defaults(group='tasks', action=action)
78
+ if scheduler and action in {'create', 'update'}:
79
+ q = q.add_subparsers(dest='target_type', required=True).add_parser('http')
80
+ if action != 'list': q.add_argument('name', nargs='?' if action == 'create-http-task' else None)
81
+ else: q.add_argument('--page-size', type=int, default=100); q.add_argument('--page-token')
82
+ q.add_argument('--location')
83
+ if group == 'tasks':
84
+ q.add_argument('--queue', required=True); q.add_argument('--response-view', choices=['basic', 'full'], default='basic')
85
+ if scheduler and action in {'create', 'update'}:
86
+ q.add_argument('--schedule', required=action == 'create'); q.add_argument('--time-zone'); q.add_argument('--description'); q.add_argument('--attempt-deadline')
87
+ q.add_argument('--max-retry-attempts', type=int); q.add_argument('--min-backoff'); q.add_argument('--max-backoff'); q.add_argument('--max-doublings', type=int); q.add_argument('--max-retry-duration')
88
+ target_flags(q, True)
89
+ if group == 'queues' and action in {'create', 'update'}:
90
+ q.add_argument('--max-dispatches-per-second', type=int); q.add_argument('--max-concurrent-dispatches', type=int)
91
+ q.add_argument('--max-attempts', type=int); q.add_argument('--min-backoff'); q.add_argument('--max-backoff'); q.add_argument('--max-doublings', type=int); q.add_argument('--max-retry-duration')
92
+ if action == 'create-http-task':
93
+ target_flags(q, False); q.add_argument('--schedule-time'); q.add_argument('--dispatch-deadline')
94
+ a = p.parse_args(argv[1:])
95
+ with ApiClient('gcp', read_only=a.action in {'describe', 'list'}) as api:
96
+ who = api.whoami(); region = a.location or os.environ.get('FUFU_SELECTED_REGION') or who['region']
97
+ require(region == who['region'], 'ACCESS_DENIED', 'location 必須符合 credential scope。')
98
+ parent = 'projects/' + who['projectId'] + '/locations/' + region
99
+ service = 'scheduler' if scheduler else 'tasks'
100
+ task = a.group == 'tasks'
101
+ if task: parent = native_name(parent, 'queues', a.queue)
102
+ full = native_name(parent, a.group, a.name) if getattr(a, 'name', None) else None
103
+ operation = lambda action: action if scheduler else a.group + '.' + action
104
+ if a.action == 'list':
105
+ return all_pages(api, service, operation('list'), {'parent': parent, 'pageSize': a.page_size, **({'pageToken': a.page_token} if a.page_token else {}), **({'responseView': a.response_view.upper()} if task else {})}, a.group)
106
+ if a.action in {'create', 'update', 'create-http-task'}:
107
+ current = call(api, service, operation('get'), {'name': full}) if a.action == 'update' else {}
108
+ body = {'name': full} if full else {}
109
+ if scheduler:
110
+ for flag, field in [('schedule', 'schedule'), ('time_zone', 'timeZone'), ('description', 'description'), ('attempt_deadline', 'attemptDeadline')]:
111
+ if getattr(a, flag) is not None: body[field] = getattr(a, flag)
112
+ http = target(a, current.get('httpTarget'))
113
+ if http: body['httpTarget'] = http
114
+ elif task:
115
+ body['httpRequest'] = target(a, tasks=True)
116
+ if a.schedule_time: body['scheduleTime'] = a.schedule_time
117
+ if a.dispatch_deadline: body['dispatchDeadline'] = a.dispatch_deadline
118
+ else:
119
+ rate = dict(current.get('rateLimits', {}))
120
+ for flag, field in [('max_dispatches_per_second', 'maxDispatchesPerSecond'), ('max_concurrent_dispatches', 'maxConcurrentDispatches')]:
121
+ if getattr(a, flag) is not None: rate[field] = getattr(a, flag)
122
+ if rate: body['rateLimits'] = rate
123
+ if not task:
124
+ config = dict(current.get('retryConfig', {}))
125
+ for flag, field in [('max_retry_attempts' if scheduler else 'max_attempts', 'retryCount' if scheduler else 'maxAttempts'), ('min_backoff', 'minBackoffDuration' if scheduler else 'minBackoff'), ('max_backoff', 'maxBackoffDuration' if scheduler else 'maxBackoff'), ('max_doublings', 'maxDoublings'), ('max_retry_duration', 'maxRetryDuration')]:
126
+ if getattr(a, flag) is not None: config[field] = getattr(a, flag)
127
+ if config: body['retryConfig'] = config
128
+ key = 'job' if scheduler else 'task' if task else 'queue'
129
+ params = {key: body}
130
+ if a.action == 'update': params['updateMask'] = ','.join(k for k in body if k != 'name')
131
+ else: params['parent'] = parent
132
+ if task: params['responseView'] = a.response_view.upper()
133
+ return call(api, service, operation('update' if a.action == 'update' else 'create'), params)
134
+ return call(api, service, operation('get' if a.action == 'describe' else a.action), {'name': full, **({'responseView': a.response_view.upper()} if task and a.action in {'describe', 'run'} else {})})
135
+
136
+
137
+ def logging_command(argv, Parser):
138
+ p = Parser(prog='gcloud-fufu logging', description='持久化本機日誌;保留最近 10,000 筆,不含正式 Log Router、retention 或全容器 stdout。')
139
+ actions = p.add_subparsers(dest='action', required=True)
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
+ 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')
142
+ logs = actions.add_parser('logs').add_subparsers(dest='logs_action', required=True)
143
+ logs.add_parser('list'); logs.add_parser('delete').add_argument('log')
144
+ a = p.parse_args(argv)
145
+ with ApiClient('gcp', read_only=a.action == 'read' or a.action == 'logs' and a.logs_action == 'list') as api:
146
+ project = 'projects/' + api.whoami()['projectId']
147
+ if a.action == 'logs' and a.logs_action == 'list': return all_pages(api, 'logging', 'logs-list', {'parent': project}, 'logNames')
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]
151
+ name = a.log if a.log.startswith('projects/') else project + '/logs/' + a.log
152
+ if a.action == 'logs': return call(api, 'logging', 'logs-delete', {'logName': name})
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}
154
+ return call(api, 'logging', 'write', {'logName': name, 'entries': [entry]})
155
+
156
+
157
+ # Native flag spelling is kept; values remain configuration-only and never imply runtime success.
158
+ MOCK_GROUPS = {
159
+ ('compute', 'instances'): ('compute-instances', {'zone': 'zone', 'machine-type': 'machineType', 'image': 'image', 'image-project': 'imageProject', 'boot-disk-size': 'bootDiskSizeGb'}),
160
+ ('compute', 'networks'): ('networks', {'subnet-mode': 'subnetMode', 'mtu': 'mtu', 'description': 'description'}),
161
+ ('compute', 'networks', 'subnets'): ('subnets', {'network': 'network', 'range': 'ipCidrRange'}),
162
+ ('sql', 'instances'): ('sql-instances', {'database-version': 'databaseVersion', 'tier': 'tier', 'storage-size': 'storageSizeGb', 'availability-type': 'availabilityType'}),
163
+ ('sql', 'databases'): ('sql-databases', {'instance': 'instance', 'charset': 'charset', 'collation': 'collation'}),
164
+ ('artifacts', 'repositories'): ('artifact-repositories', {'repository-format': 'format', 'location': 'location', 'description': 'description'}),
165
+ ('dns', 'managed-zones'): ('dns-zones', {'dns-name': 'dnsName', 'description': 'description', 'visibility': 'visibility'}),
166
+ ('compute', 'os-config', 'os-policy-assignments'): ('vm-policies', {'zone': 'zone', 'description': 'description'}),
167
+ ('compute', 'networks', 'vpc-access', 'connectors'): ('vpc-connectors', {'network': 'network', 'range': 'range', 'min-instances': 'minInstances', 'max-instances': 'maxInstances'}),
168
+ ('eventarc', 'triggers'): ('eventarc-triggers', {'location': 'location', 'destination-run-service': 'destination', 'event-filters': 'eventType', 'service-account': 'serviceAccount'}),
169
+ }
170
+
171
+
172
+ def mock_command(argv, Parser):
173
+ match = next((k for k in sorted(MOCK_GROUPS, key=len, reverse=True) if tuple(argv[:len(k)]) == k), None)
174
+ require(match is not None, 'CAPABILITY_NOT_SUPPORTED', '此服務只有文件列出的配置 mock;目前命令未支援。')
175
+ service, flags = MOCK_GROUPS[match]
176
+ p = Parser(prog='gcloud-fufu ' + ' '.join(match), description='MOCK ONLY:只儲存配置,不建立或啟動真正資源。')
177
+ actions = p.add_subparsers(dest='action', required=True)
178
+ for action in ['create', 'update', 'describe', 'list', 'delete']:
179
+ 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)
183
+ 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')
185
+ with ApiClient('gcp', read_only=a.action in {'list', 'describe'}) as api:
186
+ if a.action == 'list': return all_pages(api, service, 'list', {}, 'items')
187
+ params = {'name': a.name}
188
+ 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']
191
+ params['config'] = config
192
+ return call(api, service, 'get' if a.action == 'describe' else a.action, params)
193
+
194
+
195
+ def command(argv, Parser):
196
+ if argv[0] in {'scheduler', 'tasks'}: return jobs_command(argv, Parser)
197
+ if argv[0] == 'logging': return logging_command(argv[1:], Parser)
198
+ return mock_command(argv, Parser)
199
+
200
+
201
+ def configuration_command(argv, Parser):
202
+ p = Parser(prog='fufu infrastructure', description='GCP 配置 mock 的明確擴充介面;不會執行 VM、SQL、DNS、Cloud Build 或 AI 推論。')
203
+ actions = p.add_subparsers(dest='action', required=True)
204
+ actions.add_parser('catalog')
205
+ for action in ['create', 'update', 'describe', 'list', 'delete']:
206
+ q = actions.add_parser(action); q.add_argument('service')
207
+ if action != 'list': q.add_argument('name')
208
+ if action in {'create', 'update'}: q.add_argument('--config-file', required=True)
209
+ a = p.parse_args(argv)
210
+ with ApiClient('gcp', read_only=a.action in {'catalog', 'describe', 'list'}) as api:
211
+ capabilities = api.request('GET', '/services/capabilities')
212
+ mocks = {s['id']: s for s in capabilities['services'] if not s['executable']}
213
+ if a.action == 'catalog': return {'services': list(mocks.values())}
214
+ require(a.service in mocks, 'CAPABILITY_NOT_SUPPORTED', '這個介面只處理明確列出的配置 mock。')
215
+ sys.stderr.write('FUFU MOCK ONLY: configuration is persisted; no underlying resource is provisioned.\n')
216
+ if a.action == 'list': return all_pages(api, a.service, 'list', {}, 'items')
217
+ params = {'name': a.name}
218
+ if a.action in {'create', 'update'}:
219
+ source = Path(a.config_file)
220
+ require(source.is_file() and source.stat().st_size <= 1024**2, 'INVALID_CONFIG', '配置 JSON 檔案須存在且不超過 1 MiB。')
221
+ params['config'] = json.loads(source.read_text())
222
+ return call(api, a.service, 'get' if a.action == 'describe' else a.action, params)
@@ -0,0 +1,60 @@
1
+ """Explicit Google SDK client factories for deployed FUFU workloads.
2
+
3
+ Native request/response serialization stays in Google's SDK. Only documented
4
+ REST operations are routed; there is no ADC search or real-cloud fallback.
5
+ """
6
+ import os
7
+ from urllib.parse import urlsplit
8
+
9
+
10
+ def _configuration():
11
+ endpoint = os.environ.get('FUFU_RUNTIME_ENDPOINT', '')
12
+ parsed = urlsplit(endpoint)
13
+ token = os.environ.get('FUFU_WORKLOAD_TOKEN')
14
+ project = os.environ.get('GOOGLE_CLOUD_PROJECT')
15
+ if parsed.scheme != 'http' or parsed.hostname != 'fufu-api' or parsed.port != 8080 or parsed.path != '/api/runtime/gcp' or parsed.query or parsed.fragment or parsed.username or parsed.password or not token or not project:
16
+ raise RuntimeError('A deployed FUFU GCP workload and its scoped identity are required')
17
+ from google.auth.credentials import Credentials
18
+
19
+ class WorkloadCredentials(Credentials):
20
+ def __init__(self):
21
+ super().__init__(); self.token = token
22
+ def refresh(self, request):
23
+ # The server renews the lease only while this exact deployment is live.
24
+ self.token = token
25
+
26
+ return endpoint, project, WorkloadCredentials()
27
+
28
+
29
+ def bigquery_client():
30
+ from google.cloud import bigquery
31
+ endpoint, project, credentials = _configuration()
32
+ return bigquery.Client(project=project, credentials=credentials, client_options={'api_endpoint': endpoint + '/bigquery'})
33
+
34
+
35
+ def tasks_client():
36
+ from google.cloud import tasks_v2
37
+ endpoint, _, credentials = _configuration()
38
+ return tasks_v2.CloudTasksClient(credentials=credentials, transport='rest', client_options={'api_endpoint': endpoint + '/tasks'})
39
+
40
+
41
+ def logging_client():
42
+ from google.cloud import logging_v2
43
+ from google.cloud.logging_v2.resource import Resource
44
+ from google.cloud.logging_v2.handlers import CloudLoggingHandler
45
+ from google.cloud.logging_v2.handlers.transports import SyncTransport
46
+ endpoint, project, credentials = _configuration()
47
+ resource = Resource(type='global', labels={'project_id': project})
48
+
49
+ class ScopedLoggingClient(logging_v2.Client):
50
+ def logger(self, name, *, labels=None, resource=None):
51
+ return super().logger(name, labels=labels, resource=resource or Resource(type='global', labels={'project_id': project}))
52
+
53
+ def get_default_handler(self, **kwargs):
54
+ # setup_logging must send to the explicit endpoint even when Cloud Run
55
+ # environment variables would normally choose stdout collection.
56
+ kwargs.setdefault('resource', resource)
57
+ kwargs.setdefault('transport', SyncTransport)
58
+ return CloudLoggingHandler(self, **kwargs)
59
+
60
+ return ScopedLoggingClient(project=project, credentials=credentials, _use_grpc=False, client_options={'api_endpoint': endpoint + '/logging'})
@@ -1,9 +1,56 @@
1
+ Metadata-Version: 2.4
2
+ Name: fufu-cloud-cli
3
+ Version: 0.9.5
4
+ Summary: FUFU Cloud CLI for tenant-scoped mock cloud development and deployment
5
+ License-Expression: LicenseRef-Proprietary
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Requires-Dist: PyYAML==6.0.3
10
+ Requires-Dist: tomli==2.2.1; python_version < "3.11"
11
+ Provides-Extra: yaml
12
+ Requires-Dist: PyYAML==6.0.3; extra == "yaml"
13
+ Provides-Extra: test
14
+ Requires-Dist: boto3==1.40.66; extra == "test"
15
+ Provides-Extra: terraform
16
+ Requires-Dist: python-hcl2==7.3.1; extra == "terraform"
17
+ Provides-Extra: gcp
18
+ Requires-Dist: google-cloud-bigquery==3.38.0; extra == "gcp"
19
+ Requires-Dist: google-cloud-tasks==2.20.0; extra == "gcp"
20
+ Requires-Dist: google-cloud-logging==3.12.1; extra == "gcp"
21
+ Dynamic: license-file
22
+
1
23
  # FUFU Cloud CLI
2
24
 
3
- 以五個指令操作 FUFU Cloud 的租戶隔離開發環境:`fufu`、`aws-fufu`、
4
- `gcloud-fufu`、`az-fufu`、`wrangler-fufu`。這是 AWS、Google Cloud、Azure 與 Cloudflare 的**明確子集**,
25
+ 以六個指令操作 FUFU Cloud 的租戶隔離開發環境:`fufu`、`aws-fufu`、
26
+ `gcloud-fufu`、`az-fufu`、`wrangler-fufu`、`bq-fufu`。這是 AWS、Google Cloud、Azure 與 Cloudflare 的**明確子集**,
5
27
  只供 FUFU mock 環境使用;不會把部署轉送到正式雲端。
6
28
 
29
+ 0.9.5 新增 `gcloud-fufu scheduler`、`tasks`、`logging` 與 `bq-fufu`。
30
+ 支援同租戶已登錄 HTTP 應用的排程/背景任務、BigQuery dataset/table、
31
+ insertAll/SELECT 與持久化日誌。`fufu_cloud_cli.gcp_sdk` 提供官方 Python
32
+ BigQuery、Cloud Tasks、Logging SDK 的明確 REST factory;可用 `[gcp]` extra
33
+ 安裝已驗收相依。需要對應的新後端,並非透明 ADC 或所有 Google SDK 替換。
34
+
35
+ `fufu infrastructure` 提供 14 類 typed configuration mock。Compute Engine、
36
+ Cloud SQL、App Engine、DNS、VPC、Artifact Registry、Cloud Build、Gemini、
37
+ VM Manager、Eventarc 等設定不會啟動 VM/SQL/registry/網路/推論。
38
+ UI 顯示僅設定,CLI stderr 保留 MOCK ONLY,不因輸出投影而隱藏。
39
+ 新服務尚未接入 Terraform/OpenTofu/Pulumi;單機背景投遞不是雲端 HA。
40
+
41
+ ```bash
42
+ gcloud-fufu scheduler jobs list
43
+ gcloud-fufu tasks queues list
44
+ bq-fufu query --use_legacy_sql=false 'SELECT 1 AS ready'
45
+ gcloud-fufu logging read 'severity>=ERROR'
46
+ fufu infrastructure catalog
47
+ ```
48
+
49
+ BigQuery 是 tenant emulator 的 GoogleSQL 子集,無 DML/DDL、load/export、
50
+ 外部表與效能/費用等價。Logging 保留每租戶最新 10,000 筆,不自動收集所有
51
+ 容器 stdout。Tasks 每 queue 每秒最多 1 次、每租戶 2 個同時投遞;耗盡 task
52
+ 留待檢查,與官方刪除策略不同。新的 dependency flow 不代表完整應用都能遷移。
53
+
7
54
  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 格式可讀舊資料,回滾須還原升級前備份。
8
55
 
9
56
  0.9.3 的常用輸出投影、清單篩選/排序/limit、Cloud Run 設定更新、Service Account 啟停及 Pub/Sub attributes/ACK deadline 持續保留。僅明列子集經過官方 gcloud、小用量真實 GCP 與隔離 Docker 驗收;不表示完整 Google IAM、SDK/ADC 或自動擴縮相容。
@@ -18,7 +65,7 @@ Python 預設識別而遇到 Cloudflare HTTP 403/1010。識別標頭不取代
18
65
  ## Cloudflare 前端部署與 R2
19
66
 
20
67
  ```bash
21
- pip install 'fufu-cloud-cli==0.9.4'
68
+ pip install 'fufu-cloud-cli==0.9.5'
22
69
  export FUFU_CLOUDFLARE_CREDENTIAL_FILE=/run/secrets/fufu-cloudflare.json
23
70
  wrangler-fufu capabilities
24
71
  wrangler-fufu pages project create frontend --production-branch main
@@ -62,7 +109,8 @@ pip install fufu-cloud-cli
62
109
  fufu --version
63
110
  ```
64
111
 
65
- npm 版本需要 Node.js 18 以上,以及已安裝的 Python 3.10 以上;各入口
112
+ npm registry 尚未發布;以下是 registry 可用後的安裝語法。目前可安裝經驗收
113
+ 的 npm tarball。npm 版本需要 Node.js 18 以上,以及已安裝的 Python 3.10 以上;各入口
66
114
  呼叫相同的 Python CLI。0.8.0 的 npm 建置包內附純 Python YAML/TOML parser,沒有安裝腳本,
67
115
  不會自行下載 Python 或執行 pip。
68
116
 
@@ -79,12 +127,12 @@ npx --no-install aws-fufu --help
79
127
 
80
128
  ## 憑證與 CI
81
129
 
82
- 目前工作樹另提供 `fufu tofu`、`fufu pulumi`、`fufu state`。
130
+ 目前版本另提供 `fufu tofu`、`fufu pulumi`、`fufu state`。
83
131
  Terraform/OpenTofu 可選擇租戶遠端 HTTP state,具備鎖、加密、版本與
84
132
  復原;需配套 Console API 和平台金鑰。Pulumi 使用固定官方 Node.js SDK,
85
133
  已驗收 SQS、SSM String、Pub/Sub topic/pull subscription,採獨立本機
86
- DIY backend,秘密欄位需外部 passphrase。這些新能力尚未包含在已發布
87
- 的 0.8.0,不能把套件安裝成功當成支援驗收。
134
+ DIY backend,秘密欄位需外部 passphrase。這些能力從 0.9.0 起納入 CLI,
135
+ 仍需要配套服務端;不能把套件安裝成功當成支援驗收。
88
136
 
89
137
  此工作樹也提供 `fufu terraform`:以官方 AWS/Google Terraform provider
90
138
  管理 Lambda ZIP、Cloud Run v2、IAM 與服務入口的明列子集,並使用獨立的
@@ -94,9 +142,9 @@ GCS/S3、Functions Gen 1/Gen 2 等一般 CLI 能力
94
142
  不表示官方 Terraform 資源已相容。正式環境使用正常 Terraform、正式認證
95
143
  與獨立 state,不可直接搬用 FUFU state。
96
144
 
97
- 後續開發版另接入 SQS queue、SSM String parameter、Pub/Sub topic/pull
145
+ 目前版本另接入 SQS queue、SSM String parameter、Pub/Sub topic/pull
98
146
  subscription;新增能力需要配套的 Console 與 Pub/Sub 後端更新,不能從
99
- 已發布的 0.8.0 安裝成功就推定已具備。`gcloud-fufu pubsub` 提供建立、
147
+ CLI 安裝成功就推定服務端已具備。`gcloud-fufu pubsub` 提供建立、
100
148
  查詢、更新、刪除、publish/pull/ack 子集;不支援 push、ordering、
101
149
  exactly-once、Function trigger 或保留期限保證。
102
150
 
@@ -5,6 +5,7 @@ pyproject.toml
5
5
  src/fufu_cloud_cli/__init__.py
6
6
  src/fufu_cloud_cli/__main__.py
7
7
  src/fufu_cloud_cli/api_backend.py
8
+ src/fufu_cloud_cli/bigquery.py
8
9
  src/fufu_cloud_cli/capabilities.json
9
10
  src/fufu_cloud_cli/capacity.py
10
11
  src/fufu_cloud_cli/cli.py
@@ -15,6 +16,8 @@ src/fufu_cloud_cli/docker_backend.py
15
16
  src/fufu_cloud_cli/errors.py
16
17
  src/fufu_cloud_cli/gcloud_environment.py
17
18
  src/fufu_cloud_cli/gcloud_output.py
19
+ src/fufu_cloud_cli/gcp_infra.py
20
+ src/fufu_cloud_cli/gcp_sdk.py
18
21
  src/fufu_cloud_cli/gcs.py
19
22
  src/fufu_cloud_cli/http_runtime.py
20
23
  src/fufu_cloud_cli/iac_state.py
@@ -1,6 +1,7 @@
1
1
  [console_scripts]
2
2
  aws-fufu = fufu_cloud_cli.cli:aws_main
3
3
  az-fufu = fufu_cloud_cli.cli:az_main
4
+ bq-fufu = fufu_cloud_cli.cli:bq_main
4
5
  fufu = fufu_cloud_cli.cli:main
5
6
  gcloud-fufu = fufu_cloud_cli.cli:gcloud_main
6
7
  wrangler-fufu = fufu_cloud_cli.cli:wrangler_main
@@ -3,6 +3,11 @@ PyYAML==6.0.3
3
3
  [:python_version < "3.11"]
4
4
  tomli==2.2.1
5
5
 
6
+ [gcp]
7
+ google-cloud-bigquery==3.38.0
8
+ google-cloud-tasks==2.20.0
9
+ google-cloud-logging==3.12.1
10
+
6
11
  [terraform]
7
12
  python-hcl2==7.3.1
8
13
 
File without changes
File without changes