fufu-cloud-cli 0.9.8__tar.gz → 0.10.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.9.8/src/fufu_cloud_cli.egg-info → fufu_cloud_cli-0.10.0}/PKG-INFO +33 -9
- {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/README.md +32 -8
- {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/pyproject.toml +1 -1
- {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/__init__.py +1 -1
- {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/api_backend.py +7 -0
- {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/capabilities.json +22 -12
- {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/cli.py +9 -0
- fufu_cloud_cli-0.10.0/src/fufu_cloud_cli/infrastructure.py +283 -0
- {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0/src/fufu_cloud_cli.egg-info}/PKG-INFO +33 -9
- {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli.egg-info/SOURCES.txt +1 -0
- {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/LICENSE +0 -0
- {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/MANIFEST.in +0 -0
- {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/setup.cfg +0 -0
- {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/__main__.py +0 -0
- {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/aws_errors.py +0 -0
- {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/aws_lambda.py +0 -0
- {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/aws_output.py +0 -0
- {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/bigquery.py +0 -0
- {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/capacity.py +0 -0
- {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/cloud_data.py +0 -0
- {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/cloudflare.py +0 -0
- {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/control.py +0 -0
- {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/docker_backend.py +0 -0
- {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/errors.py +0 -0
- {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/gcloud_environment.py +0 -0
- {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/gcloud_output.py +0 -0
- {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/gcp_infra.py +0 -0
- {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/gcp_sdk.py +0 -0
- {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/gcs.py +0 -0
- {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/http_runtime.py +0 -0
- {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/iac_state.py +0 -0
- {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/lambda_adapter.py +0 -0
- {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/limits.py +0 -0
- {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/manifest.py +0 -0
- {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/object_transfer.py +0 -0
- {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/observability.py +0 -0
- {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/public_access.py +0 -0
- {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/pubsub.py +0 -0
- {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/pulumi-endpoints.json +0 -0
- {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/pulumi_bridge.py +0 -0
- {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/remote_compute.py +0 -0
- {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/runtime/functions/Dockerfile +0 -0
- {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/runtime/ingress/Dockerfile +0 -0
- {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/runtime/ingress/proxy.py +0 -0
- {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/runtime_sdk.py +0 -0
- {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/secrets.py +0 -0
- {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/serverless.py +0 -0
- {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/terraform_bridge/__init__.py +0 -0
- {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/terraform_bridge/aws.py +0 -0
- {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/terraform_bridge/aws_data.py +0 -0
- {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/terraform_bridge/common.py +0 -0
- {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/terraform_bridge/google.py +0 -0
- {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/terraform_bridge/google_pubsub.py +0 -0
- {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/terraform_bridge/remote_state.py +0 -0
- {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/terraform_bridge/runner.py +0 -0
- {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/vendor/AWS-CLI-LICENSE.txt +0 -0
- {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/vendor/__init__.py +0 -0
- {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/vendor/aws_text.py +0 -0
- {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli.egg-info/dependency_links.txt +0 -0
- {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli.egg-info/entry_points.txt +0 -0
- {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli.egg-info/requires.txt +0 -0
- {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.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.10.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
|
|
@@ -27,10 +27,33 @@ Dynamic: license-file
|
|
|
27
27
|
`gcloud-fufu`、`az-fufu`、`wrangler-fufu`、`bq-fufu`。這是 AWS、Google Cloud、Azure 與 Cloudflare 的**明確子集**,
|
|
28
28
|
只供 FUFU mock 環境使用;不會把部署轉送到正式雲端。
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
0.10.0 新增 Compute Engine/EC2 VM 與 GKE/EKS 介面的實際運算命令。
|
|
31
|
+
需要啟用相應 infrastructure backend 的 FUFU 服務端;VM 使用 KVM/KubeVirt,
|
|
32
|
+
叢集使用獨立 Kubernetes API 與共用節點,均不是官方雲端資源。
|
|
33
|
+
支援 VM 建立、SSH 指令、停止/啟動與刪除,以及叢集建立、查詢、
|
|
34
|
+
get-credentials/update-kubeconfig、waiter 與刪除。
|
|
35
|
+
取得憑證後可使用標準 kubectl 部署 Deployment、Service、Job 及受管 GPU 容器。
|
|
36
|
+
工作站需另安裝 kubectl;配套 Cloud Shell 已提供。
|
|
37
|
+
|
|
38
|
+
VM 尚無公開 IP、複寫儲存、live migration 或自助 GPU passthrough;
|
|
39
|
+
叢集尚無 workload PVC、公開 LoadBalancer、互動式 exec/attach/port-forward。
|
|
40
|
+
這批新 VM/cluster API 尚未支援官方 Terraform/OpenTofu/Pulumi provider。
|
|
41
|
+
原有 firewall、global LB、Redis 等 typed configuration 仍明示 `MOCK ONLY`,
|
|
42
|
+
不代表已建立相應網路或 Redis 資料面。
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
gcloud-fufu compute instances create demo-vm --image-family=debian-12
|
|
46
|
+
gcloud-fufu compute ssh demo-vm --command='uname -s; nproc'
|
|
47
|
+
gcloud-fufu container clusters create demo-gke
|
|
48
|
+
gcloud-fufu container clusters get-credentials demo-gke
|
|
49
|
+
kubectl get pods -A
|
|
50
|
+
aws-fufu eks create-cluster --name demo-eks
|
|
51
|
+
aws-fufu eks wait cluster-active --name demo-eks
|
|
52
|
+
aws-fufu eks update-kubeconfig --name demo-eks
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
這些操作需要 FUFU credential;不使用官方雲端金鑰。資源以租戶隔離,配額不足
|
|
56
|
+
會明確回覆 RESOURCE_EXHAUSTED。測試後請刪除所建 VM/cluster 以釋放資源。
|
|
34
57
|
|
|
35
58
|
0.9.8 提供 Cloud Run/Functions 的原生形式日誌查詢、CloudWatch Logs
|
|
36
59
|
tail/follow,以及 `fufu observability` 的日誌、請求指標與追蹤查詢。
|
|
@@ -73,9 +96,10 @@ insertAll/SELECT 與持久化日誌。`fufu_cloud_cli.gcp_sdk` 提供官方 Py
|
|
|
73
96
|
BigQuery、Cloud Tasks、Logging SDK 的明確 REST factory;可用 `[gcp]` extra
|
|
74
97
|
安裝已驗收相依。需要對應的新後端,並非透明 ADC 或所有 Google SDK 替換。
|
|
75
98
|
|
|
76
|
-
`fufu infrastructure`
|
|
77
|
-
|
|
78
|
-
|
|
99
|
+
`fufu infrastructure` 另保留 typed configuration mock 目錄,包含 Cloud SQL、
|
|
100
|
+
App Engine、DNS、VPC、Artifact Registry、Cloud Build、Gemini、VM Manager、
|
|
101
|
+
Eventarc 等。這個目錄只保存配置,不啟動 SQL/registry/網路/推論;
|
|
102
|
+
實際 VM/Kubernetes 請使用上述 compute/ec2/container/eks 指令。
|
|
79
103
|
UI 顯示僅設定,CLI stderr 保留 MOCK ONLY,不因輸出投影而隱藏。
|
|
80
104
|
新服務尚未接入 Terraform/OpenTofu/Pulumi;單機背景投遞不是雲端 HA。
|
|
81
105
|
|
|
@@ -107,7 +131,7 @@ Python 預設識別而遇到 Cloudflare HTTP 403/1010。識別標頭不取代
|
|
|
107
131
|
## Cloudflare 前端部署與 R2
|
|
108
132
|
|
|
109
133
|
```bash
|
|
110
|
-
pip install 'fufu-cloud-cli==0.
|
|
134
|
+
pip install 'fufu-cloud-cli==0.10.0'
|
|
111
135
|
export FUFU_CLOUDFLARE_CREDENTIAL_FILE=/run/secrets/fufu-cloudflare.json
|
|
112
136
|
wrangler-fufu capabilities
|
|
113
137
|
wrangler-fufu pages project create frontend --production-branch main
|
|
@@ -4,10 +4,33 @@
|
|
|
4
4
|
`gcloud-fufu`、`az-fufu`、`wrangler-fufu`、`bq-fufu`。這是 AWS、Google Cloud、Azure 與 Cloudflare 的**明確子集**,
|
|
5
5
|
只供 FUFU mock 環境使用;不會把部署轉送到正式雲端。
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
0.10.0 新增 Compute Engine/EC2 VM 與 GKE/EKS 介面的實際運算命令。
|
|
8
|
+
需要啟用相應 infrastructure backend 的 FUFU 服務端;VM 使用 KVM/KubeVirt,
|
|
9
|
+
叢集使用獨立 Kubernetes API 與共用節點,均不是官方雲端資源。
|
|
10
|
+
支援 VM 建立、SSH 指令、停止/啟動與刪除,以及叢集建立、查詢、
|
|
11
|
+
get-credentials/update-kubeconfig、waiter 與刪除。
|
|
12
|
+
取得憑證後可使用標準 kubectl 部署 Deployment、Service、Job 及受管 GPU 容器。
|
|
13
|
+
工作站需另安裝 kubectl;配套 Cloud Shell 已提供。
|
|
14
|
+
|
|
15
|
+
VM 尚無公開 IP、複寫儲存、live migration 或自助 GPU passthrough;
|
|
16
|
+
叢集尚無 workload PVC、公開 LoadBalancer、互動式 exec/attach/port-forward。
|
|
17
|
+
這批新 VM/cluster API 尚未支援官方 Terraform/OpenTofu/Pulumi provider。
|
|
18
|
+
原有 firewall、global LB、Redis 等 typed configuration 仍明示 `MOCK ONLY`,
|
|
19
|
+
不代表已建立相應網路或 Redis 資料面。
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
gcloud-fufu compute instances create demo-vm --image-family=debian-12
|
|
23
|
+
gcloud-fufu compute ssh demo-vm --command='uname -s; nproc'
|
|
24
|
+
gcloud-fufu container clusters create demo-gke
|
|
25
|
+
gcloud-fufu container clusters get-credentials demo-gke
|
|
26
|
+
kubectl get pods -A
|
|
27
|
+
aws-fufu eks create-cluster --name demo-eks
|
|
28
|
+
aws-fufu eks wait cluster-active --name demo-eks
|
|
29
|
+
aws-fufu eks update-kubeconfig --name demo-eks
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
這些操作需要 FUFU credential;不使用官方雲端金鑰。資源以租戶隔離,配額不足
|
|
33
|
+
會明確回覆 RESOURCE_EXHAUSTED。測試後請刪除所建 VM/cluster 以釋放資源。
|
|
11
34
|
|
|
12
35
|
0.9.8 提供 Cloud Run/Functions 的原生形式日誌查詢、CloudWatch Logs
|
|
13
36
|
tail/follow,以及 `fufu observability` 的日誌、請求指標與追蹤查詢。
|
|
@@ -50,9 +73,10 @@ insertAll/SELECT 與持久化日誌。`fufu_cloud_cli.gcp_sdk` 提供官方 Py
|
|
|
50
73
|
BigQuery、Cloud Tasks、Logging SDK 的明確 REST factory;可用 `[gcp]` extra
|
|
51
74
|
安裝已驗收相依。需要對應的新後端,並非透明 ADC 或所有 Google SDK 替換。
|
|
52
75
|
|
|
53
|
-
`fufu infrastructure`
|
|
54
|
-
|
|
55
|
-
|
|
76
|
+
`fufu infrastructure` 另保留 typed configuration mock 目錄,包含 Cloud SQL、
|
|
77
|
+
App Engine、DNS、VPC、Artifact Registry、Cloud Build、Gemini、VM Manager、
|
|
78
|
+
Eventarc 等。這個目錄只保存配置,不啟動 SQL/registry/網路/推論;
|
|
79
|
+
實際 VM/Kubernetes 請使用上述 compute/ec2/container/eks 指令。
|
|
56
80
|
UI 顯示僅設定,CLI stderr 保留 MOCK ONLY,不因輸出投影而隱藏。
|
|
57
81
|
新服務尚未接入 Terraform/OpenTofu/Pulumi;單機背景投遞不是雲端 HA。
|
|
58
82
|
|
|
@@ -84,7 +108,7 @@ Python 預設識別而遇到 Cloudflare HTTP 403/1010。識別標頭不取代
|
|
|
84
108
|
## Cloudflare 前端部署與 R2
|
|
85
109
|
|
|
86
110
|
```bash
|
|
87
|
-
pip install 'fufu-cloud-cli==0.
|
|
111
|
+
pip install 'fufu-cloud-cli==0.10.0'
|
|
88
112
|
export FUFU_CLOUDFLARE_CREDENTIAL_FILE=/run/secrets/fufu-cloudflare.json
|
|
89
113
|
wrangler-fufu capabilities
|
|
90
114
|
wrangler-fufu pages project create frontend --production-branch main
|
|
@@ -184,6 +184,13 @@ class ApiClient:
|
|
|
184
184
|
return json.loads(raw)
|
|
185
185
|
except HTTPError as exc:
|
|
186
186
|
status = exc.code
|
|
187
|
+
if '/infrastructure/' in path and status == 429:
|
|
188
|
+
try:
|
|
189
|
+
value = json.loads(exc.read(4096))
|
|
190
|
+
if isinstance(value, dict) and value.get('code') == 'RESOURCE_EXHAUSTED':
|
|
191
|
+
raise FufuError('RESOURCE_EXHAUSTED', 'FUFU 租戶資源配額或主機容量已滿;請刪除不需要的資源,或調整平台資源配額後再重試。') from None
|
|
192
|
+
except (ValueError, OSError): pass
|
|
193
|
+
finally: exc.close()
|
|
187
194
|
if self.provider == 'aws' and '/compute/' in path and status in {400, 412}:
|
|
188
195
|
try:
|
|
189
196
|
value = json.loads(exc.read(4096))
|
|
@@ -534,21 +534,23 @@
|
|
|
534
534
|
},
|
|
535
535
|
"gcp-compute-engine": {
|
|
536
536
|
"name": "Compute Engine",
|
|
537
|
-
"status": "
|
|
537
|
+
"status": "implemented",
|
|
538
538
|
"backend": "api",
|
|
539
|
-
"mode": "
|
|
539
|
+
"mode": "simplified",
|
|
540
540
|
"operations": [
|
|
541
|
-
"
|
|
541
|
+
"create-list-describe-delete", "stop-start", "guest-ssh-command"
|
|
542
542
|
],
|
|
543
|
-
"
|
|
543
|
+
"limitations": ["requires-FUFU-infrastructure-backend", "Debian-12-only", "private-network", "single-node-disk", "no-self-service-GPU-passthrough", "no-official-IaC-provider"],
|
|
544
|
+
"reason": "實際 KVM/KubeVirt VM、持久磁碟與 SSH 指令;原生風格指令子集,無官方雲端資源。"
|
|
544
545
|
},
|
|
545
546
|
"gcp-gke": {
|
|
546
547
|
"name": "GKE",
|
|
547
|
-
"status": "
|
|
548
|
+
"status": "implemented",
|
|
548
549
|
"backend": "api",
|
|
549
|
-
"mode": "
|
|
550
|
-
"operations": ["
|
|
551
|
-
"
|
|
550
|
+
"mode": "simplified",
|
|
551
|
+
"operations": ["create-list-describe-delete", "get-credentials", "kubectl-workloads", "GPU-container-jobs"],
|
|
552
|
+
"limitations": ["requires-FUFU-infrastructure-backend", "shared-workers", "single-control-plane-replica", "no-workload-PVC", "no-public-LoadBalancer", "no-interactive-exec-or-portforward", "no-official-IaC-provider"],
|
|
553
|
+
"reason": "獨立 vCluster Kubernetes API 與控制面資料;共用受管節點,支援標準 kubectl 工作負載子集。"
|
|
552
554
|
},
|
|
553
555
|
"gcp-vpc": {
|
|
554
556
|
"name": "VPC / Firewall",
|
|
@@ -637,8 +639,12 @@
|
|
|
637
639
|
},
|
|
638
640
|
"aws-ec2": {
|
|
639
641
|
"name": "EC2",
|
|
640
|
-
"status": "
|
|
641
|
-
"
|
|
642
|
+
"status": "implemented",
|
|
643
|
+
"backend": "api",
|
|
644
|
+
"mode": "simplified",
|
|
645
|
+
"operations": ["run-describe-terminate-instances", "stop-start", "instance-waiters"],
|
|
646
|
+
"limitations": ["requires-FUFU-infrastructure-backend", "Debian-12-only", "private-network", "single-node-disk", "no-self-service-GPU-passthrough", "no-official-IaC-provider"],
|
|
647
|
+
"reason": "實際 KVM/KubeVirt VM;支援生命週期與 scoped instance ID,無官方 EC2 網路或完整 API。"
|
|
642
648
|
},
|
|
643
649
|
"aws-ecs": {
|
|
644
650
|
"name": "ECS / Fargate",
|
|
@@ -647,8 +653,12 @@
|
|
|
647
653
|
},
|
|
648
654
|
"aws-eks": {
|
|
649
655
|
"name": "EKS",
|
|
650
|
-
"status": "
|
|
651
|
-
"
|
|
656
|
+
"status": "implemented",
|
|
657
|
+
"backend": "api",
|
|
658
|
+
"mode": "simplified",
|
|
659
|
+
"operations": ["create-list-describe-delete", "update-kubeconfig", "cluster-waiters", "kubectl-workloads", "GPU-container-jobs"],
|
|
660
|
+
"limitations": ["requires-FUFU-infrastructure-backend", "shared-workers", "single-control-plane-replica", "no-workload-PVC", "no-public-LoadBalancer", "no-interactive-exec-or-portforward", "no-official-IaC-provider"],
|
|
661
|
+
"reason": "獨立 vCluster Kubernetes API 與控制面資料;FUFU 短效認證,不是 AWS 原生 IAM 或託管控制面。"
|
|
652
662
|
},
|
|
653
663
|
"aws-app-runner": {
|
|
654
664
|
"name": "App Runner",
|
|
@@ -518,6 +518,9 @@ def entry(provider, argv=None):
|
|
|
518
518
|
if provider == 'fufu' and argv[:1] == ['infrastructure']:
|
|
519
519
|
from .gcp_infra import configuration_command
|
|
520
520
|
emit(configuration_command(argv[1:], Parser)); return
|
|
521
|
+
if provider == 'fufu' and argv[:1] == ['kubernetes-token']:
|
|
522
|
+
from .infrastructure import token_command
|
|
523
|
+
emit(token_command(argv[1:], Parser)); return
|
|
521
524
|
# Native global selectors are checked against the authenticated tenant scope.
|
|
522
525
|
if provider in {'aws-fufu', 'gcloud-fufu'}:
|
|
523
526
|
rest = []; i = 0; explicit_region = False
|
|
@@ -580,6 +583,12 @@ def entry(provider, argv=None):
|
|
|
580
583
|
if output_format and argv[:2] == ['functions', 'call']:
|
|
581
584
|
require(output_format == 'json', 'UNSUPPORTED_FORMAT', 'Functions call 目前只支援 JSON 回應。')
|
|
582
585
|
from . import public_access
|
|
586
|
+
if provider == 'gcloud-fufu' and argv[:2] in [['compute', 'instances'], ['compute', 'ssh'], ['container', 'clusters']]:
|
|
587
|
+
from .infrastructure import gcp_command
|
|
588
|
+
emit(gcp_command(argv, Parser)); return
|
|
589
|
+
if provider == 'aws-fufu' and argv[:1] in [['ec2'], ['eks']]:
|
|
590
|
+
from .infrastructure import aws_command as infrastructure_aws_command
|
|
591
|
+
emit(infrastructure_aws_command(argv, Parser)); return
|
|
583
592
|
if provider == 'gcloud-fufu' and argv and argv[0] in {'scheduler', 'tasks', 'logging', 'compute', 'sql', 'artifacts', 'dns', 'eventarc', 'container', 'redis'}:
|
|
584
593
|
from .gcp_infra import command
|
|
585
594
|
emit(command(argv, Parser)); return
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
"""Native command subset backed by the private RKE2 infrastructure adapter."""
|
|
2
|
+
import base64
|
|
3
|
+
import datetime
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import re
|
|
9
|
+
import shutil
|
|
10
|
+
import ssl
|
|
11
|
+
import sys
|
|
12
|
+
import tempfile
|
|
13
|
+
import time
|
|
14
|
+
import uuid
|
|
15
|
+
|
|
16
|
+
import yaml
|
|
17
|
+
|
|
18
|
+
from .api_backend import ApiClient, segment
|
|
19
|
+
from .errors import FufuError, require
|
|
20
|
+
|
|
21
|
+
BASE = '/infrastructure/resources/'
|
|
22
|
+
VM_TYPES = {'e2-micro': (2, 1), 'e2-small': (2, 2), 'e2-medium': (2, 4), 'e2-standard-2': (2, 8),
|
|
23
|
+
'e2-standard-4': (4, 16), 'e2-standard-8': (8, 32), 't3.micro': (2, 1), 't3.small': (2, 2),
|
|
24
|
+
't3.medium': (2, 4), 'm5.large': (2, 8), 'm5.xlarge': (4, 16), 'm5.2xlarge': (8, 32)}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def warn(kind):
|
|
28
|
+
sys.stderr.write('FUFU runtime: ' + ('KVM VM with a private network and one-node persistent disk; no GPU passthrough.\n' if kind == 'vm' else
|
|
29
|
+
'Independent Kubernetes API with shared worker nodes; private ClusterIP services, no workload PVCs or native cloud IAM.\n'))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def route(kind, name=None):
|
|
33
|
+
return BASE + kind + ('/' + segment(name) if name else '')
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def wait(api, kind, name, desired='RUNNING', seconds=480):
|
|
37
|
+
deadline = time.monotonic() + seconds
|
|
38
|
+
while time.monotonic() < deadline:
|
|
39
|
+
try: value = api.request('GET', route(kind, name))
|
|
40
|
+
except FufuError as error:
|
|
41
|
+
if desired == 'DELETED' and error.code in {'RESOURCE_NOT_FOUND', 'NOT_FOUND'}: return {'name': name, 'state': 'DELETED'}
|
|
42
|
+
raise
|
|
43
|
+
require(value['state'] != 'ERROR', 'INFRASTRUCTURE_FAILED', '基礎設施建立失敗;請查看資源事件,修正後刪除重建。')
|
|
44
|
+
if value['state'] == desired: return value
|
|
45
|
+
time.sleep(2)
|
|
46
|
+
raise FufuError('DEADLINE_EXCEEDED', '資源尚未達到預期狀態;請查詢狀態,勿直接重複建立。')
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def fufu_fields(value):
|
|
50
|
+
return {key: value.get(key) for key in ['name', 'state', 'mode', 'backend', 'node', 'limitations', 'spec'] if key in value}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def gcp_shape(value, kind):
|
|
54
|
+
spec = value.get('spec', {})
|
|
55
|
+
if kind == 'vm':
|
|
56
|
+
return {'name': value['name'], 'status': {'STOPPED': 'TERMINATED', 'DELETING': 'STOPPING'}.get(value['state'], value['state']),
|
|
57
|
+
'machineType': spec.get('machineType'), 'zone': spec.get('location'),
|
|
58
|
+
'disks': [{'boot': True, 'diskSizeGb': str(spec.get('diskGiB', 20))}], 'Fufu': fufu_fields(value)}
|
|
59
|
+
return {'name': value['name'], 'status': {'PROVISIONING': 'PROVISIONING', 'RUNNING': 'RUNNING', 'ERROR': 'ERROR', 'DELETING': 'STOPPING'}.get(value['state'], value['state']),
|
|
60
|
+
'location': spec.get('location'), 'currentMasterVersion': spec.get('version'), 'Fufu': fufu_fields(value)}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def aws_shape(value, kind, who):
|
|
64
|
+
spec = value.get('spec', {})
|
|
65
|
+
if kind == 'vm':
|
|
66
|
+
identifier = 'i-' + hashlib.sha256((str(who['tenantId']) + value['name'] + value.get('resourceId', value.get('createdAt', ''))).encode()).hexdigest()[:17]
|
|
67
|
+
state = {'RUNNING': (16, 'running'), 'STOPPED': (80, 'stopped'), 'STOPPING': (64, 'stopping'), 'DELETING': (32, 'shutting-down'), 'DELETED': (48, 'terminated')}.get(value['state'], (0, 'pending'))
|
|
68
|
+
return {'InstanceId': identifier, 'InstanceType': spec.get('machineType'), 'ImageId': 'fufu-debian-12',
|
|
69
|
+
'State': {'Code': state[0], 'Name': state[1]}, 'Tags': [{'Key': 'Name', 'Value': value['name']}], 'Fufu': fufu_fields(value)}
|
|
70
|
+
return {'name': value['name'], 'arn': f"arn:aws:eks:{who['region']}:{who['accountId']}:cluster/{value['name']}",
|
|
71
|
+
'status': {'RUNNING': 'ACTIVE', 'PROVISIONING': 'CREATING', 'DELETING': 'DELETING', 'ERROR': 'FAILED'}.get(value['state'], value['state']),
|
|
72
|
+
'version': spec.get('version'), 'Fufu': fufu_fields(value)}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def kubernetes_transport(api, name, who):
|
|
76
|
+
endpoint = api.endpoint
|
|
77
|
+
cluster = {}
|
|
78
|
+
if who.get('sessionKind') == 'terminal' and endpoint == 'http://127.0.0.1:8787':
|
|
79
|
+
require(os.environ.get('FUFU_SESSION_FILE') == '/run/fufu/session.json',
|
|
80
|
+
'INVALID_SESSION', 'Cloud Shell 工作階段來源不符。')
|
|
81
|
+
certificate = Path('/run/fufu/kubernetes-ca.crt')
|
|
82
|
+
require(certificate.is_file() and not certificate.is_symlink() and certificate.stat().st_size <= 16384,
|
|
83
|
+
'INVALID_SESSION', 'Cloud Shell TLS 憑證不存在或無效,請重新開啟 Shell。')
|
|
84
|
+
content = certificate.read_text(encoding='ascii')
|
|
85
|
+
try:
|
|
86
|
+
ssl.PEM_cert_to_DER_cert(content)
|
|
87
|
+
except ValueError:
|
|
88
|
+
raise FufuError('INVALID_SESSION', 'Cloud Shell TLS 憑證無效。') from None
|
|
89
|
+
endpoint = 'https://127.0.0.1:8788'
|
|
90
|
+
cluster['certificate-authority-data'] = base64.b64encode(content.encode('ascii')).decode('ascii')
|
|
91
|
+
cluster['server'] = endpoint + f'/api/cloud/{api.provider}/infrastructure/clusters/{segment(name)}/kubernetes'
|
|
92
|
+
return cluster
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def write_kubeconfig(api, name, filename=None, alias=None):
|
|
96
|
+
who = api.whoami()
|
|
97
|
+
require(not who.get('role'), 'PERMISSION_DENIED', '目前叢集存取需使用租戶擁有者;不會將受限 IAM 角色提升為 Kubernetes 管理員。')
|
|
98
|
+
value = api.request('GET', route('cluster', name))
|
|
99
|
+
require(value['state'] == 'RUNNING', 'CLUSTER_NOT_READY', '叢集尚未就緒。')
|
|
100
|
+
selected = filename or os.environ.get('KUBECONFIG') or str(Path.home() / '.kube/config')
|
|
101
|
+
require(os.pathsep not in selected, 'UNSUPPORTED_CONFIG', '目前請指定單一 kubeconfig 檔案。')
|
|
102
|
+
path = Path(selected).expanduser().absolute()
|
|
103
|
+
require(not path.is_symlink(), 'INVALID_CONFIG', 'kubeconfig 不可為符號連結。')
|
|
104
|
+
path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
105
|
+
context = alias or f'fufu-{api.provider}-{name}'
|
|
106
|
+
require(isinstance(context, str) and 0 < len(context) < 256 and not re.search(r'[\x00-\x1f]', context), 'INVALID_CONFIG', 'Context 名稱無效。')
|
|
107
|
+
data = {'apiVersion': 'v1', 'kind': 'Config', 'clusters': [], 'contexts': [], 'users': []}
|
|
108
|
+
if path.exists():
|
|
109
|
+
require(path.is_file() and path.stat().st_size <= 4 * 1024**2 and path.stat().st_mode & 0o022 == 0,
|
|
110
|
+
'INVALID_CONFIG', '既有 kubeconfig 的格式或寫入權限不安全。')
|
|
111
|
+
data = yaml.safe_load(path.read_text()) or data
|
|
112
|
+
require(type(data) is dict, 'INVALID_CONFIG', 'kubeconfig 格式無效。')
|
|
113
|
+
backup = path.with_name(path.name + '.fufu-backup-' + str(time.time_ns()))
|
|
114
|
+
shutil.copy2(path, backup); os.chmod(backup, 0o600)
|
|
115
|
+
scope = hashlib.sha256(who['tenantId'].encode()).hexdigest()
|
|
116
|
+
cluster = kubernetes_transport(api, name, who)
|
|
117
|
+
user = {'name': context, 'user': {'exec': {'apiVersion': 'client.authentication.k8s.io/v1', 'command': 'fufu',
|
|
118
|
+
'args': ['kubernetes-token', '--provider', api.provider, '--cluster', name, '--scope', scope],
|
|
119
|
+
'interactiveMode': 'Never', 'provideClusterInfo': True}}}
|
|
120
|
+
for key, item in [('clusters', {'name': context, 'cluster': cluster}), ('users', user),
|
|
121
|
+
('contexts', {'name': context, 'context': {'cluster': context, 'user': context}})]:
|
|
122
|
+
require(type(data.get(key, [])) is list, 'INVALID_CONFIG', 'kubeconfig 清單格式無效。')
|
|
123
|
+
data[key] = [x for x in data.get(key, []) if x.get('name') != context] + [item]
|
|
124
|
+
data['current-context'] = context
|
|
125
|
+
fd, tmp = tempfile.mkstemp(prefix='.fufu-kube-', dir=path.parent)
|
|
126
|
+
try:
|
|
127
|
+
with os.fdopen(fd, 'w') as f:
|
|
128
|
+
yaml.safe_dump(data, f, sort_keys=False); f.flush(); os.fsync(f.fileno())
|
|
129
|
+
os.replace(tmp, path)
|
|
130
|
+
finally:
|
|
131
|
+
if os.path.exists(tmp): os.unlink(tmp)
|
|
132
|
+
return {'context': context, 'kubeconfig': str(path), 'credentials': 'exec-plugin; no static cloud or host keys'}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def token_command(argv, Parser):
|
|
136
|
+
p = Parser(prog='fufu kubernetes-token', description='Kubernetes exec credential protocol; not intended for interactive use.')
|
|
137
|
+
p.add_argument('--provider', required=True, choices=['gcp', 'aws']); p.add_argument('--cluster', required=True); p.add_argument('--scope', required=True)
|
|
138
|
+
a = p.parse_args(argv)
|
|
139
|
+
require(bool(os.environ.get('KUBERNETES_EXEC_INFO')), 'INVALID_SESSION', '此指令僅供 Kubernetes exec credential protocol 使用。')
|
|
140
|
+
info = json.loads(os.environ['KUBERNETES_EXEC_INFO'])
|
|
141
|
+
with ApiClient(a.provider) as api:
|
|
142
|
+
who = api.whoami()
|
|
143
|
+
expected = kubernetes_transport(api, a.cluster, who)
|
|
144
|
+
supplied = info.get('spec', {}).get('cluster', {})
|
|
145
|
+
require(all(supplied.get(key) == expected.get(key) for key in ['server', 'certificate-authority-data'])
|
|
146
|
+
and not any(supplied.get(key) for key in ['insecure-skip-tls-verify', 'tls-server-name', 'proxy-url']) and
|
|
147
|
+
hashlib.sha256(who['tenantId'].encode()).hexdigest() == a.scope and not who.get('role'),
|
|
148
|
+
'RESOURCE_SCOPE_MISMATCH', 'Kubernetes context 與目前租戶或 endpoint 不符。')
|
|
149
|
+
api.request('GET', route('cluster', a.cluster))
|
|
150
|
+
result = {'apiVersion': 'client.authentication.k8s.io/v1', 'kind': 'ExecCredential',
|
|
151
|
+
'status': {'token': api.token, 'expirationTimestamp': (datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(minutes=4)).isoformat().replace('+00:00', 'Z')}}
|
|
152
|
+
api.owns_token = False
|
|
153
|
+
return result
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def gcp_command(argv, Parser):
|
|
157
|
+
ssh = argv[:2] == ['compute', 'ssh']
|
|
158
|
+
kind = 'vm' if argv[0] == 'compute' else 'cluster'
|
|
159
|
+
tail = argv[2:]
|
|
160
|
+
p = Parser(prog='gcloud-fufu ' + ' '.join(argv[:2]), description='FUFU private infrastructure runtime; supported native command subset.')
|
|
161
|
+
if ssh:
|
|
162
|
+
p.add_argument('name'); p.add_argument('--command', required=True); p.add_argument('--zone')
|
|
163
|
+
a = p.parse_args(tail)
|
|
164
|
+
with ApiClient('gcp') as api:
|
|
165
|
+
if a.zone:
|
|
166
|
+
item = api.request('GET', route('vm', a.name))
|
|
167
|
+
require(item.get('spec', {}).get('location') == a.zone, 'RESOURCE_NOT_FOUND', 'VM 不在指定 zone。')
|
|
168
|
+
value = api.request('POST', route('vm', a.name) + '/command', {'command': a.command})
|
|
169
|
+
sys.stdout.write(value['stdout']); sys.stderr.write(value['stderr'])
|
|
170
|
+
raise SystemExit(value['exitCode'])
|
|
171
|
+
actions = p.add_subparsers(dest='action', required=True)
|
|
172
|
+
names = ['create', 'describe', 'list', 'delete', 'start', 'stop'] if kind == 'vm' else ['create', 'describe', 'list', 'delete', 'get-credentials']
|
|
173
|
+
for action in names:
|
|
174
|
+
q = actions.add_parser(action)
|
|
175
|
+
if action != 'list': q.add_argument('name')
|
|
176
|
+
q.add_argument('--zone'); q.add_argument('--location')
|
|
177
|
+
if action in {'create', 'delete', 'start', 'stop'}: q.add_argument('--async', dest='asynchronous', action='store_true')
|
|
178
|
+
if action == 'create':
|
|
179
|
+
if kind == 'vm':
|
|
180
|
+
q.add_argument('--machine-type', default='e2-small'); q.add_argument('--image-family', default='debian-12')
|
|
181
|
+
q.add_argument('--image-project', default='debian-cloud'); q.add_argument('--boot-disk-size', default='20GB')
|
|
182
|
+
else:
|
|
183
|
+
q.add_argument('--cluster-version', default='1.36')
|
|
184
|
+
a = p.parse_args(tail)
|
|
185
|
+
warn(kind)
|
|
186
|
+
with ApiClient('gcp', read_only=a.action in {'list', 'describe'}) as api:
|
|
187
|
+
location = a.zone or a.location
|
|
188
|
+
require(not (a.zone and a.location and a.zone != a.location), 'INVALID_CONFIG', 'Zone 與 location 不一致。')
|
|
189
|
+
if a.action == 'list': return [gcp_shape(x, kind) for x in api.request('GET', route(kind))['items']
|
|
190
|
+
if not location or x.get('spec', {}).get('location') == location]
|
|
191
|
+
if a.action != 'create' and location:
|
|
192
|
+
item = api.request('GET', route(kind, a.name))
|
|
193
|
+
require(item.get('spec', {}).get('location') == location, 'RESOURCE_NOT_FOUND', '資源不在指定 location。')
|
|
194
|
+
if a.action == 'get-credentials': return write_kubeconfig(api, a.name)
|
|
195
|
+
if a.action == 'describe': return gcp_shape(api.request('GET', route(kind, a.name)), kind)
|
|
196
|
+
if a.action == 'create':
|
|
197
|
+
who = api.whoami()
|
|
198
|
+
spec = {'location': a.zone or a.location or who.get('region')}
|
|
199
|
+
if kind == 'vm':
|
|
200
|
+
require(a.machine_type in VM_TYPES and a.machine_type.startswith('e2-') and a.image_family == 'debian-12' and a.image_project == 'debian-cloud',
|
|
201
|
+
'UNSUPPORTED_CONFIG', '目前提供已驗證的 Debian 12 映像與列明的 VM 規格。')
|
|
202
|
+
match = re.fullmatch(r'(\d+)(?:GB|GiB)?', a.boot_disk_size, re.I)
|
|
203
|
+
require(match is not None, 'INVALID_CONFIG', '磁碟大小須為整數 GB。')
|
|
204
|
+
cpu, memory = VM_TYPES[a.machine_type]
|
|
205
|
+
spec.update(cpu=cpu, memoryGiB=memory, machineType=a.machine_type, diskGiB=int(match[1]), image=a.image_family)
|
|
206
|
+
else: spec['version'] = a.cluster_version
|
|
207
|
+
value = api.request('POST', route(kind, a.name), {'spec': spec})
|
|
208
|
+
elif a.action == 'delete': value = api.request('DELETE', route(kind, a.name))
|
|
209
|
+
else: value = api.request('POST', route(kind, a.name) + '/' + a.action, {})
|
|
210
|
+
if not a.asynchronous:
|
|
211
|
+
value = wait(api, kind, a.name, 'DELETED' if a.action == 'delete' else 'STOPPED' if a.action == 'stop' else 'RUNNING')
|
|
212
|
+
return gcp_shape(value, kind)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def aws_command(argv, Parser):
|
|
216
|
+
kind = 'cluster' if argv[0] == 'eks' else 'vm'
|
|
217
|
+
p = Parser(prog='aws-fufu ' + argv[0], description='FUFU private infrastructure runtime; unsupported cloud options fail explicitly.')
|
|
218
|
+
actions = p.add_subparsers(dest='action', required=True)
|
|
219
|
+
if kind == 'cluster':
|
|
220
|
+
for action in ['create-cluster', 'describe-cluster', 'delete-cluster', 'list-clusters', 'update-kubeconfig']:
|
|
221
|
+
q = actions.add_parser(action)
|
|
222
|
+
if action != 'list-clusters': q.add_argument('--name', required=True)
|
|
223
|
+
if action == 'create-cluster': q.add_argument('--kubernetes-version', default='1.36')
|
|
224
|
+
if action == 'update-kubeconfig': q.add_argument('--kubeconfig'); q.add_argument('--alias')
|
|
225
|
+
q = actions.add_parser('wait'); q.add_argument('waiter', choices=['cluster-active', 'cluster-deleted']); q.add_argument('--name', required=True)
|
|
226
|
+
else:
|
|
227
|
+
q = actions.add_parser('run-instances'); q.add_argument('--image-id', required=True); q.add_argument('--instance-type', default='t3.small')
|
|
228
|
+
q.add_argument('--count', type=int, default=1); q.add_argument('--tag-specifications')
|
|
229
|
+
for action in ['describe-instances', 'start-instances', 'stop-instances', 'terminate-instances']:
|
|
230
|
+
q = actions.add_parser(action); q.add_argument('--instance-ids', nargs='+', required=action != 'describe-instances')
|
|
231
|
+
q = actions.add_parser('wait'); q.add_argument('waiter', choices=['instance-running', 'instance-stopped', 'instance-terminated']); q.add_argument('--instance-ids', nargs='+', required=True)
|
|
232
|
+
a = p.parse_args(argv[1:])
|
|
233
|
+
warn(kind)
|
|
234
|
+
with ApiClient('aws', read_only=a.action in {'list-clusters', 'describe-cluster', 'describe-instances', 'wait'}) as api:
|
|
235
|
+
who = api.whoami()
|
|
236
|
+
if kind == 'cluster':
|
|
237
|
+
if a.action == 'list-clusters': return {'clusters': [x['name'] for x in api.request('GET', route(kind))['items']]}
|
|
238
|
+
if a.action == 'update-kubeconfig': return write_kubeconfig(api, a.name, a.kubeconfig, a.alias)
|
|
239
|
+
if a.action == 'wait': wait(api, kind, a.name, 'DELETED' if a.waiter == 'cluster-deleted' else 'RUNNING'); return None
|
|
240
|
+
if a.action == 'create-cluster': value = api.request('POST', route(kind, a.name), {'spec': {'version': a.kubernetes_version, 'location': who['region']}})
|
|
241
|
+
elif a.action == 'delete-cluster': value = api.request('DELETE', route(kind, a.name))
|
|
242
|
+
else: value = api.request('GET', route(kind, a.name))
|
|
243
|
+
return {'cluster': aws_shape(value, kind, who)}
|
|
244
|
+
if a.action == 'run-instances':
|
|
245
|
+
require(a.count == 1 and a.image_id == 'fufu-debian-12' and a.instance_type in VM_TYPES and not a.instance_type.startswith('e2-'), 'UNSUPPORTED_CONFIG',
|
|
246
|
+
'目前使用 fufu-debian-12,且每次建立一台 VM;不會將官方 AMI 冒充為本地映像。')
|
|
247
|
+
name = 'vm-' + uuid.uuid4().hex[:12]
|
|
248
|
+
if a.tag_specifications:
|
|
249
|
+
tags = json.loads(a.tag_specifications)
|
|
250
|
+
require(type(tags) is list and len(tags) == 1 and tags[0].get('ResourceType') == 'instance'
|
|
251
|
+
and len(tags[0].get('Tags', [])) == 1 and tags[0]['Tags'][0].get('Key') == 'Name', 'UNSUPPORTED_CONFIG', '目前只接受一個 Name 標籤。')
|
|
252
|
+
name = tags[0]['Tags'][0]['Value']
|
|
253
|
+
cpu, memory = VM_TYPES[a.instance_type]
|
|
254
|
+
api.request('POST', route(kind, name), {'spec': {'image': 'debian-12', 'machineType': a.instance_type, 'cpu': cpu, 'memoryGiB': memory, 'location': who['region']}})
|
|
255
|
+
value = api.request('GET', route(kind, name))
|
|
256
|
+
return {'Instances': [aws_shape(value, kind, who)]}
|
|
257
|
+
rows = api.request('GET', route(kind))['items']
|
|
258
|
+
selected = [x for x in rows if not a.instance_ids or aws_shape(x, kind, who)['InstanceId'] in a.instance_ids]
|
|
259
|
+
if a.instance_ids: require(len(selected) == len(set(a.instance_ids)), 'InvalidInstanceID.NotFound', 'Instance ID 不存在於目前租戶。')
|
|
260
|
+
if a.action == 'describe-instances': return {'Reservations': [{'Instances': [aws_shape(x, kind, who) for x in selected]}]}
|
|
261
|
+
if a.action == 'wait':
|
|
262
|
+
desired = {'instance-running': 'RUNNING', 'instance-stopped': 'STOPPED', 'instance-terminated': 'DELETED'}[a.waiter]
|
|
263
|
+
deadline = time.monotonic() + 480
|
|
264
|
+
while not all(x['state'] == desired for x in selected):
|
|
265
|
+
require(time.monotonic() < deadline, 'DEADLINE_EXCEEDED', 'Instance 尚未達到預期狀態。')
|
|
266
|
+
require(not any(x['state'] in {'ERROR', 'DELETED'} for x in selected), 'WAITER_FAILED', 'Instance 已進入無法達成預期狀態的狀態。')
|
|
267
|
+
time.sleep(2)
|
|
268
|
+
rows = api.request('GET', route(kind))['items']
|
|
269
|
+
selected = [x for x in rows if aws_shape(x, kind, who)['InstanceId'] in a.instance_ids]
|
|
270
|
+
require(len(selected) == len(set(a.instance_ids)), 'InvalidInstanceID.NotFound', 'Instance ID 不存在於目前租戶。')
|
|
271
|
+
return None
|
|
272
|
+
output = []
|
|
273
|
+
for x in selected:
|
|
274
|
+
old = aws_shape(x, kind, who)
|
|
275
|
+
action = {'start-instances': 'start', 'stop-instances': 'stop', 'terminate-instances': 'delete'}[a.action]
|
|
276
|
+
if x['state'] == 'DELETED':
|
|
277
|
+
require(action == 'delete', 'IncorrectInstanceState', '已終止的 Instance 無法啟動或停止。')
|
|
278
|
+
output.append({'InstanceId': old['InstanceId'], 'PreviousState': old['State'], 'CurrentState': old['State']})
|
|
279
|
+
continue
|
|
280
|
+
api.request('DELETE' if action == 'delete' else 'POST', route(kind, x['name']) + ('' if action == 'delete' else '/' + action), None if action == 'delete' else {})
|
|
281
|
+
target = {'start': (0, 'pending'), 'stop': (64, 'stopping'), 'delete': (32, 'shutting-down')}[action]
|
|
282
|
+
output.append({'InstanceId': old['InstanceId'], 'PreviousState': old['State'], 'CurrentState': {'Code': target[0], 'Name': target[1]}})
|
|
283
|
+
return {{'start-instances': 'StartingInstances', 'stop-instances': 'StoppingInstances', 'terminate-instances': 'TerminatingInstances'}[a.action]: output}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fufu-cloud-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.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
|
|
@@ -27,10 +27,33 @@ Dynamic: license-file
|
|
|
27
27
|
`gcloud-fufu`、`az-fufu`、`wrangler-fufu`、`bq-fufu`。這是 AWS、Google Cloud、Azure 與 Cloudflare 的**明確子集**,
|
|
28
28
|
只供 FUFU mock 環境使用;不會把部署轉送到正式雲端。
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
0.10.0 新增 Compute Engine/EC2 VM 與 GKE/EKS 介面的實際運算命令。
|
|
31
|
+
需要啟用相應 infrastructure backend 的 FUFU 服務端;VM 使用 KVM/KubeVirt,
|
|
32
|
+
叢集使用獨立 Kubernetes API 與共用節點,均不是官方雲端資源。
|
|
33
|
+
支援 VM 建立、SSH 指令、停止/啟動與刪除,以及叢集建立、查詢、
|
|
34
|
+
get-credentials/update-kubeconfig、waiter 與刪除。
|
|
35
|
+
取得憑證後可使用標準 kubectl 部署 Deployment、Service、Job 及受管 GPU 容器。
|
|
36
|
+
工作站需另安裝 kubectl;配套 Cloud Shell 已提供。
|
|
37
|
+
|
|
38
|
+
VM 尚無公開 IP、複寫儲存、live migration 或自助 GPU passthrough;
|
|
39
|
+
叢集尚無 workload PVC、公開 LoadBalancer、互動式 exec/attach/port-forward。
|
|
40
|
+
這批新 VM/cluster API 尚未支援官方 Terraform/OpenTofu/Pulumi provider。
|
|
41
|
+
原有 firewall、global LB、Redis 等 typed configuration 仍明示 `MOCK ONLY`,
|
|
42
|
+
不代表已建立相應網路或 Redis 資料面。
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
gcloud-fufu compute instances create demo-vm --image-family=debian-12
|
|
46
|
+
gcloud-fufu compute ssh demo-vm --command='uname -s; nproc'
|
|
47
|
+
gcloud-fufu container clusters create demo-gke
|
|
48
|
+
gcloud-fufu container clusters get-credentials demo-gke
|
|
49
|
+
kubectl get pods -A
|
|
50
|
+
aws-fufu eks create-cluster --name demo-eks
|
|
51
|
+
aws-fufu eks wait cluster-active --name demo-eks
|
|
52
|
+
aws-fufu eks update-kubeconfig --name demo-eks
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
這些操作需要 FUFU credential;不使用官方雲端金鑰。資源以租戶隔離,配額不足
|
|
56
|
+
會明確回覆 RESOURCE_EXHAUSTED。測試後請刪除所建 VM/cluster 以釋放資源。
|
|
34
57
|
|
|
35
58
|
0.9.8 提供 Cloud Run/Functions 的原生形式日誌查詢、CloudWatch Logs
|
|
36
59
|
tail/follow,以及 `fufu observability` 的日誌、請求指標與追蹤查詢。
|
|
@@ -73,9 +96,10 @@ insertAll/SELECT 與持久化日誌。`fufu_cloud_cli.gcp_sdk` 提供官方 Py
|
|
|
73
96
|
BigQuery、Cloud Tasks、Logging SDK 的明確 REST factory;可用 `[gcp]` extra
|
|
74
97
|
安裝已驗收相依。需要對應的新後端,並非透明 ADC 或所有 Google SDK 替換。
|
|
75
98
|
|
|
76
|
-
`fufu infrastructure`
|
|
77
|
-
|
|
78
|
-
|
|
99
|
+
`fufu infrastructure` 另保留 typed configuration mock 目錄,包含 Cloud SQL、
|
|
100
|
+
App Engine、DNS、VPC、Artifact Registry、Cloud Build、Gemini、VM Manager、
|
|
101
|
+
Eventarc 等。這個目錄只保存配置,不啟動 SQL/registry/網路/推論;
|
|
102
|
+
實際 VM/Kubernetes 請使用上述 compute/ec2/container/eks 指令。
|
|
79
103
|
UI 顯示僅設定,CLI stderr 保留 MOCK ONLY,不因輸出投影而隱藏。
|
|
80
104
|
新服務尚未接入 Terraform/OpenTofu/Pulumi;單機背景投遞不是雲端 HA。
|
|
81
105
|
|
|
@@ -107,7 +131,7 @@ Python 預設識別而遇到 Cloudflare HTTP 403/1010。識別標頭不取代
|
|
|
107
131
|
## Cloudflare 前端部署與 R2
|
|
108
132
|
|
|
109
133
|
```bash
|
|
110
|
-
pip install 'fufu-cloud-cli==0.
|
|
134
|
+
pip install 'fufu-cloud-cli==0.10.0'
|
|
111
135
|
export FUFU_CLOUDFLARE_CREDENTIAL_FILE=/run/secrets/fufu-cloudflare.json
|
|
112
136
|
wrangler-fufu capabilities
|
|
113
137
|
wrangler-fufu pages project create frontend --production-branch main
|
|
@@ -24,6 +24,7 @@ src/fufu_cloud_cli/gcp_sdk.py
|
|
|
24
24
|
src/fufu_cloud_cli/gcs.py
|
|
25
25
|
src/fufu_cloud_cli/http_runtime.py
|
|
26
26
|
src/fufu_cloud_cli/iac_state.py
|
|
27
|
+
src/fufu_cloud_cli/infrastructure.py
|
|
27
28
|
src/fufu_cloud_cli/lambda_adapter.py
|
|
28
29
|
src/fufu_cloud_cli/limits.py
|
|
29
30
|
src/fufu_cloud_cli/manifest.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
|
|
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
|
|
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
|
|
File without changes
|
{fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/runtime/functions/Dockerfile
RENAMED
|
File without changes
|
{fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/runtime/ingress/Dockerfile
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/terraform_bridge/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/terraform_bridge/aws_data.py
RENAMED
|
File without changes
|
{fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/terraform_bridge/common.py
RENAMED
|
File without changes
|
{fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/terraform_bridge/google.py
RENAMED
|
File without changes
|
{fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/terraform_bridge/google_pubsub.py
RENAMED
|
File without changes
|
{fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/terraform_bridge/remote_state.py
RENAMED
|
File without changes
|
{fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/terraform_bridge/runner.py
RENAMED
|
File without changes
|
{fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/vendor/AWS-CLI-LICENSE.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|