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.
Files changed (62) hide show
  1. {fufu_cloud_cli-0.9.8/src/fufu_cloud_cli.egg-info → fufu_cloud_cli-0.10.0}/PKG-INFO +33 -9
  2. {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/README.md +32 -8
  3. {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/pyproject.toml +1 -1
  4. {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/__init__.py +1 -1
  5. {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/api_backend.py +7 -0
  6. {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/capabilities.json +22 -12
  7. {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/cli.py +9 -0
  8. fufu_cloud_cli-0.10.0/src/fufu_cloud_cli/infrastructure.py +283 -0
  9. {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0/src/fufu_cloud_cli.egg-info}/PKG-INFO +33 -9
  10. {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli.egg-info/SOURCES.txt +1 -0
  11. {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/LICENSE +0 -0
  12. {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/MANIFEST.in +0 -0
  13. {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/setup.cfg +0 -0
  14. {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/__main__.py +0 -0
  15. {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/aws_errors.py +0 -0
  16. {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/aws_lambda.py +0 -0
  17. {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/aws_output.py +0 -0
  18. {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/bigquery.py +0 -0
  19. {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/capacity.py +0 -0
  20. {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/cloud_data.py +0 -0
  21. {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/cloudflare.py +0 -0
  22. {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/control.py +0 -0
  23. {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/docker_backend.py +0 -0
  24. {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/errors.py +0 -0
  25. {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/gcloud_environment.py +0 -0
  26. {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/gcloud_output.py +0 -0
  27. {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/gcp_infra.py +0 -0
  28. {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/gcp_sdk.py +0 -0
  29. {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/gcs.py +0 -0
  30. {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/http_runtime.py +0 -0
  31. {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/iac_state.py +0 -0
  32. {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/lambda_adapter.py +0 -0
  33. {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/limits.py +0 -0
  34. {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/manifest.py +0 -0
  35. {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/object_transfer.py +0 -0
  36. {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/observability.py +0 -0
  37. {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/public_access.py +0 -0
  38. {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/pubsub.py +0 -0
  39. {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/pulumi-endpoints.json +0 -0
  40. {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/pulumi_bridge.py +0 -0
  41. {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/remote_compute.py +0 -0
  42. {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/runtime/functions/Dockerfile +0 -0
  43. {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/runtime/ingress/Dockerfile +0 -0
  44. {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/runtime/ingress/proxy.py +0 -0
  45. {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/runtime_sdk.py +0 -0
  46. {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/secrets.py +0 -0
  47. {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/serverless.py +0 -0
  48. {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/terraform_bridge/__init__.py +0 -0
  49. {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/terraform_bridge/aws.py +0 -0
  50. {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/terraform_bridge/aws_data.py +0 -0
  51. {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/terraform_bridge/common.py +0 -0
  52. {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/terraform_bridge/google.py +0 -0
  53. {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/terraform_bridge/google_pubsub.py +0 -0
  54. {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/terraform_bridge/remote_state.py +0 -0
  55. {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/terraform_bridge/runner.py +0 -0
  56. {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/vendor/AWS-CLI-LICENSE.txt +0 -0
  57. {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/vendor/__init__.py +0 -0
  58. {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli/vendor/aws_text.py +0 -0
  59. {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli.egg-info/dependency_links.txt +0 -0
  60. {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli.egg-info/entry_points.txt +0 -0
  61. {fufu_cloud_cli-0.9.8 → fufu_cloud_cli-0.10.0}/src/fufu_cloud_cli.egg-info/requires.txt +0 -0
  62. {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.9.8
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
- 2026-09-10 後續原始碼與 Cloud Shell 新增 GKE、firewall、五類 global LB 與 Redis
31
- 配置命令,typed configuration 合計 22 類;都有 `MOCK ONLY` 提示、引用存在性與
32
- 相依刪除檢查,不建立 Kubernetes、VM、Redis 或網路資料面,也未增加 IaC 資源。
33
- 這批包含在 0.9.8;需要配套的 FUFU Console 與運算服務。
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` 提供 14 類 typed configuration mock。Compute Engine、
77
- Cloud SQL、App Engine、DNS、VPC、Artifact Registry、Cloud Build、Gemini、
78
- VM Manager、Eventarc 等設定不會啟動 VM/SQL/registry/網路/推論。
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.9.8'
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
- 2026-09-10 後續原始碼與 Cloud Shell 新增 GKE、firewall、五類 global LB 與 Redis
8
- 配置命令,typed configuration 合計 22 類;都有 `MOCK ONLY` 提示、引用存在性與
9
- 相依刪除檢查,不建立 Kubernetes、VM、Redis 或網路資料面,也未增加 IaC 資源。
10
- 這批包含在 0.9.8;需要配套的 FUFU Console 與運算服務。
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` 提供 14 類 typed configuration mock。Compute Engine、
54
- Cloud SQL、App Engine、DNS、VPC、Artifact Registry、Cloud Build、Gemini、
55
- VM Manager、Eventarc 等設定不會啟動 VM/SQL/registry/網路/推論。
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.9.8'
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "fufu-cloud-cli"
7
- version = "0.9.8"
7
+ version = "0.10.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"
@@ -1,3 +1,3 @@
1
1
  """FUFU CLI 與本機部署工具;相容範圍見 capabilities.json。"""
2
2
 
3
- __version__ = "0.9.8"
3
+ __version__ = "0.10.0"
@@ -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": "mock-only",
537
+ "status": "implemented",
538
538
  "backend": "api",
539
- "mode": "mock-only",
539
+ "mode": "simplified",
540
540
  "operations": [
541
- "typed-instance-configuration-crud"
541
+ "create-list-describe-delete", "stop-start", "guest-ssh-command"
542
542
  ],
543
- "reason": "僅保存設定;無 VM、disk、SSH、guest OS 或 hypervisor。"
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": "mock-only",
548
+ "status": "implemented",
548
549
  "backend": "api",
549
- "mode": "mock-only",
550
- "operations": ["typed-cluster-configuration-crud", "network-reference-validation"],
551
- "reason": "僅保存叢集配置;無 Kubernetes API、nodes、kubectl、scheduler 或 workloads。"
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": "mock-only",
641
- "reason": "明示 mock 目錄;VM 未執行,不能列為成功部署。"
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": "unsupported",
651
- "reason": "尚未實作可供 CLI 自動部署與驗證的服務契約。"
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.9.8
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
- 2026-09-10 後續原始碼與 Cloud Shell 新增 GKE、firewall、五類 global LB 與 Redis
31
- 配置命令,typed configuration 合計 22 類;都有 `MOCK ONLY` 提示、引用存在性與
32
- 相依刪除檢查,不建立 Kubernetes、VM、Redis 或網路資料面,也未增加 IaC 資源。
33
- 這批包含在 0.9.8;需要配套的 FUFU Console 與運算服務。
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` 提供 14 類 typed configuration mock。Compute Engine、
77
- Cloud SQL、App Engine、DNS、VPC、Artifact Registry、Cloud Build、Gemini、
78
- VM Manager、Eventarc 等設定不會啟動 VM/SQL/registry/網路/推論。
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.9.8'
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