fufu-cloud-cli 0.6.0__tar.gz → 0.7.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.
- {fufu_cloud_cli-0.6.0/src/fufu_cloud_cli.egg-info → fufu_cloud_cli-0.7.0}/PKG-INFO +28 -4
- {fufu_cloud_cli-0.6.0 → fufu_cloud_cli-0.7.0}/README.md +27 -3
- {fufu_cloud_cli-0.6.0 → fufu_cloud_cli-0.7.0}/pyproject.toml +1 -1
- {fufu_cloud_cli-0.6.0 → fufu_cloud_cli-0.7.0}/src/fufu_cloud_cli/__init__.py +1 -1
- {fufu_cloud_cli-0.6.0 → fufu_cloud_cli-0.7.0}/src/fufu_cloud_cli/capabilities.json +8 -1
- {fufu_cloud_cli-0.6.0 → fufu_cloud_cli-0.7.0}/src/fufu_cloud_cli/cli.py +29 -10
- {fufu_cloud_cli-0.6.0 → fufu_cloud_cli-0.7.0}/src/fufu_cloud_cli/docker_backend.py +9 -3
- {fufu_cloud_cli-0.6.0 → fufu_cloud_cli-0.7.0}/src/fufu_cloud_cli/manifest.py +11 -3
- fufu_cloud_cli-0.7.0/src/fufu_cloud_cli/public_access.py +171 -0
- {fufu_cloud_cli-0.6.0 → fufu_cloud_cli-0.7.0/src/fufu_cloud_cli.egg-info}/PKG-INFO +28 -4
- {fufu_cloud_cli-0.6.0 → fufu_cloud_cli-0.7.0}/src/fufu_cloud_cli.egg-info/SOURCES.txt +1 -0
- {fufu_cloud_cli-0.6.0 → fufu_cloud_cli-0.7.0}/LICENSE +0 -0
- {fufu_cloud_cli-0.6.0 → fufu_cloud_cli-0.7.0}/MANIFEST.in +0 -0
- {fufu_cloud_cli-0.6.0 → fufu_cloud_cli-0.7.0}/setup.cfg +0 -0
- {fufu_cloud_cli-0.6.0 → fufu_cloud_cli-0.7.0}/src/fufu_cloud_cli/__main__.py +0 -0
- {fufu_cloud_cli-0.6.0 → fufu_cloud_cli-0.7.0}/src/fufu_cloud_cli/api_backend.py +0 -0
- {fufu_cloud_cli-0.6.0 → fufu_cloud_cli-0.7.0}/src/fufu_cloud_cli/cloud_data.py +0 -0
- {fufu_cloud_cli-0.6.0 → fufu_cloud_cli-0.7.0}/src/fufu_cloud_cli/control.py +0 -0
- {fufu_cloud_cli-0.6.0 → fufu_cloud_cli-0.7.0}/src/fufu_cloud_cli/errors.py +0 -0
- {fufu_cloud_cli-0.6.0 → fufu_cloud_cli-0.7.0}/src/fufu_cloud_cli/lambda_adapter.py +0 -0
- {fufu_cloud_cli-0.6.0 → fufu_cloud_cli-0.7.0}/src/fufu_cloud_cli/remote_compute.py +0 -0
- {fufu_cloud_cli-0.6.0 → fufu_cloud_cli-0.7.0}/src/fufu_cloud_cli/runtime/functions/Dockerfile +0 -0
- {fufu_cloud_cli-0.6.0 → fufu_cloud_cli-0.7.0}/src/fufu_cloud_cli/runtime/ingress/Dockerfile +0 -0
- {fufu_cloud_cli-0.6.0 → fufu_cloud_cli-0.7.0}/src/fufu_cloud_cli/runtime/ingress/proxy.py +0 -0
- {fufu_cloud_cli-0.6.0 → fufu_cloud_cli-0.7.0}/src/fufu_cloud_cli/runtime_sdk.py +0 -0
- {fufu_cloud_cli-0.6.0 → fufu_cloud_cli-0.7.0}/src/fufu_cloud_cli/serverless.py +0 -0
- {fufu_cloud_cli-0.6.0 → fufu_cloud_cli-0.7.0}/src/fufu_cloud_cli.egg-info/dependency_links.txt +0 -0
- {fufu_cloud_cli-0.6.0 → fufu_cloud_cli-0.7.0}/src/fufu_cloud_cli.egg-info/entry_points.txt +0 -0
- {fufu_cloud_cli-0.6.0 → fufu_cloud_cli-0.7.0}/src/fufu_cloud_cli.egg-info/requires.txt +0 -0
- {fufu_cloud_cli-0.6.0 → fufu_cloud_cli-0.7.0}/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.
|
|
3
|
+
Version: 0.7.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
|
|
@@ -69,7 +69,8 @@ credential 內含 FUFU endpoint。`FUFU_ENDPOINT` 可明確覆寫目的地:僅
|
|
|
69
69
|
一般命令交換短期權杖並在結束時嘗試撤銷。`fufu auth assume-role`
|
|
70
70
|
可建立受限 session 檔,檔案以排他方式建立且權限為 `0600`;請勿提交。
|
|
71
71
|
FUFU credential 與 session 檔仍屬秘密,必須由作業系統和 CI 控制存取。
|
|
72
|
-
原生雲端的 GitHub auth actions、OIDC
|
|
72
|
+
原生雲端的 GitHub auth actions、OIDC 和 OAuth 不直接相容。
|
|
73
|
+
Lambda Function URL 的 AWS_IAM 支援長效 FUFU access key 的 SigV4 header signing;管理 API 仍使用 FUFU token。
|
|
73
74
|
|
|
74
75
|
## 部署與支援範圍
|
|
75
76
|
|
|
@@ -100,9 +101,9 @@ CLI 無法判斷任意原始碼或 ZIP 內的資料是否可以分享。
|
|
|
100
101
|
|
|
101
102
|
| 服務 | 可用範圍與主要限制 |
|
|
102
103
|
| --- | --- |
|
|
103
|
-
| Cloud Run | 已核准 HTTP 容器或 API 自動建置 Python ASGI
|
|
104
|
+
| Cloud Run | 已核准 HTTP 容器或 API 自動建置 Python ASGI 來源;支援配置後的 HTTPS 入口、Invoker policy;無 autoscaling、traffic splitting、Jobs |
|
|
104
105
|
| Cloud Run functions / Functions Gen 1 | Python 3.12 HTTP 函式;Gen 1 為相同 HTTP runtime 的模擬子集,無背景事件 |
|
|
105
|
-
| AWS Lambda | Python 3.10 / 3.12 ZIP、同步 invoke
|
|
106
|
+
| AWS Lambda | Python 3.10 / 3.12 ZIP、同步 invoke、程式碼更新、BUFFERED Function URL;遠端 ZIP 上限 6 MiB |
|
|
106
107
|
| GCS / S3 | bucket 與 object 操作子集;單一 object 上限 6 MiB,無 versioning / signed URL |
|
|
107
108
|
| DynamoDB / SQS / SSM | 明列資料操作;無完整交易、Streams、FIFO 或 KMS / SecureString |
|
|
108
109
|
| AWS IAM / GCP IAM | 租戶內 role / service account / policy 子集;default deny、限定 session;無原生雲端全量 IAM 語義 |
|
|
@@ -118,11 +119,34 @@ CLI 無法判斷任意原始碼或 ZIP 內的資料是否可以分享。
|
|
|
118
119
|
|
|
119
120
|
Cloud Run manifest 支援 `serving.knative.dev/v1`、`kind: Service`、
|
|
120
121
|
`metadata.name`、單一 container 的 image / port / env,以及明列的 service account。
|
|
122
|
+
服務 metadata.annotations 支援 `run.googleapis.com/ingress`(all/internal)與
|
|
123
|
+
`run.googleapis.com/invoker-iam-disabled`(字串 true/false)。
|
|
121
124
|
`fufu plan --file service.yaml` 可在不部署的情況下檢查設定;plan 僅列出環境變數名稱。
|
|
122
125
|
相同設定可用 JSON。其他正式雲端欄位需要顯式調整,不能宣稱所有設定無痛相容。
|
|
123
126
|
|
|
124
127
|
## 隱私與執行邊界
|
|
125
128
|
|
|
129
|
+
0.7.0 新增服務公開存取。需由平台配置 DNS、HTTPS 入口與 WAF 放行,
|
|
130
|
+
`publicAccess.status=ready` 表示入口已配置;仍應由 CI 實際 HTTP 呼叫確認外部可達。
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
gcloud-fufu run services update example --allow-unauthenticated --ingress all
|
|
134
|
+
gcloud-fufu run services get-iam-policy example
|
|
135
|
+
gcloud-fufu run services update example --no-allow-unauthenticated
|
|
136
|
+
gcloud-fufu auth print-identity-token --audiences https://app-SERVICE_ID.example.com
|
|
137
|
+
aws-fufu lambda create-function-url-config --function-name example --auth-type AWS_IAM
|
|
138
|
+
aws-fufu lambda get-function-url-config --function-name example
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
UI 與 CLI 共用同一份服務政策。Cloud Run 與 HTTP Functions 的匿名政策使用
|
|
142
|
+
`allUsers` 與對應 Invoker 角色。Lambda `NONE` 必須另加 `lambda:InvokeFunctionUrl`
|
|
143
|
+
(`--function-url-auth-type NONE`)和 `lambda:InvokeFunction`
|
|
144
|
+
(`--invoked-via-function-url`)兩項資源權限;只設定 NONE 不會自動開放呼叫。
|
|
145
|
+
入口支援有界限的普通 HTTP:請求與回應各 6 MiB、執行 15 秒,每服務每分鐘 120 次。
|
|
146
|
+
不支援 SSE、WebSocket、串流、STS 暫時金鑰、presigned URL 或真實 VPC。
|
|
147
|
+
GCP ID token 由 FUFU 的 RS256 issuer 簽發,綁定精確 audience,15 分鐘後過期;不接受 Google ID token。
|
|
148
|
+
`print-identity-token` 明確輸出秘密,應直接放入受控變數,避免 CI 日誌或 shell tracing。
|
|
149
|
+
|
|
126
150
|
- 套件沒有遙測、廣告、背景更新或安裝時網路腳本。
|
|
127
151
|
- API 請求只使用明確提供的 FUFU endpoint 和 credential;不掃描使用者家目錄。
|
|
128
152
|
- `whoami` 會輸出目前租戶、provider 與 scope;查詢/invoke 會輸出所要求的資料。
|
|
@@ -56,7 +56,8 @@ credential 內含 FUFU endpoint。`FUFU_ENDPOINT` 可明確覆寫目的地:僅
|
|
|
56
56
|
一般命令交換短期權杖並在結束時嘗試撤銷。`fufu auth assume-role`
|
|
57
57
|
可建立受限 session 檔,檔案以排他方式建立且權限為 `0600`;請勿提交。
|
|
58
58
|
FUFU credential 與 session 檔仍屬秘密,必須由作業系統和 CI 控制存取。
|
|
59
|
-
原生雲端的 GitHub auth actions、OIDC
|
|
59
|
+
原生雲端的 GitHub auth actions、OIDC 和 OAuth 不直接相容。
|
|
60
|
+
Lambda Function URL 的 AWS_IAM 支援長效 FUFU access key 的 SigV4 header signing;管理 API 仍使用 FUFU token。
|
|
60
61
|
|
|
61
62
|
## 部署與支援範圍
|
|
62
63
|
|
|
@@ -87,9 +88,9 @@ CLI 無法判斷任意原始碼或 ZIP 內的資料是否可以分享。
|
|
|
87
88
|
|
|
88
89
|
| 服務 | 可用範圍與主要限制 |
|
|
89
90
|
| --- | --- |
|
|
90
|
-
| Cloud Run | 已核准 HTTP 容器或 API 自動建置 Python ASGI
|
|
91
|
+
| Cloud Run | 已核准 HTTP 容器或 API 自動建置 Python ASGI 來源;支援配置後的 HTTPS 入口、Invoker policy;無 autoscaling、traffic splitting、Jobs |
|
|
91
92
|
| Cloud Run functions / Functions Gen 1 | Python 3.12 HTTP 函式;Gen 1 為相同 HTTP runtime 的模擬子集,無背景事件 |
|
|
92
|
-
| AWS Lambda | Python 3.10 / 3.12 ZIP、同步 invoke
|
|
93
|
+
| AWS Lambda | Python 3.10 / 3.12 ZIP、同步 invoke、程式碼更新、BUFFERED Function URL;遠端 ZIP 上限 6 MiB |
|
|
93
94
|
| GCS / S3 | bucket 與 object 操作子集;單一 object 上限 6 MiB,無 versioning / signed URL |
|
|
94
95
|
| DynamoDB / SQS / SSM | 明列資料操作;無完整交易、Streams、FIFO 或 KMS / SecureString |
|
|
95
96
|
| AWS IAM / GCP IAM | 租戶內 role / service account / policy 子集;default deny、限定 session;無原生雲端全量 IAM 語義 |
|
|
@@ -105,11 +106,34 @@ CLI 無法判斷任意原始碼或 ZIP 內的資料是否可以分享。
|
|
|
105
106
|
|
|
106
107
|
Cloud Run manifest 支援 `serving.knative.dev/v1`、`kind: Service`、
|
|
107
108
|
`metadata.name`、單一 container 的 image / port / env,以及明列的 service account。
|
|
109
|
+
服務 metadata.annotations 支援 `run.googleapis.com/ingress`(all/internal)與
|
|
110
|
+
`run.googleapis.com/invoker-iam-disabled`(字串 true/false)。
|
|
108
111
|
`fufu plan --file service.yaml` 可在不部署的情況下檢查設定;plan 僅列出環境變數名稱。
|
|
109
112
|
相同設定可用 JSON。其他正式雲端欄位需要顯式調整,不能宣稱所有設定無痛相容。
|
|
110
113
|
|
|
111
114
|
## 隱私與執行邊界
|
|
112
115
|
|
|
116
|
+
0.7.0 新增服務公開存取。需由平台配置 DNS、HTTPS 入口與 WAF 放行,
|
|
117
|
+
`publicAccess.status=ready` 表示入口已配置;仍應由 CI 實際 HTTP 呼叫確認外部可達。
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
gcloud-fufu run services update example --allow-unauthenticated --ingress all
|
|
121
|
+
gcloud-fufu run services get-iam-policy example
|
|
122
|
+
gcloud-fufu run services update example --no-allow-unauthenticated
|
|
123
|
+
gcloud-fufu auth print-identity-token --audiences https://app-SERVICE_ID.example.com
|
|
124
|
+
aws-fufu lambda create-function-url-config --function-name example --auth-type AWS_IAM
|
|
125
|
+
aws-fufu lambda get-function-url-config --function-name example
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
UI 與 CLI 共用同一份服務政策。Cloud Run 與 HTTP Functions 的匿名政策使用
|
|
129
|
+
`allUsers` 與對應 Invoker 角色。Lambda `NONE` 必須另加 `lambda:InvokeFunctionUrl`
|
|
130
|
+
(`--function-url-auth-type NONE`)和 `lambda:InvokeFunction`
|
|
131
|
+
(`--invoked-via-function-url`)兩項資源權限;只設定 NONE 不會自動開放呼叫。
|
|
132
|
+
入口支援有界限的普通 HTTP:請求與回應各 6 MiB、執行 15 秒,每服務每分鐘 120 次。
|
|
133
|
+
不支援 SSE、WebSocket、串流、STS 暫時金鑰、presigned URL 或真實 VPC。
|
|
134
|
+
GCP ID token 由 FUFU 的 RS256 issuer 簽發,綁定精確 audience,15 分鐘後過期;不接受 Google ID token。
|
|
135
|
+
`print-identity-token` 明確輸出秘密,應直接放入受控變數,避免 CI 日誌或 shell tracing。
|
|
136
|
+
|
|
113
137
|
- 套件沒有遙測、廣告、背景更新或安裝時網路腳本。
|
|
114
138
|
- API 請求只使用明確提供的 FUFU endpoint 和 credential;不掃描使用者家目錄。
|
|
115
139
|
- `whoami` 會輸出目前租戶、provider 與 scope;查詢/invoke 會輸出所要求的資料。
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"no-autoscaling",
|
|
31
31
|
"no-traffic-splitting",
|
|
32
32
|
"no-jobs",
|
|
33
|
-
"
|
|
33
|
+
"public-ingress-requires-configured-api-gateway"
|
|
34
34
|
],
|
|
35
35
|
"backends": [
|
|
36
36
|
"local-docker",
|
|
@@ -82,6 +82,13 @@
|
|
|
82
82
|
"api"
|
|
83
83
|
]
|
|
84
84
|
},
|
|
85
|
+
"public-http-access": {
|
|
86
|
+
"status": "implemented",
|
|
87
|
+
"backend": "api",
|
|
88
|
+
"mode": "simplified",
|
|
89
|
+
"operations": ["stable-service-https-url", "enable-disable", "service-invoker-policy", "cloud-run-ingress-annotation", "audience-bound-id-token", "lambda-function-url-config", "lambda-url-resource-policy", "aws-sigv4-header-auth"],
|
|
90
|
+
"limitations": ["operator-DNS-TLS-WAF-required", "Cloud-Run-and-HTTP-Functions-and-Lambda-only", "buffered-6MiB-15s", "no-WebSocket-or-SSE", "no-STS-or-presigned-URL", "FUFU-ID-token-issuer-not-Google", "internal-means-platform-not-VPC", "120-requests-per-minute-per-service", "no-custom-domain-or-CORS-management", "no-full-cloud-IAM-equivalence"]
|
|
91
|
+
},
|
|
85
92
|
"serverless-framework": {
|
|
86
93
|
"status": "unsupported",
|
|
87
94
|
"reason": "未實作原生 Framework provider/plugins/CloudFormation;可改用明確的 serverless-profile-adapter 子集。"
|
|
@@ -13,6 +13,7 @@ from .docker_backend import (Backend, FUNCTIONS_IMAGE, INGRESS_IMAGE, KINDS, LAM
|
|
|
13
13
|
from .errors import FufuError, require
|
|
14
14
|
from .api_backend import ApiClient, api_selected, remote_command
|
|
15
15
|
from .remote_compute import RemoteBackend, source_archive, lambda_archive, cloud_run_archive
|
|
16
|
+
from .public_access import deployment_flags, run_iam_flags, configuration, configure_gcp, configure_manifest
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
class Parser(argparse.ArgumentParser):
|
|
@@ -177,9 +178,10 @@ def fufu_command(argv):
|
|
|
177
178
|
"images": available, "auth": "local-docker-permission", "remote_cloud": False}
|
|
178
179
|
if a.action == "deploy":
|
|
179
180
|
spec = manifest.load(a.file)
|
|
181
|
+
configure_manifest(spec)
|
|
180
182
|
spec["env"].update(env_from_names(a.env))
|
|
181
|
-
return compute_backend('cloud-run-http').deploy(kind="cloud-run-http", service=spec["name"], image=spec["image"],
|
|
182
|
-
port=spec["port"], env=spec["env"], startup_timeout=a.startup_timeout, **({'role': spec['role']} if spec.get('role') else {}))
|
|
183
|
+
return configure_manifest(spec, compute_backend('cloud-run-http').deploy(kind="cloud-run-http", service=spec["name"], image=spec["image"],
|
|
184
|
+
port=spec["port"], env=spec["env"], startup_timeout=a.startup_timeout, **({'role': spec['role']} if spec.get('role') else {})))
|
|
183
185
|
if a.action == "down":
|
|
184
186
|
require(not api_selected(), 'UNSUPPORTED_ARGUMENT', 'API 模式請逐一刪除指定服務;不提供整個租戶清空。')
|
|
185
187
|
return Backend().down()
|
|
@@ -211,6 +213,7 @@ def gcloud_command(argv):
|
|
|
211
213
|
d.add_argument("--port", type=int, default=8080)
|
|
212
214
|
d.add_argument('--service-account')
|
|
213
215
|
deployment_options(d)
|
|
216
|
+
deployment_flags(d); run_iam_flags(d)
|
|
214
217
|
services = run_sub.add_parser("services").add_subparsers(dest="service_action", required=True)
|
|
215
218
|
replace = services.add_parser("replace")
|
|
216
219
|
replace.add_argument("file")
|
|
@@ -228,6 +231,7 @@ def gcloud_command(argv):
|
|
|
228
231
|
generation_options(d)
|
|
229
232
|
d.add_argument("--trigger-http", required=True, action="store_true")
|
|
230
233
|
deployment_options(d)
|
|
234
|
+
deployment_flags(d)
|
|
231
235
|
for action in ("describe", "delete", "call"):
|
|
232
236
|
q = fn.add_parser(action)
|
|
233
237
|
q.add_argument("name")
|
|
@@ -240,25 +244,28 @@ def gcloud_command(argv):
|
|
|
240
244
|
action = a.service_action if a.action == "services" else a.action
|
|
241
245
|
if action == "replace":
|
|
242
246
|
spec = manifest.load(a.file)
|
|
247
|
+
configure_manifest(spec)
|
|
243
248
|
spec["env"].update(env_from_names(a.env))
|
|
244
|
-
return compute_backend(kind).deploy(kind=kind, service=spec["name"], image=spec["image"], port=spec["port"],
|
|
245
|
-
env=spec["env"], startup_timeout=a.startup_timeout, **({'role': spec['role']} if spec.get('role') else {}))
|
|
249
|
+
return configure_manifest(spec, compute_backend(kind).deploy(kind=kind, service=spec["name"], image=spec["image"], port=spec["port"],
|
|
250
|
+
env=spec["env"], startup_timeout=a.startup_timeout, **({'role': spec['role']} if spec.get('role') else {})))
|
|
246
251
|
if action == "deploy":
|
|
247
252
|
manifest.name(a.name)
|
|
253
|
+
configuration(a, kind)
|
|
254
|
+
finish = lambda deployed: configure_gcp(a, kind, a.name, deployed)
|
|
248
255
|
env = env_from_names(a.env)
|
|
249
256
|
if a.service == 'run' and a.source:
|
|
250
257
|
require(api_selected(), 'API_BACKEND_REQUIRED', 'Cloud Run --source 自動核准需要 FUFU API 模式。')
|
|
251
|
-
return RemoteBackend(kind).deploy(kind=kind, service=a.name, env=env, port=a.port,
|
|
258
|
+
return finish(RemoteBackend(kind).deploy(kind=kind, service=a.name, env=env, port=a.port,
|
|
252
259
|
archive=cloud_run_archive(a.source), runtime='python312', handler=a.entry_point,
|
|
253
|
-
startup_timeout=a.startup_timeout, role=a.service_account)
|
|
260
|
+
startup_timeout=a.startup_timeout, role=a.service_account))
|
|
254
261
|
if a.service == 'run':
|
|
255
262
|
require(a.entry_point == 'main:app', 'UNSUPPORTED_ARGUMENT', '--entry-point 只用於 --source。')
|
|
256
263
|
if a.service == 'functions' and api_selected():
|
|
257
|
-
return RemoteBackend(kind).deploy(kind=kind, service=a.name, env=env,
|
|
258
|
-
archive=source_archive(a.source), runtime=a.runtime, handler=a.entry_point, startup_timeout=a.startup_timeout, role=a.service_account)
|
|
264
|
+
return finish(RemoteBackend(kind).deploy(kind=kind, service=a.name, env=env,
|
|
265
|
+
archive=source_archive(a.source), runtime=a.runtime, handler=a.entry_point, startup_timeout=a.startup_timeout, role=a.service_account))
|
|
259
266
|
image = a.image if a.service == "run" else build_function(a.source, a.entry_point)
|
|
260
|
-
return compute_backend(kind).deploy(kind=kind, service=a.name, image=image, port=getattr(a, "port", 8080),
|
|
261
|
-
env=env, startup_timeout=a.startup_timeout, **({'role': a.service_account} if a.service_account else {}))
|
|
267
|
+
return finish(compute_backend(kind).deploy(kind=kind, service=a.name, image=image, port=getattr(a, "port", 8080),
|
|
268
|
+
env=env, startup_timeout=a.startup_timeout, **({'role': a.service_account} if a.service_account else {})))
|
|
262
269
|
b = compute_backend(kind)
|
|
263
270
|
if action == "describe":
|
|
264
271
|
return b.describe(kind, a.name)
|
|
@@ -371,6 +378,18 @@ def entry(provider, argv=None):
|
|
|
371
378
|
require('=' not in argv[i], 'UNSUPPORTED_ARGUMENT', '布林旗標不接受額外值。'); i += 1
|
|
372
379
|
else: rest.append(argv[i]); i += 1
|
|
373
380
|
argv = rest
|
|
381
|
+
from . import public_access
|
|
382
|
+
public_command = None
|
|
383
|
+
if provider == 'aws-fufu' and len(argv) > 1 and argv[0] == 'lambda' and argv[1] in public_access.AWS_ACTIONS:
|
|
384
|
+
public_command = public_access.aws_command
|
|
385
|
+
if provider == 'gcloud-fufu' and (argv[:2] == ['auth', 'print-identity-token']
|
|
386
|
+
or len(argv) > 2 and argv[:2] == ['run', 'services'] and argv[2] in public_access.GCP_POLICY_ACTIONS | {'update'}
|
|
387
|
+
or len(argv) > 1 and argv[0] == 'functions' and argv[1] in public_access.GCP_POLICY_ACTIONS):
|
|
388
|
+
public_command = public_access.gcp_command
|
|
389
|
+
if public_command:
|
|
390
|
+
result = public_command(argv, Parser)
|
|
391
|
+
if type(result) is int: raise SystemExit(result)
|
|
392
|
+
emit(result); return
|
|
374
393
|
if provider == 'aws-fufu' and argv and argv[0] == 'serverless':
|
|
375
394
|
from .serverless import command
|
|
376
395
|
emit(command(argv[1:], Parser)); return
|
|
@@ -228,9 +228,13 @@ class Backend:
|
|
|
228
228
|
require(len(ports) == 1 and ports[0]["HostIp"] == "127.0.0.1", "PORT_MISMATCH", "服務必須只綁定 loopback。")
|
|
229
229
|
return f"http://127.0.0.1:{int(ports[0]['HostPort'])}"
|
|
230
230
|
|
|
231
|
+
def service_id(self, r):
|
|
232
|
+
return r.get('serviceId') or hashlib.sha256(json.dumps(
|
|
233
|
+
[self.workspace, r['kind'], r['name'], r['revision']]).encode()).hexdigest()[:32]
|
|
234
|
+
|
|
231
235
|
def public(self, r):
|
|
232
236
|
return {"name": r["name"], "kind": r["kind"], "workspace": self.workspace,
|
|
233
|
-
"revision": r["revision"], "url": self.url(r), "readiness": "tcp",
|
|
237
|
+
"revision": r["revision"], "serviceId": self.service_id(r), "url": self.url(r), "readiness": "tcp",
|
|
234
238
|
**{k: r[k] for k in ("runtime", "handler", "role", "sdkTransport", "iamEnforced", "build") if k in r},
|
|
235
239
|
"status": "running", "backend": "local-docker"}
|
|
236
240
|
|
|
@@ -298,7 +302,7 @@ class Backend:
|
|
|
298
302
|
# 環境值經子程序環境注入,不出現在命令列、狀態或部署輸出。
|
|
299
303
|
candidate = docker(*args, env=supplied)
|
|
300
304
|
record = {"name": service, "kind": kind, "container_id": candidate,
|
|
301
|
-
"port": port, "revision": revision,
|
|
305
|
+
"port": port, "revision": revision, "serviceId": self.service_id(old) if old else uuid.uuid4().hex,
|
|
302
306
|
**({'network': network, 'gateway_id': gateway['id']} if gateway else {}),
|
|
303
307
|
**({'role': role, 'sdkTransport': 'fufu-workload-token', 'iamEnforced': True} if role else {}),
|
|
304
308
|
**({'build': build} if build else {}),
|
|
@@ -423,12 +427,14 @@ class Backend:
|
|
|
423
427
|
docker("network", "rm", network)
|
|
424
428
|
return {"workspace": self.workspace, "removed_containers": len(ids)}
|
|
425
429
|
|
|
426
|
-
def invoke(self, kind, service, payload, *, path="/", timeout=15):
|
|
430
|
+
def invoke(self, kind, service, payload, *, path="/", timeout=15, expected_service_id=None):
|
|
427
431
|
require(1 <= timeout <= 120, "INVALID_TIMEOUT", "請求期限須介於 1–120 秒。")
|
|
428
432
|
require(len(payload) <= MAX_PAYLOAD, "PAYLOAD_LIMIT", "Payload 超過 6 MiB。")
|
|
429
433
|
require(isinstance(path, str) and path.startswith("/") and not path.startswith("//")
|
|
430
434
|
and "\r" not in path and "\n" not in path, "INVALID_PATH", "只能使用本機服務相對路徑。")
|
|
431
435
|
data = self.describe(kind, service)
|
|
436
|
+
require(expected_service_id is None or data.get('serviceId') == expected_service_id,
|
|
437
|
+
'NOT_FOUND', '應用網址已失效。')
|
|
432
438
|
route = "/2015-03-31/functions/function/invocations" if kind == "aws-lambda-sync" else path
|
|
433
439
|
|
|
434
440
|
class NoRedirect(request.HTTPRedirectHandler):
|
|
@@ -83,8 +83,16 @@ def parse(d):
|
|
|
83
83
|
fields(d, {"apiVersion", "kind", "metadata", "spec"}, "root")
|
|
84
84
|
require(d.get("apiVersion") == "serving.knative.dev/v1" and d.get("kind") == "Service",
|
|
85
85
|
"UNSUPPORTED_CONFIG", "目前只支援 serving.knative.dev/v1 Service 的明列子集。")
|
|
86
|
-
metadata = fields(d.get("metadata"), {"name"}, "metadata")
|
|
86
|
+
metadata = fields(d.get("metadata"), {"name", "annotations"}, "metadata")
|
|
87
87
|
service = name(metadata.get("name"))
|
|
88
|
+
annotations = fields(metadata.get('annotations', {}), {'run.googleapis.com/ingress', 'run.googleapis.com/invoker-iam-disabled'}, 'metadata.annotations')
|
|
89
|
+
access = {}
|
|
90
|
+
if 'run.googleapis.com/ingress' in annotations:
|
|
91
|
+
require(annotations['run.googleapis.com/ingress'] in {'all', 'internal'}, 'UNSUPPORTED_CONFIG', 'Ingress 僅支援 all 或 FUFU 平台 internal。')
|
|
92
|
+
access['ingress'] = annotations['run.googleapis.com/ingress']
|
|
93
|
+
if 'run.googleapis.com/invoker-iam-disabled' in annotations:
|
|
94
|
+
require(annotations['run.googleapis.com/invoker-iam-disabled'] in {'true', 'false'}, 'UNSUPPORTED_CONFIG', 'Invoker IAM annotation 必須為字串 true 或 false。')
|
|
95
|
+
access['invokerIamDisabled'] = annotations['run.googleapis.com/invoker-iam-disabled'] == 'true'
|
|
88
96
|
spec = fields(d.get("spec"), {"template"}, "spec")
|
|
89
97
|
template = fields(spec.get("template"), {"spec"}, "spec.template")
|
|
90
98
|
pod = fields(template.get("spec"), {"containers", 'serviceAccountName'}, "spec.template.spec")
|
|
@@ -111,10 +119,10 @@ def parse(d):
|
|
|
111
119
|
require(isinstance(k, str) and k not in env, "INVALID_ENV", "環境變數名稱重複或無效。")
|
|
112
120
|
env[k] = item.get("value")
|
|
113
121
|
environment(env)
|
|
114
|
-
return {"name": service, "image": image, "port": port, "env": env, **({'role': role} if role else {})}
|
|
122
|
+
return {"name": service, "image": image, "port": port, "env": env, **({'role': role} if role else {}), **({'publicAccess': access} if access else {})}
|
|
115
123
|
|
|
116
124
|
|
|
117
125
|
def public_plan(spec):
|
|
118
126
|
return {"backend": "local-docker", "kind": "cloud-run-http", "name": spec["name"],
|
|
119
127
|
"port": spec["port"], "environment_names": sorted(spec["env"]),
|
|
120
|
-
"checks": "strict-supported-subset", "image_resolution": "at-deploy"}
|
|
128
|
+
"checks": "strict-supported-subset", "image_resolution": "at-deploy", **({'publicAccess': spec['publicAccess']} if spec.get('publicAccess') else {})}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Provider-shaped commands over the shared service access API."""
|
|
2
|
+
import json
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from types import SimpleNamespace
|
|
6
|
+
|
|
7
|
+
from .api_backend import ApiClient, api_selected, segment
|
|
8
|
+
from .errors import FufuError, require
|
|
9
|
+
|
|
10
|
+
AWS_ACTIONS = {'create-function-url-config', 'get-function-url-config', 'update-function-url-config',
|
|
11
|
+
'delete-function-url-config', 'add-permission', 'remove-permission', 'get-policy'}
|
|
12
|
+
GCP_POLICY_ACTIONS = {'get-iam-policy', 'set-iam-policy', 'add-iam-policy-binding', 'remove-iam-policy-binding'}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def configure_manifest(spec, deployed=None):
|
|
16
|
+
config = spec.get('publicAccess', {})
|
|
17
|
+
args = SimpleNamespace(anonymous=None, ingress=config.get('ingress'),
|
|
18
|
+
invoker_iam=not config['invokerIamDisabled'] if 'invokerIamDisabled' in config else None)
|
|
19
|
+
configuration(args, 'cloud-run-http')
|
|
20
|
+
if deployed is None: return
|
|
21
|
+
return configure_gcp(args, 'cloud-run-http', spec['name'], deployed)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def access_path(kind, name):
|
|
25
|
+
return '/compute/services/' + segment(kind) + '/' + segment(name) + '/access'
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def deployment_flags(parser):
|
|
29
|
+
group = parser.add_mutually_exclusive_group()
|
|
30
|
+
group.add_argument('--allow-unauthenticated', dest='anonymous', action='store_true')
|
|
31
|
+
group.add_argument('--no-allow-unauthenticated', dest='anonymous', action='store_false')
|
|
32
|
+
parser.set_defaults(anonymous=None)
|
|
33
|
+
parser.add_argument('--ingress', choices=['all', 'internal'])
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def run_iam_flags(parser):
|
|
37
|
+
group = parser.add_mutually_exclusive_group()
|
|
38
|
+
group.add_argument('--invoker-iam-check', dest='invoker_iam', action='store_true')
|
|
39
|
+
group.add_argument('--no-invoker-iam-check', dest='invoker_iam', action='store_false')
|
|
40
|
+
parser.set_defaults(invoker_iam=None)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def configuration(args, kind):
|
|
44
|
+
anonymous, invoker = getattr(args, 'anonymous', None), getattr(args, 'invoker_iam', None)
|
|
45
|
+
require(anonymous is None or invoker is None, 'CONFLICTING_ARGUMENTS',
|
|
46
|
+
'請選擇 allUsers policy 或 Invoker IAM check 其中一種設定方式。')
|
|
47
|
+
config = {}
|
|
48
|
+
if getattr(args, 'ingress', None) is not None: config['ingress'] = args.ingress
|
|
49
|
+
if invoker is not None: config['invokerIamDisabled'] = not invoker
|
|
50
|
+
if config or anonymous is not None:
|
|
51
|
+
require(api_selected(), 'API_BACKEND_REQUIRED', '公開入口與 IAM 設定需要 FUFU API 模式;本機 Docker 不會假裝發布公網。')
|
|
52
|
+
return config
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def configure_gcp(args, kind, name, deployed=None):
|
|
56
|
+
config = configuration(args, kind)
|
|
57
|
+
anonymous = getattr(args, 'anonymous', None)
|
|
58
|
+
if not config and anonymous is None:
|
|
59
|
+
return deployed
|
|
60
|
+
path = access_path(kind, name)
|
|
61
|
+
try:
|
|
62
|
+
with ApiClient('gcp') as api:
|
|
63
|
+
current = api.request('GET', path)
|
|
64
|
+
if anonymous is not None:
|
|
65
|
+
# One CAS update makes anonymous permission and ingress visible together.
|
|
66
|
+
config['allowUnauthenticated'] = anonymous
|
|
67
|
+
config['enabled'] = True
|
|
68
|
+
updated = api.request('PATCH', path, {**config, 'etag': current['etag']})
|
|
69
|
+
result = api.request('GET', path.removesuffix('/access'))
|
|
70
|
+
result['publicAccess'] = updated
|
|
71
|
+
return result
|
|
72
|
+
except FufuError as exc:
|
|
73
|
+
if deployed is not None:
|
|
74
|
+
raise FufuError('DEPLOYED_ACCESS_UPDATE_FAILED',
|
|
75
|
+
'程式部署已完成,但入口設定未完成;請 describe 確認目前狀態,再更新存取設定。') from exc
|
|
76
|
+
raise
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def gcp_command(argv, Parser):
|
|
80
|
+
if argv[:2] == ['auth', 'print-identity-token']:
|
|
81
|
+
p = Parser(prog='gcloud-fufu auth print-identity-token', description='輸出 FUFU issuer、15 分鐘且綁定 audience 的 ID token。')
|
|
82
|
+
p.add_argument('--audiences', required=True)
|
|
83
|
+
a = p.parse_args(argv[2:])
|
|
84
|
+
with ApiClient('gcp') as api:
|
|
85
|
+
result = api.request('POST', '/compute/auth/identity-token', {'audience': a.audiences})
|
|
86
|
+
require(isinstance(result.get('token'), str), 'INVALID_RESPONSE', 'ID token 回應無效。')
|
|
87
|
+
sys.stdout.write(result['token'] + '\n')
|
|
88
|
+
return 0
|
|
89
|
+
run = argv[:2] == ['run', 'services']
|
|
90
|
+
args = argv[2:] if run else argv[1:]
|
|
91
|
+
action = args[0]
|
|
92
|
+
p = Parser(prog='gcloud-fufu ' + ('run services ' if run else 'functions ') + action)
|
|
93
|
+
p.add_argument('name')
|
|
94
|
+
if not run:
|
|
95
|
+
group = p.add_mutually_exclusive_group()
|
|
96
|
+
group.add_argument('--gen2', dest='gen2', action='store_true')
|
|
97
|
+
group.add_argument('--no-gen2', dest='gen2', action='store_false')
|
|
98
|
+
p.set_defaults(gen2=True)
|
|
99
|
+
if action == 'update':
|
|
100
|
+
deployment_flags(p); run_iam_flags(p)
|
|
101
|
+
elif action in {'add-iam-policy-binding', 'remove-iam-policy-binding'}:
|
|
102
|
+
p.add_argument('--member', required=True); p.add_argument('--role', required=True)
|
|
103
|
+
elif action == 'set-iam-policy':
|
|
104
|
+
p.add_argument('policy_file')
|
|
105
|
+
a = p.parse_args(args[1:])
|
|
106
|
+
kind = 'cloud-run-http' if run else 'cloud-run-functions-http' if a.gen2 else 'cloud-functions-gen1-http'
|
|
107
|
+
if action == 'update':
|
|
108
|
+
require(configuration(a, kind) or a.anonymous is not None, 'MISSING_CONFIGURATION', '至少提供一個存取設定旗標。')
|
|
109
|
+
return configure_gcp(a, kind, a.name)
|
|
110
|
+
path = access_path(kind, a.name)
|
|
111
|
+
with ApiClient('gcp', read_only=action == 'get-iam-policy') as api:
|
|
112
|
+
current = api.request('GET', path)
|
|
113
|
+
if action == 'set-iam-policy':
|
|
114
|
+
file = Path(a.policy_file)
|
|
115
|
+
require(file.is_file() and file.stat().st_size <= 65536, 'INVALID_POLICY', 'Policy JSON 上限為 64 KiB。')
|
|
116
|
+
policy = json.loads(file.read_text())
|
|
117
|
+
require(isinstance(policy, dict) and not (policy.keys() - {'bindings', 'etag', 'version'})
|
|
118
|
+
and policy.get('version', 1) == 1, 'UNSUPPORTED_POLICY', '僅支援無條件 Invoker bindings;其他欄位不會被忽略。')
|
|
119
|
+
current = api.request('POST', path + '/policy', {'operation': 'set-bindings', 'bindings': policy.get('bindings', []),
|
|
120
|
+
'etag': policy.get('etag', current['etag'])})
|
|
121
|
+
elif action != 'get-iam-policy':
|
|
122
|
+
current = api.request('POST', path + '/policy', {'operation': 'add-binding' if action.startswith('add-') else 'remove-binding',
|
|
123
|
+
'role': a.role, 'member': a.member, 'etag': current['etag']})
|
|
124
|
+
return {'version': 1, 'bindings': current['bindings'], 'etag': current['etag'], 'Fufu': metadata(current)}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def metadata(access):
|
|
128
|
+
return {'mode': 'simplified', 'visibility': access['visibility'], 'authenticationScheme': access['authenticationScheme'],
|
|
129
|
+
'limits': access['limits'], 'serviceId': access['serviceId']}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def aws_command(argv, Parser):
|
|
133
|
+
action = argv[1]
|
|
134
|
+
p = Parser(prog='aws-fufu lambda ' + action, description='Function URL BUFFERED 子集;AWS_IAM 支援長效 FUFU access key 的 SigV4 header signing。')
|
|
135
|
+
p.add_argument('--function-name', required=True)
|
|
136
|
+
if action in {'create-function-url-config', 'update-function-url-config'}:
|
|
137
|
+
p.add_argument('--auth-type', choices=['NONE', 'AWS_IAM'], required=action.startswith('create-'))
|
|
138
|
+
p.add_argument('--invoke-mode', choices=['BUFFERED'], default='BUFFERED')
|
|
139
|
+
if action in {'add-permission', 'remove-permission'}: p.add_argument('--statement-id', required=True)
|
|
140
|
+
if action == 'add-permission':
|
|
141
|
+
p.add_argument('--action', dest='permission_action', required=True)
|
|
142
|
+
p.add_argument('--principal', required=True)
|
|
143
|
+
p.add_argument('--function-url-auth-type', choices=['NONE', 'AWS_IAM'])
|
|
144
|
+
p.add_argument('--invoked-via-function-url', action='store_true')
|
|
145
|
+
p.add_argument('--revision-id')
|
|
146
|
+
a = p.parse_args(argv[2:])
|
|
147
|
+
path = access_path('aws-lambda-sync', a.function_name)
|
|
148
|
+
with ApiClient('aws', read_only=action.startswith('get-')) as api:
|
|
149
|
+
access = api.request('GET', path)
|
|
150
|
+
etag = a.revision_id or access['etag']
|
|
151
|
+
if action in {'create-function-url-config', 'update-function-url-config', 'delete-function-url-config'}:
|
|
152
|
+
body = {'operation': action.split('-')[0], 'etag': etag}
|
|
153
|
+
if getattr(a, 'auth_type', None) is not None: body['authType'] = a.auth_type
|
|
154
|
+
access = api.request('PATCH', path, body)
|
|
155
|
+
if action.startswith('delete-'): return {}
|
|
156
|
+
if action == 'get-function-url-config':
|
|
157
|
+
require(access['enabled'], 'RESOURCE_NOT_FOUND', 'Function URL 尚未建立或已刪除。')
|
|
158
|
+
if action in {'add-permission', 'remove-permission'}:
|
|
159
|
+
body = {'operation': action, 'statementId': a.statement_id, 'etag': etag}
|
|
160
|
+
if action == 'add-permission':
|
|
161
|
+
body.update(action=a.permission_action, principal=a.principal)
|
|
162
|
+
if a.function_url_auth_type is not None: body['functionUrlAuthType'] = a.function_url_auth_type
|
|
163
|
+
if a.invoked_via_function_url: body['invokedViaFunctionUrl'] = True
|
|
164
|
+
access = api.request('POST', path + '/policy', body)
|
|
165
|
+
return {'Statement': json.dumps(next(s for s in access['statements'] if s['Sid'] == a.statement_id)),
|
|
166
|
+
'Fufu': metadata(access)} if action == 'add-permission' else {}
|
|
167
|
+
if action == 'get-policy':
|
|
168
|
+
return {'Policy': json.dumps({'Version': '2012-10-17', 'Statement': access['statements']}), 'RevisionId': access['etag'], 'Fufu': metadata(access)}
|
|
169
|
+
who = api.whoami()
|
|
170
|
+
return {'FunctionUrl': access['url'], 'FunctionArn': f"arn:aws:lambda:{who['region']}:{who['accountId']}:function:{a.function_name}",
|
|
171
|
+
'AuthType': access['authType'], 'InvokeMode': 'BUFFERED', 'LastModifiedTime': access['updatedAt'], 'Fufu': metadata(access)}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fufu-cloud-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.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
|
|
@@ -69,7 +69,8 @@ credential 內含 FUFU endpoint。`FUFU_ENDPOINT` 可明確覆寫目的地:僅
|
|
|
69
69
|
一般命令交換短期權杖並在結束時嘗試撤銷。`fufu auth assume-role`
|
|
70
70
|
可建立受限 session 檔,檔案以排他方式建立且權限為 `0600`;請勿提交。
|
|
71
71
|
FUFU credential 與 session 檔仍屬秘密,必須由作業系統和 CI 控制存取。
|
|
72
|
-
原生雲端的 GitHub auth actions、OIDC
|
|
72
|
+
原生雲端的 GitHub auth actions、OIDC 和 OAuth 不直接相容。
|
|
73
|
+
Lambda Function URL 的 AWS_IAM 支援長效 FUFU access key 的 SigV4 header signing;管理 API 仍使用 FUFU token。
|
|
73
74
|
|
|
74
75
|
## 部署與支援範圍
|
|
75
76
|
|
|
@@ -100,9 +101,9 @@ CLI 無法判斷任意原始碼或 ZIP 內的資料是否可以分享。
|
|
|
100
101
|
|
|
101
102
|
| 服務 | 可用範圍與主要限制 |
|
|
102
103
|
| --- | --- |
|
|
103
|
-
| Cloud Run | 已核准 HTTP 容器或 API 自動建置 Python ASGI
|
|
104
|
+
| Cloud Run | 已核准 HTTP 容器或 API 自動建置 Python ASGI 來源;支援配置後的 HTTPS 入口、Invoker policy;無 autoscaling、traffic splitting、Jobs |
|
|
104
105
|
| Cloud Run functions / Functions Gen 1 | Python 3.12 HTTP 函式;Gen 1 為相同 HTTP runtime 的模擬子集,無背景事件 |
|
|
105
|
-
| AWS Lambda | Python 3.10 / 3.12 ZIP、同步 invoke
|
|
106
|
+
| AWS Lambda | Python 3.10 / 3.12 ZIP、同步 invoke、程式碼更新、BUFFERED Function URL;遠端 ZIP 上限 6 MiB |
|
|
106
107
|
| GCS / S3 | bucket 與 object 操作子集;單一 object 上限 6 MiB,無 versioning / signed URL |
|
|
107
108
|
| DynamoDB / SQS / SSM | 明列資料操作;無完整交易、Streams、FIFO 或 KMS / SecureString |
|
|
108
109
|
| AWS IAM / GCP IAM | 租戶內 role / service account / policy 子集;default deny、限定 session;無原生雲端全量 IAM 語義 |
|
|
@@ -118,11 +119,34 @@ CLI 無法判斷任意原始碼或 ZIP 內的資料是否可以分享。
|
|
|
118
119
|
|
|
119
120
|
Cloud Run manifest 支援 `serving.knative.dev/v1`、`kind: Service`、
|
|
120
121
|
`metadata.name`、單一 container 的 image / port / env,以及明列的 service account。
|
|
122
|
+
服務 metadata.annotations 支援 `run.googleapis.com/ingress`(all/internal)與
|
|
123
|
+
`run.googleapis.com/invoker-iam-disabled`(字串 true/false)。
|
|
121
124
|
`fufu plan --file service.yaml` 可在不部署的情況下檢查設定;plan 僅列出環境變數名稱。
|
|
122
125
|
相同設定可用 JSON。其他正式雲端欄位需要顯式調整,不能宣稱所有設定無痛相容。
|
|
123
126
|
|
|
124
127
|
## 隱私與執行邊界
|
|
125
128
|
|
|
129
|
+
0.7.0 新增服務公開存取。需由平台配置 DNS、HTTPS 入口與 WAF 放行,
|
|
130
|
+
`publicAccess.status=ready` 表示入口已配置;仍應由 CI 實際 HTTP 呼叫確認外部可達。
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
gcloud-fufu run services update example --allow-unauthenticated --ingress all
|
|
134
|
+
gcloud-fufu run services get-iam-policy example
|
|
135
|
+
gcloud-fufu run services update example --no-allow-unauthenticated
|
|
136
|
+
gcloud-fufu auth print-identity-token --audiences https://app-SERVICE_ID.example.com
|
|
137
|
+
aws-fufu lambda create-function-url-config --function-name example --auth-type AWS_IAM
|
|
138
|
+
aws-fufu lambda get-function-url-config --function-name example
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
UI 與 CLI 共用同一份服務政策。Cloud Run 與 HTTP Functions 的匿名政策使用
|
|
142
|
+
`allUsers` 與對應 Invoker 角色。Lambda `NONE` 必須另加 `lambda:InvokeFunctionUrl`
|
|
143
|
+
(`--function-url-auth-type NONE`)和 `lambda:InvokeFunction`
|
|
144
|
+
(`--invoked-via-function-url`)兩項資源權限;只設定 NONE 不會自動開放呼叫。
|
|
145
|
+
入口支援有界限的普通 HTTP:請求與回應各 6 MiB、執行 15 秒,每服務每分鐘 120 次。
|
|
146
|
+
不支援 SSE、WebSocket、串流、STS 暫時金鑰、presigned URL 或真實 VPC。
|
|
147
|
+
GCP ID token 由 FUFU 的 RS256 issuer 簽發,綁定精確 audience,15 分鐘後過期;不接受 Google ID token。
|
|
148
|
+
`print-identity-token` 明確輸出秘密,應直接放入受控變數,避免 CI 日誌或 shell tracing。
|
|
149
|
+
|
|
126
150
|
- 套件沒有遙測、廣告、背景更新或安裝時網路腳本。
|
|
127
151
|
- API 請求只使用明確提供的 FUFU endpoint 和 credential;不掃描使用者家目錄。
|
|
128
152
|
- `whoami` 會輸出目前租戶、provider 與 scope;查詢/invoke 會輸出所要求的資料。
|
|
@@ -13,6 +13,7 @@ src/fufu_cloud_cli/docker_backend.py
|
|
|
13
13
|
src/fufu_cloud_cli/errors.py
|
|
14
14
|
src/fufu_cloud_cli/lambda_adapter.py
|
|
15
15
|
src/fufu_cloud_cli/manifest.py
|
|
16
|
+
src/fufu_cloud_cli/public_access.py
|
|
16
17
|
src/fufu_cloud_cli/remote_compute.py
|
|
17
18
|
src/fufu_cloud_cli/runtime_sdk.py
|
|
18
19
|
src/fufu_cloud_cli/serverless.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fufu_cloud_cli-0.6.0 → fufu_cloud_cli-0.7.0}/src/fufu_cloud_cli/runtime/functions/Dockerfile
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fufu_cloud_cli-0.6.0 → fufu_cloud_cli-0.7.0}/src/fufu_cloud_cli.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|