fufu-cloud-cli 0.7.0__tar.gz → 0.9.0__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 (50) hide show
  1. {fufu_cloud_cli-0.7.0/src/fufu_cloud_cli.egg-info → fufu_cloud_cli-0.9.0}/PKG-INFO +104 -9
  2. {fufu_cloud_cli-0.7.0 → fufu_cloud_cli-0.9.0}/README.md +98 -8
  3. {fufu_cloud_cli-0.7.0 → fufu_cloud_cli-0.9.0}/pyproject.toml +6 -3
  4. {fufu_cloud_cli-0.7.0 → fufu_cloud_cli-0.9.0}/src/fufu_cloud_cli/__init__.py +1 -1
  5. {fufu_cloud_cli-0.7.0 → fufu_cloud_cli-0.9.0}/src/fufu_cloud_cli/api_backend.py +33 -14
  6. {fufu_cloud_cli-0.7.0 → fufu_cloud_cli-0.9.0}/src/fufu_cloud_cli/capabilities.json +80 -17
  7. fufu_cloud_cli-0.9.0/src/fufu_cloud_cli/capacity.py +43 -0
  8. {fufu_cloud_cli-0.7.0 → fufu_cloud_cli-0.9.0}/src/fufu_cloud_cli/cli.py +75 -16
  9. {fufu_cloud_cli-0.7.0 → fufu_cloud_cli-0.9.0}/src/fufu_cloud_cli/cloud_data.py +5 -4
  10. fufu_cloud_cli-0.9.0/src/fufu_cloud_cli/cloudflare.py +331 -0
  11. {fufu_cloud_cli-0.7.0 → fufu_cloud_cli-0.9.0}/src/fufu_cloud_cli/control.py +25 -11
  12. {fufu_cloud_cli-0.7.0 → fufu_cloud_cli-0.9.0}/src/fufu_cloud_cli/docker_backend.py +91 -24
  13. fufu_cloud_cli-0.9.0/src/fufu_cloud_cli/http_runtime.py +160 -0
  14. fufu_cloud_cli-0.9.0/src/fufu_cloud_cli/iac_state.py +61 -0
  15. fufu_cloud_cli-0.9.0/src/fufu_cloud_cli/limits.py +72 -0
  16. {fufu_cloud_cli-0.7.0 → fufu_cloud_cli-0.9.0}/src/fufu_cloud_cli/manifest.py +26 -7
  17. fufu_cloud_cli-0.9.0/src/fufu_cloud_cli/object_transfer.py +77 -0
  18. fufu_cloud_cli-0.9.0/src/fufu_cloud_cli/pubsub.py +66 -0
  19. fufu_cloud_cli-0.9.0/src/fufu_cloud_cli/pulumi-endpoints.json +526 -0
  20. fufu_cloud_cli-0.9.0/src/fufu_cloud_cli/pulumi_bridge.py +205 -0
  21. {fufu_cloud_cli-0.7.0 → fufu_cloud_cli-0.9.0}/src/fufu_cloud_cli/remote_compute.py +53 -14
  22. {fufu_cloud_cli-0.7.0 → fufu_cloud_cli-0.9.0}/src/fufu_cloud_cli/runtime/ingress/proxy.py +3 -3
  23. fufu_cloud_cli-0.9.0/src/fufu_cloud_cli/runtime_sdk.py +191 -0
  24. fufu_cloud_cli-0.9.0/src/fufu_cloud_cli/secrets.py +90 -0
  25. {fufu_cloud_cli-0.7.0 → fufu_cloud_cli-0.9.0}/src/fufu_cloud_cli/serverless.py +8 -6
  26. fufu_cloud_cli-0.9.0/src/fufu_cloud_cli/terraform_bridge/__init__.py +1 -0
  27. fufu_cloud_cli-0.9.0/src/fufu_cloud_cli/terraform_bridge/aws.py +187 -0
  28. fufu_cloud_cli-0.9.0/src/fufu_cloud_cli/terraform_bridge/aws_data.py +63 -0
  29. fufu_cloud_cli-0.9.0/src/fufu_cloud_cli/terraform_bridge/common.py +129 -0
  30. fufu_cloud_cli-0.9.0/src/fufu_cloud_cli/terraform_bridge/google.py +155 -0
  31. fufu_cloud_cli-0.9.0/src/fufu_cloud_cli/terraform_bridge/google_pubsub.py +57 -0
  32. fufu_cloud_cli-0.9.0/src/fufu_cloud_cli/terraform_bridge/remote_state.py +74 -0
  33. fufu_cloud_cli-0.9.0/src/fufu_cloud_cli/terraform_bridge/runner.py +402 -0
  34. {fufu_cloud_cli-0.7.0 → fufu_cloud_cli-0.9.0/src/fufu_cloud_cli.egg-info}/PKG-INFO +104 -9
  35. {fufu_cloud_cli-0.7.0 → fufu_cloud_cli-0.9.0}/src/fufu_cloud_cli.egg-info/SOURCES.txt +19 -1
  36. {fufu_cloud_cli-0.7.0 → fufu_cloud_cli-0.9.0}/src/fufu_cloud_cli.egg-info/entry_points.txt +1 -0
  37. fufu_cloud_cli-0.9.0/src/fufu_cloud_cli.egg-info/requires.txt +13 -0
  38. fufu_cloud_cli-0.7.0/src/fufu_cloud_cli/runtime_sdk.py +0 -114
  39. fufu_cloud_cli-0.7.0/src/fufu_cloud_cli.egg-info/requires.txt +0 -4
  40. {fufu_cloud_cli-0.7.0 → fufu_cloud_cli-0.9.0}/LICENSE +0 -0
  41. {fufu_cloud_cli-0.7.0 → fufu_cloud_cli-0.9.0}/MANIFEST.in +0 -0
  42. {fufu_cloud_cli-0.7.0 → fufu_cloud_cli-0.9.0}/setup.cfg +0 -0
  43. {fufu_cloud_cli-0.7.0 → fufu_cloud_cli-0.9.0}/src/fufu_cloud_cli/__main__.py +0 -0
  44. {fufu_cloud_cli-0.7.0 → fufu_cloud_cli-0.9.0}/src/fufu_cloud_cli/errors.py +0 -0
  45. {fufu_cloud_cli-0.7.0 → fufu_cloud_cli-0.9.0}/src/fufu_cloud_cli/lambda_adapter.py +0 -0
  46. {fufu_cloud_cli-0.7.0 → fufu_cloud_cli-0.9.0}/src/fufu_cloud_cli/public_access.py +0 -0
  47. {fufu_cloud_cli-0.7.0 → fufu_cloud_cli-0.9.0}/src/fufu_cloud_cli/runtime/functions/Dockerfile +0 -0
  48. {fufu_cloud_cli-0.7.0 → fufu_cloud_cli-0.9.0}/src/fufu_cloud_cli/runtime/ingress/Dockerfile +0 -0
  49. {fufu_cloud_cli-0.7.0 → fufu_cloud_cli-0.9.0}/src/fufu_cloud_cli.egg-info/dependency_links.txt +0 -0
  50. {fufu_cloud_cli-0.7.0 → fufu_cloud_cli-0.9.0}/src/fufu_cloud_cli.egg-info/top_level.txt +0 -0
@@ -1,22 +1,67 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fufu-cloud-cli
3
- Version: 0.7.0
3
+ Version: 0.9.0
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
7
7
  Description-Content-Type: text/markdown
8
8
  License-File: LICENSE
9
9
  Requires-Dist: PyYAML==6.0.3
10
+ Requires-Dist: tomli==2.2.1; python_version < "3.11"
10
11
  Provides-Extra: yaml
11
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"
12
17
  Dynamic: license-file
13
18
 
14
19
  # FUFU Cloud CLI
15
20
 
16
- 以四個指令操作 FUFU Cloud 的租戶隔離開發環境:`fufu`、`aws-fufu`、
17
- `gcloud-fufu`、`az-fufu`。這是 AWS、Google Cloud 與 Azure 的**明確子集**,
21
+ 以五個指令操作 FUFU Cloud 的租戶隔離開發環境:`fufu`、`aws-fufu`、
22
+ `gcloud-fufu`、`az-fufu`、`wrangler-fufu`。這是 AWS、Google Cloud、Azure 與 Cloudflare 的**明確子集**,
18
23
  只供 FUFU mock 環境使用;不會把部署轉送到正式雲端。
19
24
 
25
+ 0.8.0 包含資源配額、分段傳輸、Terraform 核心子集與 Cloudflare 靜態部署/R2。需要同步更新服務端;CLI 安裝成功不表示任意 FUFU 服務端都已有這些能力。
26
+
27
+ ## Cloudflare 前端部署與 R2
28
+
29
+ ```bash
30
+ pip install 'fufu-cloud-cli==0.8.0'
31
+ export FUFU_CLOUDFLARE_CREDENTIAL_FILE=/run/secrets/fufu-cloudflare.json
32
+ wrangler-fufu capabilities
33
+ wrangler-fufu pages project create frontend --production-branch main
34
+ npm run build
35
+ wrangler-fufu pages deploy dist --project-name frontend --branch main
36
+ wrangler-fufu pages deployment list --project-name frontend
37
+ wrangler-fufu deploy --assets dist --name frontend-assets
38
+ wrangler-fufu r2 bucket create frontend-files
39
+ wrangler-fufu r2 object put frontend-files/example.txt --file example.txt --remote
40
+ ```
41
+
42
+ 支援 Wrangler JSON、JSONC、TOML,以及 `--config`、`--cwd`、`--env`。Pages 使用
43
+ `pages_build_output_dir`;Workers Static Assets 使用 `assets.directory`,SPA 可明確設定
44
+ `assets.not_found_handling = "single-page-application"`。正式與預覽網址分開,正式部署完整
45
+ 上傳且校驗通過才切换入口;Pages 可用 `pages deployment rollback ID --project-name NAME` 回滾。
46
+
47
+ 這些命令提供真正的靜態 HTTP hosting,**不執行 Workers JavaScript 或 Pages Functions**。
48
+ KV、D1、Durable Objects、Queues、Cron、自訂網域、SSR 與全球 CDN 快取未實作;
49
+ 相關配置會被拒絕。API credential、S3 endpoint 與帳號識別使用 FUFU 值。
50
+ 目前不讀取官方 `CLOUDFLARE_API_TOKEN`;使用 FUFU credential JSON,避免誤用正式金鑰。
51
+
52
+ R2 支援 bucket/object、分段上傳、範圍下載、CORS、預設私人及可開關公開開發網址。
53
+ 原生 boto3/AWS S3 SDK 可使用 credential JSON 中的 `r2_endpoint`、`r2_access_key_id`、
54
+ `r2_secret_access_key`,region 為 `auto` 且使用 path-style addressing。
55
+ SDK checksum 配置使用 `WHEN_REQUIRED`;SigV4、預簽 PUT/GET、Content-MD5、
56
+ 基本自訂 metadata 與 multipart 已支援。未支援條件寫入、CopyObject、版本/生命週期、
57
+ 自訂加密,以及 Cache-Control/Content-Encoding 等寫入 metadata;會明確回錯,不靜默忽略。
58
+ 沒有 Cloudflare Terraform provider 相容層。
59
+
60
+ 静態檔案上限 25 MiB/每次最多 100,000 檔;R2 物件上限 5 TiB、S3 單次或分段上限
61
+ 5 GiB、最多 10,000 段。可用磁碟、請求逾時與併發容量仍會限制實際可接受大小。
62
+ 公開部署前拒絕私人路徑、符號連結與已知 credential 格式;此檢查不是所有機密的偵測器。
63
+ FUFU 沒有 CDN 快取,入口回覆 `Cache-Control: no-store`,不模擬一次成功的 cache purge。
64
+
20
65
  ## 安裝
21
66
 
22
67
  需要 Python 3.10 以上。Python 套件預設包含 YAML 相依套件:
@@ -26,8 +71,8 @@ pip install fufu-cloud-cli
26
71
  fufu --version
27
72
  ```
28
73
 
29
- npm 版本需要 Node.js 18 以上,以及已安裝的 Python 3.10 以上;四個入口
30
- 呼叫相同的 Python CLI。npm 包內附純 Python YAML parser,沒有安裝腳本,
74
+ npm 版本需要 Node.js 18 以上,以及已安裝的 Python 3.10 以上;各入口
75
+ 呼叫相同的 Python CLI。0.8.0 的 npm 建置包內附純 Python YAML/TOML parser,沒有安裝腳本,
31
76
  不會自行下載 Python 或執行 pip。
32
77
 
33
78
  ```bash
@@ -43,6 +88,27 @@ npx --no-install aws-fufu --help
43
88
 
44
89
  ## 憑證與 CI
45
90
 
91
+ 目前工作樹另提供 `fufu tofu`、`fufu pulumi`、`fufu state`。
92
+ Terraform/OpenTofu 可選擇租戶遠端 HTTP state,具備鎖、加密、版本與
93
+ 復原;需配套 Console API 和平台金鑰。Pulumi 使用固定官方 Node.js SDK,
94
+ 已驗收 SQS、SSM String、Pub/Sub topic/pull subscription,採獨立本機
95
+ DIY backend,秘密欄位需外部 passphrase。這些新能力尚未包含在已發布
96
+ 的 0.8.0,不能把套件安裝成功當成支援驗收。
97
+
98
+ 此工作樹也提供 `fufu terraform`:以官方 AWS/Google Terraform provider
99
+ 管理 Lambda ZIP、Cloud Run v2、IAM 與服務入口的明列子集,並使用獨立的
100
+ FUFU state。Terraform 需另行安裝,Python 使用 `fufu-cloud-cli[terraform]`
101
+ extra;npm 使用的 Python 也需要該 extra 的相依套件。使用 0.8.0 以上版本。
102
+ GCS/S3、Functions Gen 1/Gen 2 等一般 CLI 能力
103
+ 不表示官方 Terraform 資源已相容。正式環境使用正常 Terraform、正式認證
104
+ 與獨立 state,不可直接搬用 FUFU state。
105
+
106
+ 後續開發版另接入 SQS queue、SSM String parameter、Pub/Sub topic/pull
107
+ subscription;新增能力需要配套的 Console 與 Pub/Sub 後端更新,不能從
108
+ 已發布的 0.8.0 安裝成功就推定已具備。`gcloud-fufu pubsub` 提供建立、
109
+ 查詢、更新、刪除、publish/pull/ack 子集;不支援 push、ordering、
110
+ exactly-once、Function trigger 或保留期限保證。
111
+
46
112
  向 FUFU Cloud 管理者取得該租戶、該 provider 的專用 credential JSON。
47
113
  CLI 不附預設帳號、共用金鑰或可用的租戶憑證,也不讀取 `.env`、瀏覽器
48
114
  Cookie 或正式雲端登入工作階段。
@@ -54,11 +120,13 @@ export FUFU_BACKEND=api
54
120
  export FUFU_AWS_CREDENTIAL_FILE=/run/secrets/fufu-aws.json
55
121
  export FUFU_GCP_CREDENTIAL_FILE=/run/secrets/fufu-gcp.json
56
122
  export FUFU_AZURE_CREDENTIAL_FILE=/run/secrets/fufu-azure.json
123
+ export FUFU_CLOUDFLARE_CREDENTIAL_FILE=/run/secrets/fufu-cloudflare.json
57
124
 
58
125
  fufu auth whoami --provider aws
59
126
  aws-fufu sts get-caller-identity
60
127
  gcloud-fufu auth list
61
128
  az-fufu account show
129
+ wrangler-fufu whoami
62
130
  ```
63
131
 
64
132
  credential 內含 FUFU endpoint。`FUFU_ENDPOINT` 可明確覆寫目的地:僅限
@@ -74,6 +142,32 @@ Lambda Function URL 的 AWS_IAM 支援長效 FUFU access key 的 SigV4 header si
74
142
 
75
143
  ## 部署與支援範圍
76
144
 
145
+ 0.9.0 的配套後端新增 AWS/GCP Secrets Manager、GCP 部署秘密參照、
146
+ Cloud Run SSE/WebSocket、HTTP Functions SSE,以及目前運行版本的身分續期。
147
+ 服務端須配置私有加密金鑰;未配置時會明確拒絕秘密操作。
148
+
149
+ ```bash
150
+ aws-fufu secretsmanager create-secret --name app-key --secret-string file://./dev-secret.txt
151
+ aws-fufu secretsmanager get-secret-value --secret-id app-key --out-file ./private-result.txt
152
+ gcloud-fufu secrets create app-key --data-file ./dev-secret.txt
153
+ gcloud-fufu run deploy example --source ./prepared-asgi.zip \
154
+ --service-account "$WORKLOAD_SERVICE_ACCOUNT" --set-secrets APP_KEY=app-key:1
155
+ ```
156
+
157
+ 秘密值只由受控檔案或 `--secret-env VARIABLE` 輸入;讀取需指定私有新檔案,
158
+ 不會輸出到終端。Cloud Run/Functions 的 service account 需 Secret Accessor
159
+ 權限。支援 `--update-secrets`、`--clear-secrets` 和 Knative `secretKeyRef`。
160
+ 這是本機加密版本管理子集:AWS 為 UTF-8 SecretString,單值最大 64 KiB;
161
+ 無 KMS、rotation、跨區域複寫、排程刪除與復原。立即刪除或銷毀後,加密
162
+ 備份與既有容器可能仍持有舊值。CLI metadata 採 FUFU 共用形狀,並非完整
163
+ 官方 JSON;GCP runtime 使用明確 helper,無全量 Google SDK/ADC 替換。
164
+ 新的秘密資源與 Terraform/OpenTofu/Pulumi 參照尚未接通。
165
+
166
+ Cloud Run 串流回應不設原本的 32 MiB 緩衝上限,仍受服務期限及容量限制。
167
+ HTTP Functions Gen 1/Gen 2 支援 SSE、無 WebSocket;Gen 2 保留較寬鬆的
168
+ 32 MiB response,而官方 streaming response 是 10 MB。Lambda 仍為 buffered。
169
+ 公網 DNS、TLS、WAF 及 CDN 期限須另外驗證。
170
+
77
171
  ```bash
78
172
  fufu capabilities
79
173
  fufu capabilities --remote --provider gcp --require cloud-run-http
@@ -88,7 +182,7 @@ aws-fufu lambda list-functions
88
182
  接受已封裝 Uvicorn 與相依套件的 Python 3.12 ASGI ZIP/乾淨目錄,自動離線建置、
89
183
  檢查映像並核准到目前租戶;不需要主機 SSH 或每次人工核准。
90
184
  `--entry-point module:app` 是 FUFU 擴充旗標,預設 `main:app`。
91
- 來源上限為 ZIP 6 MiB、解壓縮 32 MiB、2000 個檔案;不接受隱藏檔、Dockerfile、
185
+ 目前來源版本的 FUFU 配方上限為 ZIP 512 MiB、解壓縮 2 GiB、100000 個檔案;不接受隱藏檔、Dockerfile、
92
186
  自訂安裝設定或非空 requirements.txt。相依套件需在 CI 封裝,服務端不執行安裝 hook。
93
187
  這是固定 ASGI 配方,不提供完整 Cloud Build 或 registry API。
94
188
  成功回應包含 `build.status`、`imageDigest` 與來源包雜湊;自動核准映像
@@ -103,8 +197,8 @@ CLI 無法判斷任意原始碼或 ZIP 內的資料是否可以分享。
103
197
  | --- | --- |
104
198
  | Cloud Run | 已核准 HTTP 容器或 API 自動建置 Python ASGI 來源;支援配置後的 HTTPS 入口、Invoker policy;無 autoscaling、traffic splitting、Jobs |
105
199
  | Cloud Run functions / Functions Gen 1 | Python 3.12 HTTP 函式;Gen 1 為相同 HTTP runtime 的模擬子集,無背景事件 |
106
- | AWS Lambda | Python 3.10 / 3.12 ZIP、同步 invoke、程式碼更新、BUFFERED Function URL;遠端 ZIP 上限 6 MiB |
107
- | GCS / S3 | bucket 與 object 操作子集;單一 object 上限 6 MiB,無 versioning / signed URL |
200
+ | AWS Lambda | Python 3.10 / 3.12 ZIP、同步 invoke、程式碼更新、BUFFERED Function URL;ZIP 50 MiB/解壓 250 MiB,同步 payload 6 MiB |
201
+ | GCS / S3 | bucket 與 object 子集;分段上傳/串流下載,受主機磁碟限制;已驗證超過 1 GiB,無 versioning / signed URL |
108
202
  | DynamoDB / SQS / SSM | 明列資料操作;無完整交易、Streams、FIFO 或 KMS / SecureString |
109
203
  | AWS IAM / GCP IAM | 租戶內 role / service account / policy 子集;default deny、限定 session;無原生雲端全量 IAM 語義 |
110
204
  | API Gateway / Serverless profile | 簡化 HTTP Lambda proxy 和明確來源清單;不執行 plugins / CloudFormation |
@@ -142,7 +236,8 @@ UI 與 CLI 共用同一份服務政策。Cloud Run 與 HTTP Functions 的匿名
142
236
  `allUsers` 與對應 Invoker 角色。Lambda `NONE` 必須另加 `lambda:InvokeFunctionUrl`
143
237
  (`--function-url-auth-type NONE`)和 `lambda:InvokeFunction`
144
238
  (`--invoked-via-function-url`)兩項資源權限;只設定 NONE 不會自動開放呼叫。
145
- 入口支援有界限的普通 HTTP:請求與回應各 6 MiB、執行 15 秒,每服務每分鐘 120 次。
239
+ 入口支援有界限的普通 HTTP:Lambda 請求與回應各 6 MiB、Gen 1 各 10 MiB、Run/Gen 2 各 32 MiB。
240
+ 依服務設定等待,期限上限分別為 900/540/3600 秒;外部 CDN 期限另計。每服務每分鐘 6000 次,另有主機容量背壓。
146
241
  不支援 SSE、WebSocket、串流、STS 暫時金鑰、presigned URL 或真實 VPC。
147
242
  GCP ID token 由 FUFU 的 RS256 issuer 簽發,綁定精確 audience,15 分鐘後過期;不接受 Google ID token。
148
243
  `print-identity-token` 明確輸出秘密,應直接放入受控變數,避免 CI 日誌或 shell tracing。
@@ -1,9 +1,49 @@
1
1
  # FUFU Cloud CLI
2
2
 
3
- 以四個指令操作 FUFU Cloud 的租戶隔離開發環境:`fufu`、`aws-fufu`、
4
- `gcloud-fufu`、`az-fufu`。這是 AWS、Google Cloud 與 Azure 的**明確子集**,
3
+ 以五個指令操作 FUFU Cloud 的租戶隔離開發環境:`fufu`、`aws-fufu`、
4
+ `gcloud-fufu`、`az-fufu`、`wrangler-fufu`。這是 AWS、Google Cloud、Azure 與 Cloudflare 的**明確子集**,
5
5
  只供 FUFU mock 環境使用;不會把部署轉送到正式雲端。
6
6
 
7
+ 0.8.0 包含資源配額、分段傳輸、Terraform 核心子集與 Cloudflare 靜態部署/R2。需要同步更新服務端;CLI 安裝成功不表示任意 FUFU 服務端都已有這些能力。
8
+
9
+ ## Cloudflare 前端部署與 R2
10
+
11
+ ```bash
12
+ pip install 'fufu-cloud-cli==0.8.0'
13
+ export FUFU_CLOUDFLARE_CREDENTIAL_FILE=/run/secrets/fufu-cloudflare.json
14
+ wrangler-fufu capabilities
15
+ wrangler-fufu pages project create frontend --production-branch main
16
+ npm run build
17
+ wrangler-fufu pages deploy dist --project-name frontend --branch main
18
+ wrangler-fufu pages deployment list --project-name frontend
19
+ wrangler-fufu deploy --assets dist --name frontend-assets
20
+ wrangler-fufu r2 bucket create frontend-files
21
+ wrangler-fufu r2 object put frontend-files/example.txt --file example.txt --remote
22
+ ```
23
+
24
+ 支援 Wrangler JSON、JSONC、TOML,以及 `--config`、`--cwd`、`--env`。Pages 使用
25
+ `pages_build_output_dir`;Workers Static Assets 使用 `assets.directory`,SPA 可明確設定
26
+ `assets.not_found_handling = "single-page-application"`。正式與預覽網址分開,正式部署完整
27
+ 上傳且校驗通過才切换入口;Pages 可用 `pages deployment rollback ID --project-name NAME` 回滾。
28
+
29
+ 這些命令提供真正的靜態 HTTP hosting,**不執行 Workers JavaScript 或 Pages Functions**。
30
+ KV、D1、Durable Objects、Queues、Cron、自訂網域、SSR 與全球 CDN 快取未實作;
31
+ 相關配置會被拒絕。API credential、S3 endpoint 與帳號識別使用 FUFU 值。
32
+ 目前不讀取官方 `CLOUDFLARE_API_TOKEN`;使用 FUFU credential JSON,避免誤用正式金鑰。
33
+
34
+ R2 支援 bucket/object、分段上傳、範圍下載、CORS、預設私人及可開關公開開發網址。
35
+ 原生 boto3/AWS S3 SDK 可使用 credential JSON 中的 `r2_endpoint`、`r2_access_key_id`、
36
+ `r2_secret_access_key`,region 為 `auto` 且使用 path-style addressing。
37
+ SDK checksum 配置使用 `WHEN_REQUIRED`;SigV4、預簽 PUT/GET、Content-MD5、
38
+ 基本自訂 metadata 與 multipart 已支援。未支援條件寫入、CopyObject、版本/生命週期、
39
+ 自訂加密,以及 Cache-Control/Content-Encoding 等寫入 metadata;會明確回錯,不靜默忽略。
40
+ 沒有 Cloudflare Terraform provider 相容層。
41
+
42
+ 静態檔案上限 25 MiB/每次最多 100,000 檔;R2 物件上限 5 TiB、S3 單次或分段上限
43
+ 5 GiB、最多 10,000 段。可用磁碟、請求逾時與併發容量仍會限制實際可接受大小。
44
+ 公開部署前拒絕私人路徑、符號連結與已知 credential 格式;此檢查不是所有機密的偵測器。
45
+ FUFU 沒有 CDN 快取,入口回覆 `Cache-Control: no-store`,不模擬一次成功的 cache purge。
46
+
7
47
  ## 安裝
8
48
 
9
49
  需要 Python 3.10 以上。Python 套件預設包含 YAML 相依套件:
@@ -13,8 +53,8 @@ pip install fufu-cloud-cli
13
53
  fufu --version
14
54
  ```
15
55
 
16
- npm 版本需要 Node.js 18 以上,以及已安裝的 Python 3.10 以上;四個入口
17
- 呼叫相同的 Python CLI。npm 包內附純 Python YAML parser,沒有安裝腳本,
56
+ npm 版本需要 Node.js 18 以上,以及已安裝的 Python 3.10 以上;各入口
57
+ 呼叫相同的 Python CLI。0.8.0 的 npm 建置包內附純 Python YAML/TOML parser,沒有安裝腳本,
18
58
  不會自行下載 Python 或執行 pip。
19
59
 
20
60
  ```bash
@@ -30,6 +70,27 @@ npx --no-install aws-fufu --help
30
70
 
31
71
  ## 憑證與 CI
32
72
 
73
+ 目前工作樹另提供 `fufu tofu`、`fufu pulumi`、`fufu state`。
74
+ Terraform/OpenTofu 可選擇租戶遠端 HTTP state,具備鎖、加密、版本與
75
+ 復原;需配套 Console API 和平台金鑰。Pulumi 使用固定官方 Node.js SDK,
76
+ 已驗收 SQS、SSM String、Pub/Sub topic/pull subscription,採獨立本機
77
+ DIY backend,秘密欄位需外部 passphrase。這些新能力尚未包含在已發布
78
+ 的 0.8.0,不能把套件安裝成功當成支援驗收。
79
+
80
+ 此工作樹也提供 `fufu terraform`:以官方 AWS/Google Terraform provider
81
+ 管理 Lambda ZIP、Cloud Run v2、IAM 與服務入口的明列子集,並使用獨立的
82
+ FUFU state。Terraform 需另行安裝,Python 使用 `fufu-cloud-cli[terraform]`
83
+ extra;npm 使用的 Python 也需要該 extra 的相依套件。使用 0.8.0 以上版本。
84
+ GCS/S3、Functions Gen 1/Gen 2 等一般 CLI 能力
85
+ 不表示官方 Terraform 資源已相容。正式環境使用正常 Terraform、正式認證
86
+ 與獨立 state,不可直接搬用 FUFU state。
87
+
88
+ 後續開發版另接入 SQS queue、SSM String parameter、Pub/Sub topic/pull
89
+ subscription;新增能力需要配套的 Console 與 Pub/Sub 後端更新,不能從
90
+ 已發布的 0.8.0 安裝成功就推定已具備。`gcloud-fufu pubsub` 提供建立、
91
+ 查詢、更新、刪除、publish/pull/ack 子集;不支援 push、ordering、
92
+ exactly-once、Function trigger 或保留期限保證。
93
+
33
94
  向 FUFU Cloud 管理者取得該租戶、該 provider 的專用 credential JSON。
34
95
  CLI 不附預設帳號、共用金鑰或可用的租戶憑證,也不讀取 `.env`、瀏覽器
35
96
  Cookie 或正式雲端登入工作階段。
@@ -41,11 +102,13 @@ export FUFU_BACKEND=api
41
102
  export FUFU_AWS_CREDENTIAL_FILE=/run/secrets/fufu-aws.json
42
103
  export FUFU_GCP_CREDENTIAL_FILE=/run/secrets/fufu-gcp.json
43
104
  export FUFU_AZURE_CREDENTIAL_FILE=/run/secrets/fufu-azure.json
105
+ export FUFU_CLOUDFLARE_CREDENTIAL_FILE=/run/secrets/fufu-cloudflare.json
44
106
 
45
107
  fufu auth whoami --provider aws
46
108
  aws-fufu sts get-caller-identity
47
109
  gcloud-fufu auth list
48
110
  az-fufu account show
111
+ wrangler-fufu whoami
49
112
  ```
50
113
 
51
114
  credential 內含 FUFU endpoint。`FUFU_ENDPOINT` 可明確覆寫目的地:僅限
@@ -61,6 +124,32 @@ Lambda Function URL 的 AWS_IAM 支援長效 FUFU access key 的 SigV4 header si
61
124
 
62
125
  ## 部署與支援範圍
63
126
 
127
+ 0.9.0 的配套後端新增 AWS/GCP Secrets Manager、GCP 部署秘密參照、
128
+ Cloud Run SSE/WebSocket、HTTP Functions SSE,以及目前運行版本的身分續期。
129
+ 服務端須配置私有加密金鑰;未配置時會明確拒絕秘密操作。
130
+
131
+ ```bash
132
+ aws-fufu secretsmanager create-secret --name app-key --secret-string file://./dev-secret.txt
133
+ aws-fufu secretsmanager get-secret-value --secret-id app-key --out-file ./private-result.txt
134
+ gcloud-fufu secrets create app-key --data-file ./dev-secret.txt
135
+ gcloud-fufu run deploy example --source ./prepared-asgi.zip \
136
+ --service-account "$WORKLOAD_SERVICE_ACCOUNT" --set-secrets APP_KEY=app-key:1
137
+ ```
138
+
139
+ 秘密值只由受控檔案或 `--secret-env VARIABLE` 輸入;讀取需指定私有新檔案,
140
+ 不會輸出到終端。Cloud Run/Functions 的 service account 需 Secret Accessor
141
+ 權限。支援 `--update-secrets`、`--clear-secrets` 和 Knative `secretKeyRef`。
142
+ 這是本機加密版本管理子集:AWS 為 UTF-8 SecretString,單值最大 64 KiB;
143
+ 無 KMS、rotation、跨區域複寫、排程刪除與復原。立即刪除或銷毀後,加密
144
+ 備份與既有容器可能仍持有舊值。CLI metadata 採 FUFU 共用形狀,並非完整
145
+ 官方 JSON;GCP runtime 使用明確 helper,無全量 Google SDK/ADC 替換。
146
+ 新的秘密資源與 Terraform/OpenTofu/Pulumi 參照尚未接通。
147
+
148
+ Cloud Run 串流回應不設原本的 32 MiB 緩衝上限,仍受服務期限及容量限制。
149
+ HTTP Functions Gen 1/Gen 2 支援 SSE、無 WebSocket;Gen 2 保留較寬鬆的
150
+ 32 MiB response,而官方 streaming response 是 10 MB。Lambda 仍為 buffered。
151
+ 公網 DNS、TLS、WAF 及 CDN 期限須另外驗證。
152
+
64
153
  ```bash
65
154
  fufu capabilities
66
155
  fufu capabilities --remote --provider gcp --require cloud-run-http
@@ -75,7 +164,7 @@ aws-fufu lambda list-functions
75
164
  接受已封裝 Uvicorn 與相依套件的 Python 3.12 ASGI ZIP/乾淨目錄,自動離線建置、
76
165
  檢查映像並核准到目前租戶;不需要主機 SSH 或每次人工核准。
77
166
  `--entry-point module:app` 是 FUFU 擴充旗標,預設 `main:app`。
78
- 來源上限為 ZIP 6 MiB、解壓縮 32 MiB、2000 個檔案;不接受隱藏檔、Dockerfile、
167
+ 目前來源版本的 FUFU 配方上限為 ZIP 512 MiB、解壓縮 2 GiB、100000 個檔案;不接受隱藏檔、Dockerfile、
79
168
  自訂安裝設定或非空 requirements.txt。相依套件需在 CI 封裝,服務端不執行安裝 hook。
80
169
  這是固定 ASGI 配方,不提供完整 Cloud Build 或 registry API。
81
170
  成功回應包含 `build.status`、`imageDigest` 與來源包雜湊;自動核准映像
@@ -90,8 +179,8 @@ CLI 無法判斷任意原始碼或 ZIP 內的資料是否可以分享。
90
179
  | --- | --- |
91
180
  | Cloud Run | 已核准 HTTP 容器或 API 自動建置 Python ASGI 來源;支援配置後的 HTTPS 入口、Invoker policy;無 autoscaling、traffic splitting、Jobs |
92
181
  | Cloud Run functions / Functions Gen 1 | Python 3.12 HTTP 函式;Gen 1 為相同 HTTP runtime 的模擬子集,無背景事件 |
93
- | AWS Lambda | Python 3.10 / 3.12 ZIP、同步 invoke、程式碼更新、BUFFERED Function URL;遠端 ZIP 上限 6 MiB |
94
- | GCS / S3 | bucket 與 object 操作子集;單一 object 上限 6 MiB,無 versioning / signed URL |
182
+ | AWS Lambda | Python 3.10 / 3.12 ZIP、同步 invoke、程式碼更新、BUFFERED Function URL;ZIP 50 MiB/解壓 250 MiB,同步 payload 6 MiB |
183
+ | GCS / S3 | bucket 與 object 子集;分段上傳/串流下載,受主機磁碟限制;已驗證超過 1 GiB,無 versioning / signed URL |
95
184
  | DynamoDB / SQS / SSM | 明列資料操作;無完整交易、Streams、FIFO 或 KMS / SecureString |
96
185
  | AWS IAM / GCP IAM | 租戶內 role / service account / policy 子集;default deny、限定 session;無原生雲端全量 IAM 語義 |
97
186
  | API Gateway / Serverless profile | 簡化 HTTP Lambda proxy 和明確來源清單;不執行 plugins / CloudFormation |
@@ -129,7 +218,8 @@ UI 與 CLI 共用同一份服務政策。Cloud Run 與 HTTP Functions 的匿名
129
218
  `allUsers` 與對應 Invoker 角色。Lambda `NONE` 必須另加 `lambda:InvokeFunctionUrl`
130
219
  (`--function-url-auth-type NONE`)和 `lambda:InvokeFunction`
131
220
  (`--invoked-via-function-url`)兩項資源權限;只設定 NONE 不會自動開放呼叫。
132
- 入口支援有界限的普通 HTTP:請求與回應各 6 MiB、執行 15 秒,每服務每分鐘 120 次。
221
+ 入口支援有界限的普通 HTTP:Lambda 請求與回應各 6 MiB、Gen 1 各 10 MiB、Run/Gen 2 各 32 MiB。
222
+ 依服務設定等待,期限上限分別為 900/540/3600 秒;外部 CDN 期限另計。每服務每分鐘 6000 次,另有主機容量背壓。
133
223
  不支援 SSE、WebSocket、串流、STS 暫時金鑰、presigned URL 或真實 VPC。
134
224
  GCP ID token 由 FUFU 的 RS256 issuer 簽發,綁定精確 audience,15 分鐘後過期;不接受 Google ID token。
135
225
  `print-identity-token` 明確輸出秘密,應直接放入受控變數,避免 CI 日誌或 shell tracing。
@@ -4,25 +4,28 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "fufu-cloud-cli"
7
- version = "0.7.0"
7
+ version = "0.9.0"
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"
11
- dependencies = ["PyYAML==6.0.3"]
11
+ dependencies = ["PyYAML==6.0.3", "tomli==2.2.1; python_version<'3.11'"]
12
12
  license = "LicenseRef-Proprietary"
13
13
  license-files = ["LICENSE"]
14
14
 
15
15
  [project.optional-dependencies]
16
16
  yaml = ["PyYAML==6.0.3"]
17
+ test = ["boto3==1.40.66"]
18
+ terraform = ["python-hcl2==7.3.1"]
17
19
 
18
20
  [project.scripts]
19
21
  fufu = "fufu_cloud_cli.cli:main"
20
22
  aws-fufu = "fufu_cloud_cli.cli:aws_main"
21
23
  gcloud-fufu = "fufu_cloud_cli.cli:gcloud_main"
22
24
  az-fufu = "fufu_cloud_cli.cli:az_main"
25
+ wrangler-fufu = "fufu_cloud_cli.cli:wrangler_main"
23
26
 
24
27
  [tool.setuptools.packages.find]
25
28
  where = ["src"]
26
29
 
27
30
  [tool.setuptools.package-data]
28
- fufu_cloud_cli = ["capabilities.json", "runtime/*/Dockerfile", "runtime/ingress/proxy.py"]
31
+ fufu_cloud_cli = ["capabilities.json", "pulumi-endpoints.json", "runtime/*/Dockerfile", "runtime/ingress/proxy.py"]
@@ -1,3 +1,3 @@
1
1
  """FUFU CLI 與本機部署工具;相容範圍見 capabilities.json。"""
2
2
 
3
- __version__ = "0.7.0"
3
+ __version__ = "0.9.0"
@@ -15,8 +15,10 @@ from urllib.request import HTTPRedirectHandler, HTTPSHandler, ProxyHandler, Requ
15
15
 
16
16
  from .errors import FufuError, require
17
17
 
18
- PROVIDERS = {"aws", "gcp", "azure"}
19
- MAX_RESPONSE = 2 * 1024 * 1024
18
+ PROVIDERS = {"aws", "gcp", "azure", "cloudflare"}
19
+ from .limits import JSON_BODY_BYTES, JSON_RESPONSE_BYTES
20
+
21
+ MAX_RESPONSE = 8 * 1024 * 1024
20
22
 
21
23
 
22
24
  @contextmanager
@@ -71,7 +73,7 @@ def api_selected():
71
73
  return False
72
74
  return mode == "api" or any(os.environ.get(k) for k in (
73
75
  "FUFU_ENDPOINT", "FUFU_CREDENTIAL_FILE", "FUFU_AWS_CREDENTIAL_FILE",
74
- "FUFU_GCP_CREDENTIAL_FILE", "FUFU_AZURE_CREDENTIAL_FILE", 'FUFU_SESSION_FILE')) or os.environ.get("AWS_ACCESS_KEY_ID", "").startswith("FUFU")
76
+ "FUFU_GCP_CREDENTIAL_FILE", "FUFU_AZURE_CREDENTIAL_FILE", "FUFU_CLOUDFLARE_CREDENTIAL_FILE", 'FUFU_SESSION_FILE')) or os.environ.get("AWS_ACCESS_KEY_ID", "").startswith("FUFU")
75
77
 
76
78
 
77
79
  def load_credential(provider=None, filename=None):
@@ -93,7 +95,7 @@ def load_credential(provider=None, filename=None):
93
95
  and data.get("fufu_provider") in PROVIDERS, "INVALID_CREDENTIAL", "需要 FUFU 專用憑證;不接受正式雲端金鑰。")
94
96
  actual = data["fufu_provider"]
95
97
  require(provider is None or actual == provider, "PROVIDER_MISMATCH", "憑證所屬 provider 不符。")
96
- fields = {"aws": ("aws_access_key_id", "aws_secret_access_key"), "gcp": ("client_id", "client_secret"), "azure": ("clientId", "clientSecret")}
98
+ fields = {"aws": ("aws_access_key_id", "aws_secret_access_key"), "gcp": ("client_id", "client_secret"), "azure": ("clientId", "clientSecret"), "cloudflare": ("credential_id", "api_token")}
97
99
  key, secret = (data.get(k) for k in fields[actual])
98
100
  require(isinstance(key, str) and re.fullmatch(r"FUFU" + actual.upper() + r"[A-Za-z0-9_-]{12,100}", key)
99
101
  and isinstance(secret, str) and 24 <= len(secret) <= 1024,
@@ -160,30 +162,48 @@ class ApiClient:
160
162
  headers["Authorization"] = f"Bearer {self.token}"
161
163
  data = None if body is None else json.dumps(body).encode("utf-8")
162
164
  if data is not None:
163
- require(len(data) <= (12 * 1024**2 if large else 65536), "PAYLOAD_LIMIT", "API JSON payload 超過上限。")
165
+ require(len(data) <= (JSON_BODY_BYTES if large else 1024**2), "PAYLOAD_LIMIT", "API JSON payload 超過上限。")
164
166
  headers["Content-Type"] = "application/json"
165
167
  req = Request(self.endpoint + path, data=data, headers=headers, method=method)
166
168
  try:
167
- with request_deadline(), self.opener.open(req, timeout=15) as response:
168
- limit = 9 * 1024**2 if large else MAX_RESPONSE
169
+ duration = min(3630, max(60, int(body.get("timeout", 30)) + 30)) if large and isinstance(body, dict) else 60
170
+ if '/objects/uploads/' in path and path.endswith('/complete'): duration = 3630
171
+ with request_deadline(duration), self.opener.open(req, timeout=duration) as response:
172
+ limit = JSON_RESPONSE_BYTES if large else MAX_RESPONSE
169
173
  raw = response.read(limit + 1)
170
174
  require(len(raw) <= limit, "RESPONSE_LIMIT", "API 回覆超過大小限制。")
171
175
  return json.loads(raw)
172
176
  except HTTPError as exc:
173
177
  status = exc.code
174
178
  exc.close()
175
- codes = {401: "AUTHENTICATION_FAILED", 403: "ACCESS_DENIED", 404: "RESOURCE_NOT_FOUND", 409: "RESOURCE_CONFLICT", 429: "RATE_LIMITED"}
179
+ codes = {401: "AUTHENTICATION_FAILED", 403: "ACCESS_DENIED", 404: "RESOURCE_NOT_FOUND", 409: "RESOURCE_CONFLICT", 410: "UPLOAD_EXPIRED", 413: "PAYLOAD_LIMIT", 429: "RATE_LIMITED", 507: "STORAGE_CAPACITY"}
180
+ if status == 507: raise FufuError('STORAGE_CAPACITY', 'FUFU 主機磁碟容量不足;這是目前可用空間的限制,並非雲端物件大小上限。') from None
176
181
  raise FufuError(codes.get(status, "API_REQUEST_FAILED"), f"FUFU API 拒絕請求(HTTP {status});未輸出憑證或原始回應。") from None
177
182
  except (URLError, TimeoutError, ConnectionError) as exc:
178
183
  raise FufuError("API_UNREACHABLE", "無法在時限內安全連線至 FUFU API。") from None
179
184
 
180
185
  def request(self, method, path, body=None):
181
- return self._request(method, f"/api/cloud/{self.provider}" + path, body, large=path.startswith(('/compute/', '/s3/', '/storage/', '/control/native')))
186
+ return self._request(method, f"/api/cloud/{self.provider}" + path, body, large=path.startswith(('/compute/', '/s3/', '/storage/', '/control/native', '/control/pubsub', '/sites')))
182
187
 
183
188
  def whoami(self):
184
189
  data = self._request("GET", "/api/auth/whoami")
185
190
  return {k: v for k, v in data.items() if k != "credentialId"}
186
191
 
192
+ def binary(self, method, path, stream=None, length=None):
193
+ require((path.startswith('/objects/') or self.provider == 'cloudflare' and path.startswith(('/sites/', '/r2/')))
194
+ and '\r' not in path and '\n' not in path, 'INVALID_REQUEST', '物件傳輸路徑無效。')
195
+ headers = {'Authorization': 'Bearer '+self.token, 'Content-Type': 'application/octet-stream'}
196
+ if length is not None: headers['Content-Length'] = str(length)
197
+ request = Request(self.endpoint+f'/api/cloud/{self.provider}'+path, data=stream, headers=headers, method=method)
198
+ try:
199
+ response = self.opener.open(request, timeout=300)
200
+ if method == 'GET': return response
201
+ with response:
202
+ return json.loads(response.read(65537))
203
+ except HTTPError as error:
204
+ status = error.code; error.close()
205
+ raise FufuError('OBJECT_TRANSFER_FAILED', f'物件傳輸未完成(HTTP {status});未覆寫下載目的地。') from None
206
+
187
207
  def close(self):
188
208
  if self.token and self.owns_token:
189
209
  try:
@@ -279,12 +299,11 @@ def remote_command(provider, argv, Parser):
279
299
  if a.action == "buckets" and a.bucket_action == "list":
280
300
  return api.request("GET", "/storage/buckets")
281
301
  if a.action == 'cp' and a.source.startswith('gs://'):
282
- from .cloud_data import download_bytes
302
+ from .object_transfer import download_file
283
303
  require(not a.destination.startswith('gs://'), 'UNSUPPORTED_ARGUMENT', 'cp 目前只支援本機與 Storage 之間傳輸。')
284
304
  bucket, key = storage_path(a.source)
285
305
  require(bool(key), 'INVALID_RESOURCE', '請指定 object 名稱。')
286
- result = api.request('GET', '/storage/buckets/' + bucket + '/object?' + urlencode({'name': key, 'encoding': 'base64'}))
287
- return download_bytes(a.destination, result['contentBase64'])
306
+ return download_file(api, a.destination, bucket, key)
288
307
  bucket, key = storage_path(a.destination if a.action == "cp" else a.url)
289
308
  path = "/storage/buckets/" + bucket
290
309
  if a.action == "buckets":
@@ -298,5 +317,5 @@ def remote_command(provider, argv, Parser):
298
317
  return api.request("GET", path + "/object?" + urlencode({"name": key}))
299
318
  if a.action == "rm":
300
319
  return api.request("DELETE", path + "/object?" + urlencode({"name": key}))
301
- from .cloud_data import upload_bytes
302
- return api.request("POST", path + "/objects", {"name": key, "contentBase64": upload_bytes(a.source)})
320
+ from .object_transfer import upload_file
321
+ return upload_file(api, a.source, bucket, key)