Skip to content

Commit 52585a4

Browse files
authored
Add stateful app preflight resource
Add a stateful phala_app_preflight resource and matching data source support so preflight compose hashes can be stored in Terraform state without destroy-time refresh side effects.
1 parent 40716e8 commit 52585a4

8 files changed

Lines changed: 942 additions & 2 deletions

File tree

docs/data-sources/app_preflight.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
---
2+
# generated by https://github.com/hashicorp/terraform-plugin-docs
3+
page_title: "phala_app_preflight Data Source - phala"
4+
subcategory: ""
5+
description: |-
6+
Runs Phala Cloud app provision/preflight and returns the compose hash without committing a CVM deployment.
7+
---
8+
9+
# phala_app_preflight (Data Source)
10+
11+
Runs Phala Cloud app provision/preflight and returns the compose hash without committing a CVM deployment.
12+
13+
14+
15+
<!-- schema generated by tfplugindocs -->
16+
## Schema
17+
18+
### Required
19+
20+
- `docker_compose` (String) Docker Compose YAML content.
21+
- `name` (String) App/CVM name included in the app compose.
22+
- `size` (String) Instance type (e.g. tdx.small).
23+
24+
### Optional
25+
26+
- `custom_app_id` (String) Optional custom app_id for deterministic identity flow.
27+
- `disk_size` (Number) Disk size in GB.
28+
- `env` (Map of String, Sensitive) Plaintext env vars. Only keys enter the app compose allowed_envs list.
29+
- `env_keys` (List of String) Allowed environment variable keys used when env values are not provided.
30+
- `gateway_enabled` (Boolean) Enable public gateway routing.
31+
- `image` (String) OS image name.
32+
- `kms` (String) KMS type for app provisioning. Defaults to phala when omitted.
33+
- `listed` (Boolean) Whether the resource should be publicly listed. Defaults to false when omitted.
34+
- `node_id` (Number) Optional target node (teepod) ID for placement.
35+
- `nonce` (Number) Optional nonce paired with custom_app_id for PHALA KMS deterministic app_id flow.
36+
- `pre_launch_script` (String) Optional pre-launch script content.
37+
- `public_logs` (Boolean) Expose container logs publicly.
38+
- `public_sysinfo` (Boolean) Expose system info publicly.
39+
- `public_tcbinfo` (Boolean) Expose TCB attestation info publicly.
40+
- `region` (String) Preferred region identifier.
41+
- `secure_time` (Boolean) Enable secure time mode.
42+
- `ssh_authorized_keys` (List of String) Per-deployment SSH public keys injected at launch via user_config.
43+
- `storage_fs` (String) Storage filesystem for deployment (`zfs` or `ext4`).
44+
45+
### Read-Only
46+
47+
- `app_env_encrypt_pubkey` (String) Public key used for app environment encryption.
48+
- `app_id` (String) Preflight app identifier returned by Phala Cloud.
49+
- `compose_hash` (String) SHA-256 hash of the normalized app compose file returned by Phala Cloud preflight.
50+
- `device_id` (String)
51+
- `fmspc` (String)
52+
- `id` (String) Stable data source ID (same as compose_hash).
53+
- `instance_type` (String)
54+
- `kms_id` (String)
55+
- `kms_info_json` (String) Raw KMS info object as JSON.
56+
- `matched_node_id` (Number) Matched teepod/node ID returned by preflight, when present.
57+
- `os_image_hash` (String)
58+
- `raw_json` (String) Full provision response as JSON.

docs/resources/app_preflight.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
---
2+
# generated by https://github.com/hashicorp/terraform-plugin-docs
3+
page_title: "phala_app_preflight Resource - phala"
4+
subcategory: ""
5+
description: |-
6+
Runs Phala Cloud app provision/preflight and stores the resulting compose hash as Terraform state. No remote object is created.
7+
---
8+
9+
# phala_app_preflight (Resource)
10+
11+
Runs Phala Cloud app provision/preflight and stores the resulting compose hash as Terraform state. No remote object is created.
12+
13+
14+
15+
<!-- schema generated by tfplugindocs -->
16+
## Schema
17+
18+
### Required
19+
20+
- `docker_compose` (String) Docker Compose YAML content.
21+
- `name` (String) App/CVM name included in the app compose.
22+
- `size` (String) Instance type (e.g. tdx.small).
23+
24+
### Optional
25+
26+
- `custom_app_id` (String) Optional custom app_id for deterministic identity flow.
27+
- `disk_size` (Number) Disk size in GB.
28+
- `env` (Map of String, Sensitive) Plaintext env vars. Only keys enter the app compose allowed_envs list.
29+
- `env_keys` (List of String) Allowed environment variable keys used when env values are not provided.
30+
- `gateway_enabled` (Boolean) Enable public gateway routing.
31+
- `image` (String) OS image name.
32+
- `kms` (String) KMS type for app provisioning. Defaults to phala when omitted.
33+
- `listed` (Boolean) Whether the resource should be publicly listed. Defaults to false when omitted.
34+
- `node_id` (Number) Optional target node (teepod) ID for placement.
35+
- `nonce` (Number) Optional nonce paired with custom_app_id for PHALA KMS deterministic app_id flow.
36+
- `pre_launch_script` (String) Optional pre-launch script content.
37+
- `public_logs` (Boolean) Expose container logs publicly.
38+
- `public_sysinfo` (Boolean) Expose system info publicly.
39+
- `public_tcbinfo` (Boolean) Expose TCB attestation info publicly.
40+
- `region` (String) Preferred region identifier.
41+
- `secure_time` (Boolean) Enable secure time mode.
42+
- `ssh_authorized_keys` (List of String) Per-deployment SSH public keys injected at launch via user_config.
43+
- `storage_fs` (String) Storage filesystem for deployment (`zfs` or `ext4`).
44+
45+
### Read-Only
46+
47+
- `app_env_encrypt_pubkey` (String) Public key used for app environment encryption.
48+
- `app_id` (String) Preflight app identifier returned by Phala Cloud.
49+
- `compose_hash` (String) SHA-256 hash of the normalized app compose file returned by Phala Cloud preflight.
50+
- `device_id` (String)
51+
- `fmspc` (String)
52+
- `id` (String) Stable resource ID (same as compose_hash).
53+
- `instance_type` (String)
54+
- `kms_id` (String)
55+
- `kms_info_json` (String) Raw KMS info object as JSON.
56+
- `matched_node_id` (Number) Matched teepod/node ID returned by preflight, when present.
57+
- `os_image_hash` (String)
58+
- `raw_json` (String) Full provision response as JSON.

internal/provider/client.go

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,7 @@ func (c *APIClient) requestWithRetry(
131131

132132
apiErr, ok := err.(*APIError)
133133
if !ok ||
134-
!isRetryableStatus(apiErr.StatusCode) ||
135-
!shouldRetryWrite(method, path) ||
134+
!shouldRetryAPIError(method, path, apiErr) ||
136135
attempt == maxWriteRetries {
137136
return err
138137
}
@@ -177,6 +176,32 @@ func shouldRetryWrite(method, path string) bool {
177176
}
178177
}
179178

179+
func shouldRetryAPIError(method, path string, apiErr *APIError) bool {
180+
if apiErr == nil || !shouldRetryWrite(method, path) {
181+
return false
182+
}
183+
if isRetryableStatus(apiErr.StatusCode) {
184+
return true
185+
}
186+
return isRetryableProvisionCompatibilityError(method, path, apiErr)
187+
}
188+
189+
func isRetryableProvisionCompatibilityError(method, path string, apiErr *APIError) bool {
190+
if method != http.MethodPost || apiErr == nil || apiErr.StatusCode != http.StatusBadRequest {
191+
return false
192+
}
193+
194+
normalizedPath := normalizePath(path)
195+
if normalizedPath != "/cvms/provision" {
196+
if _, ok := extractCVMID(normalizedPath, "/compose_file/provision"); !ok {
197+
return false
198+
}
199+
}
200+
201+
message := strings.ToLower(apiErr.Message + " " + apiErr.Body)
202+
return strings.Contains(message, "configuration parameters are not compatible")
203+
}
204+
180205
func isRetryableStatus(status int) bool {
181206
switch status {
182207
case http.StatusConflict, http.StatusTooManyRequests, http.StatusServiceUnavailable:

internal/provider/client_contract_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,3 +480,33 @@ func TestAPIClientContract_RetriesOnlyReplaySafeWrites(t *testing.T) {
480480
t.Fatalf("unexpected retry count for PATCH: got %d want 2", got)
481481
}
482482
}
483+
484+
func TestShouldRetryAPIError_ProvisionCompatibilityBadRequest(t *testing.T) {
485+
err := &APIError{
486+
StatusCode: http.StatusBadRequest,
487+
Status: "400 Bad Request",
488+
Message: "The configuration parameters are not compatible with each other",
489+
}
490+
491+
if !shouldRetryAPIError(http.MethodPost, "/cvms/provision", err) {
492+
t.Fatal("expected transient provision compatibility 400 to be retryable")
493+
}
494+
if !shouldRetryAPIError(http.MethodPost, "/cvms/cvm123/compose_file/provision", err) {
495+
t.Fatal("expected transient compose-file provision compatibility 400 to be retryable")
496+
}
497+
}
498+
499+
func TestShouldRetryAPIError_DoesNotRetryGenericBadRequest(t *testing.T) {
500+
err := &APIError{
501+
StatusCode: http.StatusBadRequest,
502+
Status: "400 Bad Request",
503+
Message: "name is required",
504+
}
505+
506+
if shouldRetryAPIError(http.MethodPost, "/cvms/provision", err) {
507+
t.Fatal("expected generic provision 400 not to be retryable")
508+
}
509+
if shouldRetryAPIError(http.MethodPost, "/user/ssh-keys", err) {
510+
t.Fatal("expected non-replay-safe create POST not to be retryable")
511+
}
512+
}

0 commit comments

Comments
 (0)