fufu-cloud-cli 0.10.0__tar.gz → 0.10.1__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.10.0/src/fufu_cloud_cli.egg-info → fufu_cloud_cli-0.10.1}/PKG-INFO +6 -2
- {fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/README.md +5 -1
- {fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/pyproject.toml +1 -1
- {fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/src/fufu_cloud_cli/__init__.py +1 -1
- {fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/src/fufu_cloud_cli/aws_errors.py +1 -1
- {fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/src/fufu_cloud_cli/capabilities.json +13 -7
- {fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/src/fufu_cloud_cli/control.py +2 -1
- {fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/src/fufu_cloud_cli/pulumi_bridge.py +5 -3
- {fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/src/fufu_cloud_cli/terraform_bridge/common.py +5 -1
- {fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/src/fufu_cloud_cli/terraform_bridge/google.py +3 -0
- {fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/src/fufu_cloud_cli/terraform_bridge/runner.py +42 -9
- fufu_cloud_cli-0.10.1/src/fufu_cloud_cli/terraform_bridge/storage.py +173 -0
- {fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1/src/fufu_cloud_cli.egg-info}/PKG-INFO +6 -2
- {fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/src/fufu_cloud_cli.egg-info/SOURCES.txt +1 -0
- {fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/LICENSE +0 -0
- {fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/MANIFEST.in +0 -0
- {fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/setup.cfg +0 -0
- {fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/src/fufu_cloud_cli/__main__.py +0 -0
- {fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/src/fufu_cloud_cli/api_backend.py +0 -0
- {fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/src/fufu_cloud_cli/aws_lambda.py +0 -0
- {fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/src/fufu_cloud_cli/aws_output.py +0 -0
- {fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/src/fufu_cloud_cli/bigquery.py +0 -0
- {fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/src/fufu_cloud_cli/capacity.py +0 -0
- {fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/src/fufu_cloud_cli/cli.py +0 -0
- {fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/src/fufu_cloud_cli/cloud_data.py +0 -0
- {fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/src/fufu_cloud_cli/cloudflare.py +0 -0
- {fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/src/fufu_cloud_cli/docker_backend.py +0 -0
- {fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/src/fufu_cloud_cli/errors.py +0 -0
- {fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/src/fufu_cloud_cli/gcloud_environment.py +0 -0
- {fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/src/fufu_cloud_cli/gcloud_output.py +0 -0
- {fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/src/fufu_cloud_cli/gcp_infra.py +0 -0
- {fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/src/fufu_cloud_cli/gcp_sdk.py +0 -0
- {fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/src/fufu_cloud_cli/gcs.py +0 -0
- {fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/src/fufu_cloud_cli/http_runtime.py +0 -0
- {fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/src/fufu_cloud_cli/iac_state.py +0 -0
- {fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/src/fufu_cloud_cli/infrastructure.py +0 -0
- {fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/src/fufu_cloud_cli/lambda_adapter.py +0 -0
- {fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/src/fufu_cloud_cli/limits.py +0 -0
- {fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/src/fufu_cloud_cli/manifest.py +0 -0
- {fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/src/fufu_cloud_cli/object_transfer.py +0 -0
- {fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/src/fufu_cloud_cli/observability.py +0 -0
- {fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/src/fufu_cloud_cli/public_access.py +0 -0
- {fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/src/fufu_cloud_cli/pubsub.py +0 -0
- {fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/src/fufu_cloud_cli/pulumi-endpoints.json +0 -0
- {fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/src/fufu_cloud_cli/remote_compute.py +0 -0
- {fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/src/fufu_cloud_cli/runtime/functions/Dockerfile +0 -0
- {fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/src/fufu_cloud_cli/runtime/ingress/Dockerfile +0 -0
- {fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/src/fufu_cloud_cli/runtime/ingress/proxy.py +0 -0
- {fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/src/fufu_cloud_cli/runtime_sdk.py +0 -0
- {fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/src/fufu_cloud_cli/secrets.py +0 -0
- {fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/src/fufu_cloud_cli/serverless.py +0 -0
- {fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/src/fufu_cloud_cli/terraform_bridge/__init__.py +0 -0
- {fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/src/fufu_cloud_cli/terraform_bridge/aws.py +0 -0
- {fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/src/fufu_cloud_cli/terraform_bridge/aws_data.py +0 -0
- {fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/src/fufu_cloud_cli/terraform_bridge/google_pubsub.py +0 -0
- {fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/src/fufu_cloud_cli/terraform_bridge/remote_state.py +0 -0
- {fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/src/fufu_cloud_cli/vendor/AWS-CLI-LICENSE.txt +0 -0
- {fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/src/fufu_cloud_cli/vendor/__init__.py +0 -0
- {fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/src/fufu_cloud_cli/vendor/aws_text.py +0 -0
- {fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/src/fufu_cloud_cli.egg-info/dependency_links.txt +0 -0
- {fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/src/fufu_cloud_cli.egg-info/entry_points.txt +0 -0
- {fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/src/fufu_cloud_cli.egg-info/requires.txt +0 -0
- {fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/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.10.
|
|
3
|
+
Version: 0.10.1
|
|
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,6 +27,10 @@ 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
|
+
0.10.1 新增官方 Terraform/OpenTofu/Pulumi 的 S3/GCS bucket 生命週期與
|
|
31
|
+
S3 bucket tags。需配套 FUFU API;不支援 bucket versioning、retention、
|
|
32
|
+
KMS、bucket IAM、GCS bucket labels 或 force destroy。
|
|
33
|
+
|
|
30
34
|
0.10.0 新增 Compute Engine/EC2 VM 與 GKE/EKS 介面的實際運算命令。
|
|
31
35
|
需要啟用相應 infrastructure backend 的 FUFU 服務端;VM 使用 KVM/KubeVirt,
|
|
32
36
|
叢集使用獨立 Kubernetes API 與共用節點,均不是官方雲端資源。
|
|
@@ -131,7 +135,7 @@ Python 預設識別而遇到 Cloudflare HTTP 403/1010。識別標頭不取代
|
|
|
131
135
|
## Cloudflare 前端部署與 R2
|
|
132
136
|
|
|
133
137
|
```bash
|
|
134
|
-
pip install 'fufu-cloud-cli==0.10.
|
|
138
|
+
pip install 'fufu-cloud-cli==0.10.1'
|
|
135
139
|
export FUFU_CLOUDFLARE_CREDENTIAL_FILE=/run/secrets/fufu-cloudflare.json
|
|
136
140
|
wrangler-fufu capabilities
|
|
137
141
|
wrangler-fufu pages project create frontend --production-branch main
|
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
`gcloud-fufu`、`az-fufu`、`wrangler-fufu`、`bq-fufu`。這是 AWS、Google Cloud、Azure 與 Cloudflare 的**明確子集**,
|
|
5
5
|
只供 FUFU mock 環境使用;不會把部署轉送到正式雲端。
|
|
6
6
|
|
|
7
|
+
0.10.1 新增官方 Terraform/OpenTofu/Pulumi 的 S3/GCS bucket 生命週期與
|
|
8
|
+
S3 bucket tags。需配套 FUFU API;不支援 bucket versioning、retention、
|
|
9
|
+
KMS、bucket IAM、GCS bucket labels 或 force destroy。
|
|
10
|
+
|
|
7
11
|
0.10.0 新增 Compute Engine/EC2 VM 與 GKE/EKS 介面的實際運算命令。
|
|
8
12
|
需要啟用相應 infrastructure backend 的 FUFU 服務端;VM 使用 KVM/KubeVirt,
|
|
9
13
|
叢集使用獨立 Kubernetes API 與共用節點,均不是官方雲端資源。
|
|
@@ -108,7 +112,7 @@ Python 預設識別而遇到 Cloudflare HTTP 403/1010。識別標頭不取代
|
|
|
108
112
|
## Cloudflare 前端部署與 R2
|
|
109
113
|
|
|
110
114
|
```bash
|
|
111
|
-
pip install 'fufu-cloud-cli==0.10.
|
|
115
|
+
pip install 'fufu-cloud-cli==0.10.1'
|
|
112
116
|
export FUFU_CLOUDFLARE_CREDENTIAL_FILE=/run/secrets/fufu-cloudflare.json
|
|
113
117
|
wrangler-fufu capabilities
|
|
114
118
|
wrangler-fufu pages project create frontend --production-branch main
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Only documented provider error codes may cross the public transport boundary."""
|
|
2
2
|
AWS_ERRORS = frozenset('''ConditionalCheckFailedException ResourceNotFoundException ResourceInUseException ResourceAlreadyExistsException ValidationException
|
|
3
|
-
NoSuchKey NoSuchBucket NotFound NotModified ParameterNotFound ParameterAlreadyExists ParameterVersionNotFound QueueDoesNotExist
|
|
3
|
+
NoSuchKey NoSuchBucket NoSuchTagSet BucketAlreadyExists BucketAlreadyOwnedByYou BucketNotEmpty NotFound NotModified ParameterNotFound ParameterAlreadyExists ParameterVersionNotFound QueueDoesNotExist
|
|
4
4
|
PreconditionFailed InvalidRange InvalidParameterException InvalidParameterValue InvalidAttributeName InvalidAttributeValue QueueNameExists
|
|
5
5
|
PurgeQueueInProgress ReceiptHandleIsInvalid BatchEntryIdsNotDistinct EmptyBatchRequest BatchRequestTooLong InvalidBatchEntryId
|
|
6
6
|
TooManyEntriesInBatchRequest InvalidMessageContents AWS.SimpleQueueService.NonExistentQueue TooManyRequestsException InvalidRequestContentException'''.split())
|
|
@@ -27,11 +27,11 @@
|
|
|
27
27
|
"backend": "api",
|
|
28
28
|
"compatibility": "official-provider-subset",
|
|
29
29
|
"providers": { "hashicorp/aws": "6.63.0", "hashicorp/google": "8.1.0" },
|
|
30
|
-
"resources": ["aws_iam_role", "aws_iam_role_policy", "aws_lambda_function", "aws_lambda_function_url", "aws_lambda_permission", "google_service_account", "google_cloud_run_v2_service", "google_cloud_run_v2_service_iam_member", "aws_sqs_queue", "aws_ssm_parameter", "google_pubsub_topic", "google_pubsub_subscription"],
|
|
30
|
+
"resources": ["aws_iam_role", "aws_iam_role_policy", "aws_lambda_function", "aws_lambda_function_url", "aws_lambda_permission", "google_service_account", "google_cloud_run_v2_service", "google_cloud_run_v2_service_iam_member", "aws_sqs_queue", "aws_ssm_parameter", "google_pubsub_topic", "google_pubsub_subscription", "aws_s3_bucket", "google_storage_bucket"],
|
|
31
31
|
"operations": ["init", "plan", "saved-plan-apply", "refresh", "update", "import", "destroy"],
|
|
32
|
-
"engines": { "terraform": "1.16.
|
|
32
|
+
"engines": { "terraform": "1.16.2", "opentofu": "1.12.6" },
|
|
33
33
|
"state": ["isolated-local", "tenant-remote-http", "context-binding"],
|
|
34
|
-
"limitations": ["Linux-amd64-verified", "terraform-extra-required", "root-default-providers-only", "OpenTofu-data-resources-verified-compute-not-retested", "no-full-cloud-IAM-or-runtime-equivalence", "no-
|
|
34
|
+
"limitations": ["Linux-amd64-verified", "terraform-extra-required", "root-default-providers-only", "OpenTofu-data-resources-verified-compute-not-retested", "no-full-cloud-IAM-or-runtime-equivalence", "no-VM-cluster-Functions-Gen1-Gen2-IaC", "storage-bucket-lifecycle-and-S3-tags-only", "no-force-destroy-retention-versioning-bucket-IAM-KMS", "PubSub-requires-patched-emulator", "unsupported-APIs-and-fields-fail-explicitly"]
|
|
35
35
|
},
|
|
36
36
|
"iac-remote-state": {
|
|
37
37
|
"status": "implemented", "backend": "api", "mode": "simplified",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"pulumi-data": {
|
|
42
42
|
"status": "implemented", "backend": "api", "compatibility": "official-provider-subset",
|
|
43
43
|
"providers": { "pulumi": "3.261.0", "aws": "7.45.0", "gcp": "9.36.1" },
|
|
44
|
-
"resources": ["aws:sqs/queue:Queue", "aws:ssm/parameter:Parameter", "gcp:pubsub/topic:Topic", "gcp:pubsub/subscription:Subscription"],
|
|
44
|
+
"resources": ["aws:sqs/queue:Queue", "aws:ssm/parameter:Parameter", "gcp:pubsub/topic:Topic", "gcp:pubsub/subscription:Subscription", "aws:s3/bucket:Bucket", "gcp:storage/bucket:Bucket"],
|
|
45
45
|
"operations": ["preview", "up", "refresh", "update", "import", "destroy"],
|
|
46
46
|
"limitations": ["Node.js-runtime", "default-providers", "separate-local-DIY-state", "external-passphrase-required", "secret-fields-encrypted-not-whole-state", "no-remote-backend", "no-compute-resource-acceptance", "trusted-programs-only"]
|
|
47
47
|
},
|
|
@@ -197,7 +197,8 @@
|
|
|
197
197
|
"metadata-describe-update-merge-delete",
|
|
198
198
|
"paginated-object-list",
|
|
199
199
|
"binary-cat",
|
|
200
|
-
"UTF8-preview"
|
|
200
|
+
"UTF8-preview",
|
|
201
|
+
"Terraform-OpenTofu-Pulumi-bucket-lifecycle"
|
|
201
202
|
],
|
|
202
203
|
"limitations": [
|
|
203
204
|
"object-5TiB-policy-subject-to-host-disk-capacity",
|
|
@@ -209,7 +210,8 @@
|
|
|
209
210
|
"single-writer-per-volume",
|
|
210
211
|
"no-signed-URLs",
|
|
211
212
|
"no-versioning-retention-ACL-KMS",
|
|
212
|
-
"no-
|
|
213
|
+
"no-bucket-labels-IAM-PATCH-force-destroy",
|
|
214
|
+
"bucket-current-tenant-location-STANDARD-only"
|
|
213
215
|
]
|
|
214
216
|
},
|
|
215
217
|
"azure-catalog-api": {
|
|
@@ -270,7 +272,11 @@
|
|
|
270
272
|
"rb",
|
|
271
273
|
"ls",
|
|
272
274
|
"cp-upload-download",
|
|
273
|
-
"rm"
|
|
275
|
+
"rm",
|
|
276
|
+
"get-bucket-tagging",
|
|
277
|
+
"put-bucket-tagging",
|
|
278
|
+
"delete-bucket-tagging",
|
|
279
|
+
"Terraform-OpenTofu-Pulumi-bucket-lifecycle"
|
|
274
280
|
],
|
|
275
281
|
"limitations": [
|
|
276
282
|
"object-10000-parts-of-up-to-5GiB-subject-to-host-disk-capacity",
|
|
@@ -28,6 +28,7 @@ AWS_OPERATIONS = {
|
|
|
28
28
|
},
|
|
29
29
|
's3api': {
|
|
30
30
|
'CreateBucket': 'Bucket CreateBucketConfiguration', 'DeleteBucket': 'Bucket', 'ListBuckets': '', 'HeadBucket': 'Bucket', 'GetBucketLocation': 'Bucket',
|
|
31
|
+
'GetBucketTagging': 'Bucket', 'PutBucketTagging': 'Bucket Tagging', 'DeleteBucketTagging': 'Bucket',
|
|
31
32
|
'ListObjectsV2': 'Bucket Prefix MaxKeys ContinuationToken Delimiter StartAfter EncodingType', 'PutObject': 'Bucket Key Body ContentType Metadata',
|
|
32
33
|
'GetObject': 'Bucket Key ChecksumMode Range IfMatch IfNoneMatch', 'HeadObject': 'Bucket Key ChecksumMode IfMatch IfNoneMatch', 'DeleteObject': 'Bucket Key',
|
|
33
34
|
},
|
|
@@ -53,7 +54,7 @@ AWS_OPERATIONS = {
|
|
|
53
54
|
'PutRetentionPolicy': 'logGroupName retentionInDays', 'DeleteRetentionPolicy': 'logGroupName',
|
|
54
55
|
},
|
|
55
56
|
}
|
|
56
|
-
JSON_FIELDS = set('AssumeRolePolicyDocument PolicyDocument AttributeDefinitions KeySchema ProvisionedThroughput GlobalSecondaryIndexes LocalSecondaryIndexes StreamSpecification Item Key ExpressionAttributeNames ExpressionAttributeValues ExclusiveStartKey RequestItems CreateBucketConfiguration Metadata Attributes MessageAttributes Tags tags ParameterFilters Entries logEvents'.split())
|
|
57
|
+
JSON_FIELDS = set('AssumeRolePolicyDocument PolicyDocument AttributeDefinitions KeySchema ProvisionedThroughput GlobalSecondaryIndexes LocalSecondaryIndexes StreamSpecification Item Key ExpressionAttributeNames ExpressionAttributeValues ExclusiveStartKey RequestItems CreateBucketConfiguration Metadata Attributes MessageAttributes Tags tags Tagging ParameterFilters Entries logEvents'.split())
|
|
57
58
|
LIST_FIELDS = set('ActionNames ResourceArns Names AttributeNames MessageAttributeNames MessageSystemAttributeNames TagKeys logStreamNames'.split())
|
|
58
59
|
BOOL_FIELDS = set('ConsistentRead ScanIndexForward WithDecryption Overwrite Enabled Recursive descending startFromHead'.split())
|
|
59
60
|
INT_FIELDS = set('Limit MaxKeys MaxResults MaxNumberOfMessages VisibilityTimeout WaitTimeSeconds DelaySeconds BatchSize limit startTime endTime retentionInDays'.split())
|
|
@@ -16,11 +16,12 @@ import yaml
|
|
|
16
16
|
|
|
17
17
|
from .errors import FufuError, require
|
|
18
18
|
from .terraform_bridge.common import Bridge
|
|
19
|
-
from .terraform_bridge.runner import AWS_ENDPOINT_ALIASES, bind_context, child_environment, fcntl, private_directory, serve
|
|
19
|
+
from .terraform_bridge.runner import AWS_ENDPOINT_ALIASES, bind_context, child_environment, fcntl, private_directory, serve, s3_control_environment
|
|
20
20
|
|
|
21
21
|
SDK_VERSIONS = {'pulumi': '3.261.0', 'aws': '7.45.0', 'gcp': '9.36.1'}
|
|
22
22
|
RESOURCE_TYPES = {'aws:sqs/queue:Queue', 'aws:ssm/parameter:Parameter',
|
|
23
|
-
'gcp:pubsub/topic:Topic', 'gcp:pubsub/subscription:Subscription'
|
|
23
|
+
'gcp:pubsub/topic:Topic', 'gcp:pubsub/subscription:Subscription',
|
|
24
|
+
'aws:s3/bucket:Bucket', 'gcp:storage/bucket:Bucket'}
|
|
24
25
|
|
|
25
26
|
|
|
26
27
|
def read_mapping(path):
|
|
@@ -55,7 +56,7 @@ def provider_config(port, bridge, providers):
|
|
|
55
56
|
keys = [key for key in keys if key not in aliases[1:]]
|
|
56
57
|
values = {'endpoints': [{key: f'http://127.0.0.1:{port}/aws/{key}' for key in keys}],
|
|
57
58
|
'region': bridge.identities['aws']['region'], 'maxRetries': 1,
|
|
58
|
-
'skipCredentialsValidation': True, 'skipMetadataApiCheck': True, 'skipRegionValidation': True}
|
|
59
|
+
'skipCredentialsValidation': True, 'skipMetadataApiCheck': True, 'skipRegionValidation': True, 's3UsePathStyle': True}
|
|
59
60
|
else:
|
|
60
61
|
values = {}
|
|
61
62
|
versions = {'iam': 'v1/', 'iam_beta': 'v1/', 'cloud_run_v2': 'v2/', 'cloud_run': 'v1/',
|
|
@@ -164,6 +165,7 @@ def command(argv, Parser):
|
|
|
164
165
|
port = adapter_port(work)
|
|
165
166
|
server, thread = serve(bridge, port, key, secret, token)
|
|
166
167
|
env = child_environment(root, work, key, secret, token)
|
|
168
|
+
if 'aws' in args.providers: env.update(s3_control_environment(port))
|
|
167
169
|
for key_name in ['NODE_OPTIONS', 'NODE_PATH']: env.pop(key_name, None)
|
|
168
170
|
env.update(PULUMI_HOME=str(work / 'home'), PULUMI_BACKEND_URL=(work / 'backend').as_uri(),
|
|
169
171
|
PULUMI_CONFIG_PASSPHRASE=passphrase, PULUMI_SKIP_UPDATE_CHECK='true', PULUMI_SKIP_CONFIRMATIONS='false',
|
{fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/src/fufu_cloud_cli/terraform_bridge/common.py
RENAMED
|
@@ -49,13 +49,17 @@ def signing_valid(method, target, headers, body, access_key, secret):
|
|
|
49
49
|
if headers.get('x-amz-content-sha256', digest) != digest:
|
|
50
50
|
return None
|
|
51
51
|
path, _, query = target.partition('?')
|
|
52
|
+
s3_path = path
|
|
52
53
|
parts = []
|
|
53
54
|
for part in path.split('/'):
|
|
54
55
|
if part == '..':
|
|
55
56
|
if parts: parts.pop()
|
|
56
57
|
elif part and part != '.': parts.append(part)
|
|
57
58
|
path = '/' + '/'.join(parts) + ('/' if path.endswith('/') and parts else '')
|
|
58
|
-
|
|
59
|
+
# S3 and S3 Control use the transmitted path without normalization or
|
|
60
|
+
# double encoding. The official SDK signs encoded ARN path components this
|
|
61
|
+
# way; all services still require the actual payload digest and full HMAC.
|
|
62
|
+
canonical_path = s3_path if service == 's3' else quote(path, safe='/~')
|
|
59
63
|
canonical_query = '&'.join(sorted(v if '=' in v else v + '=' for v in query.split('&') if v))
|
|
60
64
|
canonical_headers = ''.join(n + ':' + ' '.join(headers[n].strip().split()) + '\n' for n in names)
|
|
61
65
|
canonical = '\n'.join([method, canonical_path, canonical_query, canonical_headers, signed, digest])
|
{fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/src/fufu_cloud_cli/terraform_bridge/google.py
RENAMED
|
@@ -151,6 +151,9 @@ def run(bridge, method, path, query, body):
|
|
|
151
151
|
|
|
152
152
|
def dispatch(bridge, service, method, path, query, data):
|
|
153
153
|
body = json.loads(data) if data else {}
|
|
154
|
+
if service == 'storage':
|
|
155
|
+
from .storage import google as bucket
|
|
156
|
+
return bucket(bridge, method, path, query, body)
|
|
154
157
|
if service == 'pubsub':
|
|
155
158
|
from .google_pubsub import dispatch as pubsub
|
|
156
159
|
return pubsub(bridge, method, path, query, body)
|
{fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/src/fufu_cloud_cli/terraform_bridge/runner.py
RENAMED
|
@@ -24,14 +24,14 @@ from urllib.parse import parse_qs, urlsplit
|
|
|
24
24
|
from ..errors import FufuError, require
|
|
25
25
|
from ..aws_errors import AWS_ERRORS
|
|
26
26
|
from .common import Bridge, signing_valid
|
|
27
|
-
from . import aws, aws_data, google
|
|
27
|
+
from . import aws, aws_data, google, storage
|
|
28
28
|
from .remote_state import StateProxy, state_name
|
|
29
29
|
|
|
30
30
|
SUPPORTED_PROVIDERS = {'aws': '6.63.0', 'google': '8.1.0'}
|
|
31
31
|
SUPPORTED_RESOURCES = {'aws_iam_role', 'aws_iam_role_policy', 'aws_lambda_function', 'aws_lambda_function_url',
|
|
32
32
|
'aws_lambda_permission', 'google_service_account', 'google_cloud_run_v2_service',
|
|
33
33
|
'google_cloud_run_v2_service_iam_member', 'aws_sqs_queue', 'aws_ssm_parameter',
|
|
34
|
-
'google_pubsub_topic', 'google_pubsub_subscription'}
|
|
34
|
+
'google_pubsub_topic', 'google_pubsub_subscription', 'aws_s3_bucket', 'google_storage_bucket'}
|
|
35
35
|
SUPPORTED_DATA = {'aws_caller_identity', 'aws_iam_policy_document', 'aws_region', 'aws_partition'}
|
|
36
36
|
# Mutually exclusive aliases declared by the pinned official AWS provider.
|
|
37
37
|
AWS_ENDPOINT_ALIASES = '''directoryservice ds;cognitoidentityprovider cognitoidp;amp prometheus prometheusservice;
|
|
@@ -69,6 +69,7 @@ def validate_provider_configuration(root, data_dir):
|
|
|
69
69
|
for kind in declaration:
|
|
70
70
|
require(not kind.startswith(('aws_', 'google_')) or kind in supported,
|
|
71
71
|
'UNSUPPORTED_TERRAFORM_RESOURCE', '未支援的 Terraform 資源或 data source:' + kind)
|
|
72
|
+
if category == 'resource': storage.validate_hcl(kind, declaration[kind])
|
|
72
73
|
providers = document.get('provider', [])
|
|
73
74
|
if isinstance(providers, dict): providers = [providers]
|
|
74
75
|
for entry in providers:
|
|
@@ -127,7 +128,14 @@ def serve(bridge, port, aws_key, aws_secret, google_token, state=None):
|
|
|
127
128
|
provider = 'state' if self.path.startswith('/state') else 'gcp' if self.path.startswith('/gcp/') else 'aws'
|
|
128
129
|
headers = {k.lower(): v for k, v in self.headers.items()}
|
|
129
130
|
try:
|
|
130
|
-
|
|
131
|
+
parsed = urlsplit(self.path)
|
|
132
|
+
# S3 Control injects the account into custom endpoint hosts. A
|
|
133
|
+
# closed loopback HTTP proxy handles only this exact target;
|
|
134
|
+
# it never resolves the hostname or forwards network traffic.
|
|
135
|
+
control_host = str(bridge.identities.get('aws', {}).get('accountId', '')) + f'.127.0.0.1:{port}'
|
|
136
|
+
s3_control = (parsed.scheme == 'http' and parsed.netloc == control_host and
|
|
137
|
+
parsed.path.startswith('/aws/s3control/') and headers.get('host') == control_host)
|
|
138
|
+
require((s3_control or not parsed.netloc and headers.get('host') == f'127.0.0.1:{port}') and 'origin' not in headers and 'transfer-encoding' not in headers,
|
|
131
139
|
'AUTHENTICATION_FAILED', 'Terraform adapter 僅接受目前 loopback 工作階段。')
|
|
132
140
|
if provider == 'state':
|
|
133
141
|
require(state is not None, 'ACCESS_DENIED', '此工作階段沒有啟用遠端 state。')
|
|
@@ -141,16 +149,23 @@ def serve(bridge, port, aws_key, aws_secret, google_token, state=None):
|
|
|
141
149
|
body = self.rfile.read(length)
|
|
142
150
|
require(len(body) == length, 'INVALID_TERRAFORM_REQUEST', 'Request 未完整傳送。')
|
|
143
151
|
require(provider == 'state' or provider in bridge.clients, 'AUTHENTICATION_FAILED', '目前未啟用此 provider。')
|
|
144
|
-
parsed = urlsplit(self.path)
|
|
145
152
|
query = parse_qs(parsed.query, keep_blank_values=True)
|
|
146
153
|
with bridge.lock:
|
|
147
154
|
if provider == 'state':
|
|
148
155
|
result = state.dispatch(self.command, self.path, headers, body)
|
|
149
156
|
elif provider == 'aws':
|
|
150
|
-
signed = signing_valid(self.command,
|
|
157
|
+
signed = signing_valid(self.command, parsed.path + ('?' + parsed.query if parsed.query else ''), headers, body, aws_key, aws_secret)
|
|
151
158
|
require(signed, 'AUTHENTICATION_FAILED', 'Terraform AWS 簽章無效。')
|
|
152
159
|
service, region = signed
|
|
153
160
|
require(service in {'iam', 'sts'} or region == bridge.identities['aws']['region'], 'ACCESS_DENIED', 'AWS region 與 FUFU scope 不符。')
|
|
161
|
+
if s3_control:
|
|
162
|
+
require(service == 's3', 'AUTHENTICATION_FAILED', 'S3 Control 簽章服務無效。')
|
|
163
|
+
# The official provider explicitly falls back to
|
|
164
|
+
# Get/Put/DeleteBucketTagging on MethodNotAllowed.
|
|
165
|
+
# Actual access is checked by those native IAM APIs.
|
|
166
|
+
self.respond(405, storage.xml('ErrorResponse', {'Error': {'Code': 'MethodNotAllowed',
|
|
167
|
+
'Message': 'Use bucket tagging APIs for this S3-compatible backend.'}}), {'Content-Type': 'application/xml'})
|
|
168
|
+
return
|
|
154
169
|
prefix = '/aws/' + service
|
|
155
170
|
queue_path = service == 'sqs' and re.fullmatch(r'/[0-9]{12}/[A-Za-z0-9_-]{1,80}', parsed.path)
|
|
156
171
|
require(queue_path or parsed.path == '/aws' or parsed.path.startswith(prefix + '/') or parsed.path == prefix,
|
|
@@ -160,6 +175,7 @@ def serve(bridge, port, aws_key, aws_secret, google_token, state=None):
|
|
|
160
175
|
elif service == 'lambda': result = aws.lambda_api(bridge, self.command, route, query, body)
|
|
161
176
|
elif service in {'sqs', 'ssm'}:
|
|
162
177
|
result = aws_data.dispatch(bridge, service, self.command, route, headers, body, f'http://127.0.0.1:{port}')
|
|
178
|
+
elif service == 's3': result = storage.s3(bridge, self.command, route, query, headers, body)
|
|
163
179
|
else: raise FufuError('UNSUPPORTED_TERRAFORM_OPERATION', '此 AWS API 尚未列入 Terraform 支援範圍。')
|
|
164
180
|
else:
|
|
165
181
|
require(hmac.compare_digest(headers.get('authorization', ''), 'Bearer ' + google_token), 'AUTHENTICATION_FAILED', 'Terraform Google 工作階段無效。')
|
|
@@ -174,7 +190,7 @@ def serve(bridge, port, aws_key, aws_secret, google_token, state=None):
|
|
|
174
190
|
expected = isinstance(error, FufuError)
|
|
175
191
|
code = error.code if expected else 'TERRAFORM_ADAPTER_ERROR'
|
|
176
192
|
message = error.message if expected else 'Terraform adapter 無法處理此請求;未輸出設定或 credential。'
|
|
177
|
-
status = 404 if code in {'NOT_FOUND', 'RESOURCE_NOT_FOUND'} else 409 if code in {'RESOURCE_CONFLICT', 'ALREADY_EXISTS'} else 401 if code == 'AUTHENTICATION_FAILED' else 403 if code == 'ACCESS_DENIED' else 400 if expected else 500
|
|
193
|
+
status = 404 if code in {'NOT_FOUND', 'RESOURCE_NOT_FOUND'} else 409 if code in {'RESOURCE_CONFLICT', 'ALREADY_EXISTS'} else 401 if code == 'AUTHENTICATION_FAILED' else 403 if code == 'ACCESS_DENIED' else 412 if code == 'PRECONDITION_FAILED' else 400 if expected else 500
|
|
178
194
|
bridge.calls.append({'provider': provider, 'error': code})
|
|
179
195
|
if provider == 'aws':
|
|
180
196
|
aws_code = {404: 'ResourceNotFoundException', 409: 'ResourceConflictException', 401: 'UnrecognizedClientException', 403: 'AccessDeniedException'}.get(status, 'InvalidParameterValueException')
|
|
@@ -185,7 +201,13 @@ def serve(bridge, port, aws_key, aws_secret, google_token, state=None):
|
|
|
185
201
|
# Providers use these codes to recognize absent resources on refresh/destroy.
|
|
186
202
|
if code in AWS_ERRORS:
|
|
187
203
|
aws_code = code
|
|
188
|
-
if '/
|
|
204
|
+
if urlsplit(self.path).path.startswith(('/aws/s3/', '/aws/s3control/')) or self.path == '/aws/s3':
|
|
205
|
+
aws_code = {404: 'NoSuchBucket', 409: 'BucketAlreadyExists', 401: 'SignatureDoesNotMatch', 403: 'AccessDenied'}.get(status, 'InvalidRequest')
|
|
206
|
+
if code in AWS_ERRORS: aws_code = code
|
|
207
|
+
value = {'Code': aws_code, 'Message': message}
|
|
208
|
+
data = storage.xml('ErrorResponse', {'Error': value}) if '/aws/s3control/' in self.path else storage.xml('Error', value)
|
|
209
|
+
self.respond(status, data, {'Content-Type': 'application/xml'})
|
|
210
|
+
elif '/iam' in self.path:
|
|
189
211
|
aws_code = {404: 'NoSuchEntity', 409: 'EntityAlreadyExists', 403: 'AccessDenied'}.get(status, 'InvalidInput')
|
|
190
212
|
data = ('<ErrorResponse>' + aws.xml_value('Error', {'Type': 'Sender', 'Code': aws_code, 'Message': message}) + '</ErrorResponse>').encode()
|
|
191
213
|
self.respond(status, data, {'Content-Type': 'text/xml'})
|
|
@@ -198,7 +220,7 @@ def serve(bridge, port, aws_key, aws_secret, google_token, state=None):
|
|
|
198
220
|
error_headers['x-amzn-query-error'] = query_code + (';Receiver' if status >= 500 else ';Sender')
|
|
199
221
|
self.respond(status, json.dumps({'__type': aws_code, 'message': message}).encode(), error_headers)
|
|
200
222
|
else:
|
|
201
|
-
google_status = {404: 'NOT_FOUND', 409: 'ALREADY_EXISTS', 401: 'UNAUTHENTICATED', 403: 'PERMISSION_DENIED'}.get(status, 'INVALID_ARGUMENT')
|
|
223
|
+
google_status = {404: 'NOT_FOUND', 409: 'ALREADY_EXISTS', 401: 'UNAUTHENTICATED', 403: 'PERMISSION_DENIED', 412: 'FAILED_PRECONDITION'}.get(status, 'INVALID_ARGUMENT')
|
|
202
224
|
self.respond(status, json.dumps({'error': {'code': status, 'status': google_status, 'message': message}}).encode(), {})
|
|
203
225
|
def respond(self, status, data, headers):
|
|
204
226
|
try:
|
|
@@ -229,6 +251,16 @@ def child_environment(root, data_dir, key, secret, token):
|
|
|
229
251
|
return env
|
|
230
252
|
|
|
231
253
|
|
|
254
|
+
def s3_control_environment(port):
|
|
255
|
+
"""Route account-prefixed S3 Control requests to the closed local adapter.
|
|
256
|
+
|
|
257
|
+
All other non-loopback plaintext HTTP targets fail closed; HTTPS plugin
|
|
258
|
+
downloads are unaffected. No proxy forwarding or CONNECT is implemented.
|
|
259
|
+
"""
|
|
260
|
+
return {'HTTP_PROXY': f'http://127.0.0.1:{port}', 'http_proxy': f'http://127.0.0.1:{port}',
|
|
261
|
+
'NO_PROXY': '127.0.0.1,localhost', 'no_proxy': '127.0.0.1,localhost'}
|
|
262
|
+
|
|
263
|
+
|
|
232
264
|
def private_directory(path):
|
|
233
265
|
require(not path.is_symlink(), 'INVALID_TERRAFORM_STATE', 'FUFU state 目錄不可為符號連結。')
|
|
234
266
|
path.mkdir(mode=0o700, exist_ok=True)
|
|
@@ -282,7 +314,7 @@ def endpoints(port, providers, schemas=None):
|
|
|
282
314
|
aliases = [key for key in group.split() if key in keys]
|
|
283
315
|
keys = [key for key in keys if key not in aliases[1:]]
|
|
284
316
|
result['aws'] = {'skip_credentials_validation': True, 'skip_metadata_api_check': True, 'skip_region_validation': True,
|
|
285
|
-
'max_retries': 1, 'endpoints': [{key: base + '/aws/' + key for key in keys}]}
|
|
317
|
+
'max_retries': 1, 's3_use_path_style': True, 'endpoints': [{key: base + '/aws/' + key for key in keys}]}
|
|
286
318
|
else:
|
|
287
319
|
keys = [key for key in schema.get('attributes', {}) if key.endswith('_custom_endpoint')] or ['iam_custom_endpoint', 'cloud_run_v2_custom_endpoint']
|
|
288
320
|
versions = {'iam': 'v1/', 'iam_beta': 'v1/', 'cloud_run_v2': 'v2/', 'cloud_run': 'v1/', 'cloudfunctions2': 'v2/', 'cloud_functions': 'v1/', 'storage': 'storage/v1/', 'pubsub': 'v1/'}
|
|
@@ -332,6 +364,7 @@ def command(argv, Parser, engine='terraform'):
|
|
|
332
364
|
key, secret, token = 'FUFUTF' + secrets.token_hex(10).upper(), secrets.token_urlsafe(32), secrets.token_urlsafe(32)
|
|
333
365
|
port = 24000 + int(hashlib.sha256(str(root).encode()).hexdigest()[:8], 16) % 16000
|
|
334
366
|
env = child_environment(root, data_dir, key, secret, token)
|
|
367
|
+
if 'aws' in args.providers: env.update(s3_control_environment(port))
|
|
335
368
|
bridge = server = thread = owned_override = cli_config = lock_fd = child = None
|
|
336
369
|
previous_signals = {}
|
|
337
370
|
try:
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""Bucket lifecycle over the existing tenant storage APIs.
|
|
2
|
+
|
|
3
|
+
No metadata-only fallback for encryption, ACLs, retention or object versions.
|
|
4
|
+
Unsupported S3 optional reads return the standard NotImplemented response;
|
|
5
|
+
the official provider explicitly handles this for third-party S3 backends.
|
|
6
|
+
"""
|
|
7
|
+
import json
|
|
8
|
+
import re
|
|
9
|
+
import xml.etree.ElementTree as ET
|
|
10
|
+
from urllib.parse import quote, unquote
|
|
11
|
+
|
|
12
|
+
from ..errors import FufuError, require
|
|
13
|
+
from .common import only, default
|
|
14
|
+
|
|
15
|
+
S3_NS = 'http://s3.amazonaws.com/doc/2006-03-01/'
|
|
16
|
+
OPTIONAL_S3_READS = {'acl', 'policy', 'cors', 'website', 'versioning', 'accelerate',
|
|
17
|
+
'requestPayment', 'logging', 'lifecycle', 'replication', 'encryption', 'object-lock'}
|
|
18
|
+
META_ARGUMENTS = {'count', 'for_each', 'depends_on', 'provider', 'lifecycle', 'timeouts'}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def validate_hcl(kind, configs):
|
|
22
|
+
"""Reject unsupported bucket configuration before provider-side creation.
|
|
23
|
+
|
|
24
|
+
Runtime checks still validate the evaluated request. Client-only options
|
|
25
|
+
such as force_destroy must be explicit supported literals.
|
|
26
|
+
"""
|
|
27
|
+
if kind not in {'aws_s3_bucket', 'google_storage_bucket'}: return
|
|
28
|
+
allowed = ({'bucket', 'bucket_prefix', 'tags', 'force_destroy', 'region'} if kind == 'aws_s3_bucket'
|
|
29
|
+
else {'name', 'location', 'project', 'storage_class', 'force_destroy'}) | META_ARGUMENTS
|
|
30
|
+
for config in configs.values():
|
|
31
|
+
only(config, allowed, kind)
|
|
32
|
+
require(config.get('force_destroy', False) is False, 'UNSUPPORTED_TERRAFORM_FIELD',
|
|
33
|
+
'Bucket force_destroy 尚未支援;先明確清除物件,再刪除 bucket。')
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def xml(root, values=None):
|
|
37
|
+
node = ET.Element(root, xmlns=S3_NS)
|
|
38
|
+
def add(parent, key, value):
|
|
39
|
+
for item in value if isinstance(value, list) else [value]:
|
|
40
|
+
child = ET.SubElement(parent, key)
|
|
41
|
+
if isinstance(item, dict):
|
|
42
|
+
for name, nested in item.items(): add(child, name, nested)
|
|
43
|
+
elif item is not None: child.text = str(item)
|
|
44
|
+
for key, value in (values or {}).items(): add(node, key, value)
|
|
45
|
+
return ET.tostring(node, encoding='utf-8', xml_declaration=True)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def s3_error(code, status=400):
|
|
49
|
+
return status, xml('Error', {'Code': code, 'Message': 'S3 operation unavailable in this FUFU bucket contract.'}), {'Content-Type': 'application/xml'}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def parse_xml(data, root):
|
|
53
|
+
require(len(data) <= 65536 and b'<!DOCTYPE' not in data.upper() and b'<!ENTITY' not in data.upper(),
|
|
54
|
+
'INVALID_TERRAFORM_REQUEST', 'S3 bucket XML 格式或大小無效。')
|
|
55
|
+
try: node = ET.fromstring(data)
|
|
56
|
+
except ET.ParseError: raise FufuError('INVALID_TERRAFORM_REQUEST', 'S3 bucket XML 格式無效。') from None
|
|
57
|
+
for element in node.iter():
|
|
58
|
+
require(not element.attrib, 'UNSUPPORTED_TERRAFORM_FIELD', 'S3 bucket XML 屬性尚未支援。')
|
|
59
|
+
if element.tag.startswith('{'):
|
|
60
|
+
require(element.tag.startswith('{' + S3_NS + '}'), 'INVALID_TERRAFORM_REQUEST', 'S3 XML namespace 無效。')
|
|
61
|
+
element.tag = element.tag.split('}', 1)[1]
|
|
62
|
+
require(node.tag == root, 'INVALID_TERRAFORM_REQUEST', 'S3 bucket XML root 無效。')
|
|
63
|
+
return node
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def s3(bridge, method, path, query, headers, data):
|
|
67
|
+
match = re.fullmatch(r'/([a-z0-9][a-z0-9.-]{1,61}[a-z0-9])/?', path)
|
|
68
|
+
require(match, 'UNSUPPORTED_TERRAFORM_OPERATION', 'S3 IaC 目前提供 bucket 生命週期。')
|
|
69
|
+
bucket = match[1]
|
|
70
|
+
require(not re.fullmatch(r'[0-9]+(?:\.[0-9]+){3}', bucket) and '..' not in bucket,
|
|
71
|
+
'INVALID_TERRAFORM_REQUEST', 'S3 bucket 名稱無效。')
|
|
72
|
+
for key, value in headers.items():
|
|
73
|
+
if key.startswith(('x-amz-grant-', 'x-amz-object-lock-', 'x-amz-server-side-encryption')):
|
|
74
|
+
raise FufuError('UNSUPPORTED_TERRAFORM_FIELD', 'Bucket ACL、Object Lock 與加密尚未支援。')
|
|
75
|
+
require(headers.get('x-amz-acl') in (None, 'private') and headers.get('x-amz-bucket-namespace') in (None, 'global'),
|
|
76
|
+
'UNSUPPORTED_TERRAFORM_FIELD', '目前只支援 private、global namespace bucket。')
|
|
77
|
+
require(headers.get('x-amz-expected-bucket-owner') in (None, bridge.identities['aws']['accountId']),
|
|
78
|
+
'ACCESS_DENIED', 'Bucket owner 與目前租戶不符。')
|
|
79
|
+
params = dict(query); params.pop('x-id', None)
|
|
80
|
+
require(len(params) <= 1 and all(v == [''] for v in params.values()),
|
|
81
|
+
'UNSUPPORTED_TERRAFORM_FIELD', 'S3 bucket query 尚未支援。')
|
|
82
|
+
operation = next(iter(params), '')
|
|
83
|
+
reply = {'Content-Type': 'application/xml', 'x-amz-bucket-region': bridge.identities['aws']['region']}
|
|
84
|
+
try:
|
|
85
|
+
if operation in OPTIONAL_S3_READS and method == 'GET':
|
|
86
|
+
# Check existence and authorization; do not report unverified config
|
|
87
|
+
# as an empty successful response to the official provider.
|
|
88
|
+
bridge.native('s3', 'HeadBucket', {'Bucket': bucket})
|
|
89
|
+
return s3_error('NotImplemented', 501)
|
|
90
|
+
if not operation and method == 'HEAD':
|
|
91
|
+
bridge.native('s3', 'HeadBucket', {'Bucket': bucket})
|
|
92
|
+
return 200, b'', reply
|
|
93
|
+
if not operation and method == 'PUT':
|
|
94
|
+
config = {}
|
|
95
|
+
if data:
|
|
96
|
+
document = parse_xml(data, 'CreateBucketConfiguration')
|
|
97
|
+
keys = [item.tag for item in document]
|
|
98
|
+
require(len(keys) == len(set(keys)) and set(keys) <= {'LocationConstraint', 'Tags'},
|
|
99
|
+
'UNSUPPORTED_TERRAFORM_FIELD', 'S3 create bucket configuration 尚未支援。')
|
|
100
|
+
# The pinned provider retries without creation tags, then uses
|
|
101
|
+
# PutBucketTagging. Fail before creating any partial resource.
|
|
102
|
+
if 'Tags' in keys: return s3_error('UnsupportedArgument')
|
|
103
|
+
location = document.find('LocationConstraint')
|
|
104
|
+
if location is not None:
|
|
105
|
+
require(not list(location), 'INVALID_TERRAFORM_REQUEST', 'Bucket location 格式無效。')
|
|
106
|
+
config['CreateBucketConfiguration'] = {'LocationConstraint': location.text or ''}
|
|
107
|
+
bridge.native('s3', 'CreateBucket', {'Bucket': bucket, **config})
|
|
108
|
+
return 200, b'', {**reply, 'Location': '/' + bucket}
|
|
109
|
+
if not operation and method == 'DELETE':
|
|
110
|
+
bridge.native('s3', 'DeleteBucket', {'Bucket': bucket})
|
|
111
|
+
return 204, b'', reply
|
|
112
|
+
if operation == 'location' and method == 'GET':
|
|
113
|
+
value = bridge.native('s3', 'GetBucketLocation', {'Bucket': bucket}).get('LocationConstraint')
|
|
114
|
+
node = ET.Element('LocationConstraint', xmlns=S3_NS); node.text = value
|
|
115
|
+
return 200, ET.tostring(node, encoding='utf-8'), reply
|
|
116
|
+
if operation == 'tagging':
|
|
117
|
+
if method == 'GET':
|
|
118
|
+
value = bridge.native('s3', 'GetBucketTagging', {'Bucket': bucket})
|
|
119
|
+
return 200, xml('Tagging', {'TagSet': {'Tag': value.get('TagSet', [])}}), reply
|
|
120
|
+
if method == 'DELETE':
|
|
121
|
+
bridge.native('s3', 'DeleteBucketTagging', {'Bucket': bucket})
|
|
122
|
+
return 204, b'', reply
|
|
123
|
+
if method == 'PUT':
|
|
124
|
+
root = parse_xml(data, 'Tagging')
|
|
125
|
+
require(len(root) == 1 and root[0].tag == 'TagSet', 'INVALID_TERRAFORM_REQUEST', 'TagSet 必須唯一。')
|
|
126
|
+
tags = []
|
|
127
|
+
for tag in root[0]:
|
|
128
|
+
require(tag.tag == 'Tag' and sorted(c.tag for c in tag) == ['Key', 'Value'] and all(not list(c) for c in tag),
|
|
129
|
+
'INVALID_TERRAFORM_REQUEST', 'S3 tag 必須包含 Key/Value。')
|
|
130
|
+
tags.append({c.tag: c.text or '' for c in tag})
|
|
131
|
+
bridge.native('s3', 'PutBucketTagging', {'Bucket': bucket, 'Tagging': {'TagSet': tags}})
|
|
132
|
+
return 200, b'', reply
|
|
133
|
+
raise FufuError('UNSUPPORTED_TERRAFORM_OPERATION', '此 S3 bucket 操作尚未支援。')
|
|
134
|
+
except FufuError as error:
|
|
135
|
+
codes = {'NoSuchBucket': 404, 'NoSuchTagSet': 404, 'NotFound': 404,
|
|
136
|
+
'BucketNotEmpty': 409, 'BucketAlreadyExists': 409, 'BucketAlreadyOwnedByYou': 409}
|
|
137
|
+
if error.code in codes: return s3_error('NoSuchBucket' if error.code == 'NotFound' else error.code, codes[error.code])
|
|
138
|
+
if error.code in {'RESOURCE_NOT_FOUND', 'NOT_FOUND'}: return s3_error('NoSuchBucket', 404)
|
|
139
|
+
if error.code in {'ACCESS_DENIED', 'PERMISSION_DENIED'}: return s3_error('AccessDenied', 403)
|
|
140
|
+
raise
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def google(bridge, method, path, query, body):
|
|
144
|
+
match = re.fullmatch(r'/storage/v1/b(?:/([^/]+))?/?', path)
|
|
145
|
+
require(match, 'UNSUPPORTED_TERRAFORM_OPERATION', 'GCS IaC 目前提供 bucket 生命週期。')
|
|
146
|
+
only(query, ['project', 'alt', 'prettyPrint'], 'GCS bucket query')
|
|
147
|
+
require(all(len(v) == 1 for v in query.values()), 'INVALID_TERRAFORM_REQUEST', '重複的 GCS query。')
|
|
148
|
+
if 'project' in query: bridge.project(query['project'][0])
|
|
149
|
+
default(query.get('alt', [None])[0], 'json', 'GCS response format')
|
|
150
|
+
name = unquote(match[1]) if match[1] else None
|
|
151
|
+
if name is not None:
|
|
152
|
+
require(re.fullmatch(r'[a-z0-9][a-z0-9._-]{1,220}[a-z0-9]', name), 'INVALID_TERRAFORM_REQUEST', 'Bucket name 無效。')
|
|
153
|
+
target = '/storage/buckets' + ('/' + quote(name, safe='') if name else '')
|
|
154
|
+
if method == 'POST' and name is None:
|
|
155
|
+
require('project' in query, 'INVALID_TERRAFORM_REQUEST', 'GCS bucket create 需要 project。')
|
|
156
|
+
value = dict(body)
|
|
157
|
+
for key, expected in {'labels': {}, 'lifecycle': {'rule': []}, 'versioning': {'enabled': False},
|
|
158
|
+
'defaultEventBasedHold': False, 'billing': {'requesterPays': False}}.items():
|
|
159
|
+
default(value.pop(key, None), expected, 'GCS ' + key)
|
|
160
|
+
iam = only(value.pop('iamConfiguration', {}), ['uniformBucketLevelAccess', 'publicAccessPrevention'], 'GCS IAM')
|
|
161
|
+
default(iam.get('uniformBucketLevelAccess'), {'enabled': False}, 'GCS uniform bucket access')
|
|
162
|
+
default(iam.get('publicAccessPrevention'), 'inherited', 'GCS public access prevention')
|
|
163
|
+
only(value, ['name', 'location', 'storageClass'], 'GCS bucket')
|
|
164
|
+
result = bridge.request('gcp', 'POST', target, value)
|
|
165
|
+
elif name and method in {'GET', 'DELETE'}:
|
|
166
|
+
result = bridge.request('gcp', method, target)
|
|
167
|
+
if method == 'DELETE': return 204, b'', {}
|
|
168
|
+
else: raise FufuError('UNSUPPORTED_TERRAFORM_OPERATION', 'GCS bucket 的 labels/retention/IAM 更新尚未支援。')
|
|
169
|
+
# Stable resource identifiers; these URLs are identifiers, never a fallback
|
|
170
|
+
# destination for data traffic. Physical location comes from the backend.
|
|
171
|
+
result = {**result, 'kind': 'storage#bucket', 'id': result['name'],
|
|
172
|
+
'selfLink': 'https://storage.googleapis.com/storage/v1/b/' + quote(result['name'], safe='')}
|
|
173
|
+
return 200, result, {}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fufu-cloud-cli
|
|
3
|
-
Version: 0.10.
|
|
3
|
+
Version: 0.10.1
|
|
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,6 +27,10 @@ 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
|
+
0.10.1 新增官方 Terraform/OpenTofu/Pulumi 的 S3/GCS bucket 生命週期與
|
|
31
|
+
S3 bucket tags。需配套 FUFU API;不支援 bucket versioning、retention、
|
|
32
|
+
KMS、bucket IAM、GCS bucket labels 或 force destroy。
|
|
33
|
+
|
|
30
34
|
0.10.0 新增 Compute Engine/EC2 VM 與 GKE/EKS 介面的實際運算命令。
|
|
31
35
|
需要啟用相應 infrastructure backend 的 FUFU 服務端;VM 使用 KVM/KubeVirt,
|
|
32
36
|
叢集使用獨立 Kubernetes API 與共用節點,均不是官方雲端資源。
|
|
@@ -131,7 +135,7 @@ Python 預設識別而遇到 Cloudflare HTTP 403/1010。識別標頭不取代
|
|
|
131
135
|
## Cloudflare 前端部署與 R2
|
|
132
136
|
|
|
133
137
|
```bash
|
|
134
|
-
pip install 'fufu-cloud-cli==0.10.
|
|
138
|
+
pip install 'fufu-cloud-cli==0.10.1'
|
|
135
139
|
export FUFU_CLOUDFLARE_CREDENTIAL_FILE=/run/secrets/fufu-cloudflare.json
|
|
136
140
|
wrangler-fufu capabilities
|
|
137
141
|
wrangler-fufu pages project create frontend --production-branch main
|
|
@@ -55,6 +55,7 @@ src/fufu_cloud_cli/terraform_bridge/google.py
|
|
|
55
55
|
src/fufu_cloud_cli/terraform_bridge/google_pubsub.py
|
|
56
56
|
src/fufu_cloud_cli/terraform_bridge/remote_state.py
|
|
57
57
|
src/fufu_cloud_cli/terraform_bridge/runner.py
|
|
58
|
+
src/fufu_cloud_cli/terraform_bridge/storage.py
|
|
58
59
|
src/fufu_cloud_cli/vendor/AWS-CLI-LICENSE.txt
|
|
59
60
|
src/fufu_cloud_cli/vendor/__init__.py
|
|
60
61
|
src/fufu_cloud_cli/vendor/aws_text.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.10.0 → fufu_cloud_cli-0.10.1}/src/fufu_cloud_cli/runtime/functions/Dockerfile
RENAMED
|
File without changes
|
{fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/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.10.0 → fufu_cloud_cli-0.10.1}/src/fufu_cloud_cli/terraform_bridge/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/src/fufu_cloud_cli/terraform_bridge/aws_data.py
RENAMED
|
File without changes
|
{fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/src/fufu_cloud_cli/terraform_bridge/google_pubsub.py
RENAMED
|
File without changes
|
{fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/src/fufu_cloud_cli/terraform_bridge/remote_state.py
RENAMED
|
File without changes
|
{fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/src/fufu_cloud_cli/vendor/AWS-CLI-LICENSE.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/src/fufu_cloud_cli.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{fufu_cloud_cli-0.10.0 → fufu_cloud_cli-0.10.1}/src/fufu_cloud_cli.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|