feat: implement expiration policy logic for prebuilds (#17996)

## Summary 

This PR introduces support for expiration policies in prebuilds. The TTL
(time-to-live) is retrieved from the Terraform configuration
([terraform-provider-coder
PR](https://github.com/coder/terraform-provider-coder/pull/404)):
```
prebuilds = {
	  instances = 2
	  expiration_policy {
		  ttl = 86400
	  }
  }
```
**Note**: Since there is no need for precise TTL enforcement down to the
second, in this implementation expired prebuilds are handled in a single
reconciliation cycle: they are deleted, and new instances are created
only if needed to match the desired count.

## Changes

* The outcome of a reconciliation cycle is now expressed as a slice of
reconciliation actions, instead of a single aggregated action.
* Adjusted reconciliation logic to delete expired prebuilds and
guarantee that the number of desired instances is correct.
* Updated relevant data structures and methods to support expiration
policies parameters.
* Added documentation to `Prebuilt workspaces` page
* Update `terraform-provider-coder` to version 2.5.0:
https://github.com/coder/terraform-provider-coder/releases/tag/v2.5.0

Depends on: https://github.com/coder/terraform-provider-coder/pull/404
Fixes: https://github.com/coder/coder/issues/17916
This commit is contained in:
Susana Ferreira
2025-05-26 20:31:24 +01:00
committed by GitHub
parent 589f18627e
commit 6f6e73af03
18 changed files with 1503 additions and 994 deletions
+8 -1
View File
@@ -897,14 +897,21 @@ func ConvertState(ctx context.Context, modules []*tfjson.StateModule, rawGraph s
)
}
var prebuildInstances int32
var expirationPolicy *proto.ExpirationPolicy
if len(preset.Prebuilds) > 0 {
prebuildInstances = int32(math.Min(math.MaxInt32, float64(preset.Prebuilds[0].Instances)))
if len(preset.Prebuilds[0].ExpirationPolicy) > 0 {
expirationPolicy = &proto.ExpirationPolicy{
Ttl: int32(math.Min(math.MaxInt32, float64(preset.Prebuilds[0].ExpirationPolicy[0].TTL))),
}
}
}
protoPreset := &proto.Preset{
Name: preset.Name,
Parameters: presetParameters,
Prebuild: &proto.Prebuild{
Instances: prebuildInstances,
Instances: prebuildInstances,
ExpirationPolicy: expirationPolicy,
},
}
+4
View File
@@ -786,6 +786,7 @@ func TestConvertResources(t *testing.T) {
Name: "dev",
OperatingSystem: "windows",
Architecture: "arm64",
ApiKeyScope: "all",
Auth: &proto.Agent_Token{},
ConnectionTimeoutSeconds: 120,
DisplayApps: &displayApps,
@@ -830,6 +831,9 @@ func TestConvertResources(t *testing.T) {
}},
Prebuild: &proto.Prebuild{
Instances: 4,
ExpirationPolicy: &proto.ExpirationPolicy{
Ttl: 86400,
},
},
}},
},
@@ -25,6 +25,9 @@ data "coder_workspace_preset" "MyFirstProject" {
}
prebuilds {
instances = 4
expiration_policy {
ttl = 86400
}
}
}
+29 -1
View File
@@ -12,6 +12,7 @@
"provider_name": "registry.terraform.io/coder/coder",
"schema_version": 1,
"values": {
"api_key_scope": "all",
"arch": "arm64",
"auth": "token",
"connection_timeout": 120,
@@ -62,6 +63,7 @@
],
"before": null,
"after": {
"api_key_scope": "all",
"arch": "arm64",
"auth": "token",
"connection_timeout": 120,
@@ -134,6 +136,7 @@
"description": "blah blah",
"display_name": null,
"ephemeral": false,
"form_type": "input",
"icon": null,
"id": "57ccea62-8edf-41d1-a2c1-33f365e27567",
"mutable": false,
@@ -141,6 +144,7 @@
"option": null,
"optional": true,
"order": null,
"styling": "{}",
"type": "string",
"validation": [],
"value": "ok"
@@ -164,6 +168,11 @@
},
"prebuilds": [
{
"expiration_policy": [
{
"ttl": 86400
}
],
"instances": 4
}
]
@@ -171,7 +180,11 @@
"sensitive_values": {
"parameters": {},
"prebuilds": [
{}
{
"expiration_policy": [
{}
]
}
]
}
}
@@ -191,6 +204,7 @@
"description": "First parameter from module",
"display_name": null,
"ephemeral": false,
"form_type": "input",
"icon": null,
"id": "1774175f-0efd-4a79-8d40-dbbc559bf7c1",
"mutable": true,
@@ -198,6 +212,7 @@
"option": null,
"optional": true,
"order": null,
"styling": "{}",
"type": "string",
"validation": [],
"value": "abcdef"
@@ -218,6 +233,7 @@
"description": "Second parameter from module",
"display_name": null,
"ephemeral": false,
"form_type": "input",
"icon": null,
"id": "23d6841f-bb95-42bb-b7ea-5b254ce6c37d",
"mutable": true,
@@ -225,6 +241,7 @@
"option": null,
"optional": true,
"order": null,
"styling": "{}",
"type": "string",
"validation": [],
"value": "ghijkl"
@@ -250,6 +267,7 @@
"description": "First parameter from child module",
"display_name": null,
"ephemeral": false,
"form_type": "input",
"icon": null,
"id": "9d629df2-9846-47b2-ab1f-e7c882f35117",
"mutable": true,
@@ -257,6 +275,7 @@
"option": null,
"optional": true,
"order": null,
"styling": "{}",
"type": "string",
"validation": [],
"value": "abcdef"
@@ -277,6 +296,7 @@
"description": "Second parameter from child module",
"display_name": null,
"ephemeral": false,
"form_type": "input",
"icon": null,
"id": "52ca7b77-42a1-4887-a2f5-7a728feebdd5",
"mutable": true,
@@ -284,6 +304,7 @@
"option": null,
"optional": true,
"order": null,
"styling": "{}",
"type": "string",
"validation": [],
"value": "ghijkl"
@@ -388,6 +409,13 @@
},
"prebuilds": [
{
"expiration_policy": [
{
"ttl": {
"constant_value": 86400
}
}
],
"instances": {
"constant_value": 4
}
+21 -1
View File
@@ -16,6 +16,7 @@
"description": "blah blah",
"display_name": null,
"ephemeral": false,
"form_type": "input",
"icon": null,
"id": "491d202d-5658-40d9-9adc-fd3a67f6042b",
"mutable": false,
@@ -23,6 +24,7 @@
"option": null,
"optional": true,
"order": null,
"styling": "{}",
"type": "string",
"validation": [],
"value": "ok"
@@ -46,6 +48,11 @@
},
"prebuilds": [
{
"expiration_policy": [
{
"ttl": 86400
}
],
"instances": 4
}
]
@@ -53,7 +60,11 @@
"sensitive_values": {
"parameters": {},
"prebuilds": [
{}
{
"expiration_policy": [
{}
]
}
]
}
},
@@ -65,6 +76,7 @@
"provider_name": "registry.terraform.io/coder/coder",
"schema_version": 1,
"values": {
"api_key_scope": "all",
"arch": "arm64",
"auth": "token",
"connection_timeout": 120,
@@ -133,6 +145,7 @@
"description": "First parameter from module",
"display_name": null,
"ephemeral": false,
"form_type": "input",
"icon": null,
"id": "0a4d1299-b174-43b0-91ad-50c1ca9a4c25",
"mutable": true,
@@ -140,6 +153,7 @@
"option": null,
"optional": true,
"order": null,
"styling": "{}",
"type": "string",
"validation": [],
"value": "abcdef"
@@ -160,6 +174,7 @@
"description": "Second parameter from module",
"display_name": null,
"ephemeral": false,
"form_type": "input",
"icon": null,
"id": "f0812474-29fd-4c3c-ab40-9e66e36d4017",
"mutable": true,
@@ -167,6 +182,7 @@
"option": null,
"optional": true,
"order": null,
"styling": "{}",
"type": "string",
"validation": [],
"value": "ghijkl"
@@ -192,6 +208,7 @@
"description": "First parameter from child module",
"display_name": null,
"ephemeral": false,
"form_type": "input",
"icon": null,
"id": "27b5fae3-7671-4e61-bdfe-c940627a21b8",
"mutable": true,
@@ -199,6 +216,7 @@
"option": null,
"optional": true,
"order": null,
"styling": "{}",
"type": "string",
"validation": [],
"value": "abcdef"
@@ -219,6 +237,7 @@
"description": "Second parameter from child module",
"display_name": null,
"ephemeral": false,
"form_type": "input",
"icon": null,
"id": "d285bb17-27ff-4a49-a12b-28582264b4d9",
"mutable": true,
@@ -226,6 +245,7 @@
"option": null,
"optional": true,
"order": null,
"styling": "{}",
"type": "string",
"validation": [],
"value": "ghijkl"