env0_project_policy: infinite TTL (Infinite) is silently dropped, falls back to org policy
#1,110 opened on Jul 7, 2026
Repository metrics
- Stars
- (39 stars)
- PR merge metrics
- (PR metrics pending)
Description
Describe the bug
Setting default_ttl = "Infinite" and max_ttl = "Infinite" on env0_project_policy does not produce an infinite TTL. Environments silently inherit the organization TTL policy instead.
The provider converts "Infinite" to an empty string in the update payload:
// env0/resource_project_policy.go (Update)
if payload.DefaultTtl == INFINITE { payload.DefaultTtl = "" }
if payload.MaxTtl == INFINITE { payload.MaxTtl = "" }
But both fields are tagged omitempty, so the empty string is dropped from the request body entirely (it is not serialized as JSON null):
// client/project_policy.go
type PolicyUpdatePayload struct {
...
MaxTtl string `json:"maxTtl,omitempty"`
DefaultTtl string `json:"defaultTtl,omitempty"`
...
}
The PUT /policies API documents that infinite requires an explicit null ("Must be 6-h, 12-h, 1-d, 3-d, 1-w, 2-w, 1-M, inherit, or explicit null which means infinite"). Since omitempty yields an absent field rather than null, infinite is never set. This is most visible when both TTLs are Infinite, because the PUT then carries no TTL fields at all and the API applies the inherited org policy.
To Reproduce
- Have an organization TTL policy set (e.g.
max_ttl = "4-w"). - Apply the following:
resource "env0_project_policy" "example" {
project_id = data.env0_project.example.id
default_ttl = "Infinite"
max_ttl = "Infinite"
}
- Create an environment in that project - it gets a 4-week TTL and is auto-destroyed, instead of never being destroyed.
Expected behavior
Infinite should result in maxTtl: null / defaultTtl: null in the request body, so the API applies an infinite (never-destroy) TTL.
Provider Version
1.31.4 (also reproduces on 1.30.x)
Screenshots
N/A
Additional context
The unit test in resource_project_policy_test.go only covers max_ttl = "Infinite" alongside a concrete default_ttl = "7-h", and asserts the Go payload MaxTtl: "" at the mock boundary - it never verifies the serialized JSON nor the both-Infinite case, so the Infinite → "" → omitted round-trip passes the mock but breaks against the real API.
Suggested fix: make MaxTtl/DefaultTtl in PolicyUpdatePayload pointers (*string) and set them to nil for Infinite so they serialize as explicit null (or drop omitempty and send null). Add coverage for both fields set to Infinite.
Workaround: map Infinite to a large finite TTL (e.g. 120-M) in the caller's Terraform.