Skip to content

Commit def22a1

Browse files
committed
feat: add coder_script_order resource for declarative script ordering
Add a config-only managed resource that lets templates declare ordering constraints between coder_script resources on an agent. Each rule block declares that the scripts in run must wait for the scripts in after to reach a given lifecycle state (completes or starts). Multiple resources are additive. Follows the coder_script/coder_env pattern: CreateContext sets a uuid id and validates each rule, with Noop read/delete. Includes docs, an example, and unit tests. Generated by Coder Agents on behalf of @SasSwart.
1 parent e533b99 commit def22a1

5 files changed

Lines changed: 227 additions & 0 deletions

File tree

docs/resources/script_order.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
---
2+
# generated by https://github.com/hashicorp/terraform-plugin-docs
3+
page_title: "coder_script_order Resource - terraform-provider-coder"
4+
subcategory: ""
5+
description: |-
6+
Use this resource to declare ordering constraints between coder_script resources on an agent. Multiple coder_script_order resources are additive; each rule block adds edges to the startup dependency graph. Reference scripts by their id, e.g. coder_script.install.id.
7+
---
8+
9+
# coder_script_order (Resource)
10+
11+
Use this resource to declare ordering constraints between `coder_script` resources on an agent. Multiple `coder_script_order` resources are additive; each `rule` block adds edges to the startup dependency graph. Reference scripts by their id, e.g. `coder_script.install.id`.
12+
13+
## Example Usage
14+
15+
```terraform
16+
resource "coder_agent" "dev" {
17+
os = "linux"
18+
arch = "amd64"
19+
}
20+
21+
resource "coder_script" "clone" {
22+
agent_id = coder_agent.dev.id
23+
display_name = "clone"
24+
script = "git clone ..."
25+
run_on_start = true
26+
}
27+
28+
resource "coder_script" "install" {
29+
agent_id = coder_agent.dev.id
30+
display_name = "install"
31+
script = "make install"
32+
run_on_start = true
33+
}
34+
35+
# install waits until clone completes.
36+
resource "coder_script_order" "startup" {
37+
rule {
38+
run = [coder_script.install.id]
39+
after = [coder_script.clone.id]
40+
state = "completes"
41+
}
42+
}
43+
```
44+
45+
<!-- schema generated by tfplugindocs -->
46+
## Schema
47+
48+
### Optional
49+
50+
- `rule` (Block List) Each `rule` block declares that the scripts in `run` must wait for the scripts in `after` to reach `state`. (see [below for nested schema](#nestedblock--rule))
51+
52+
### Read-Only
53+
54+
- `id` (String) The ID of this resource.
55+
56+
<a id="nestedblock--rule"></a>
57+
### Nested Schema for `rule`
58+
59+
Required:
60+
61+
- `run` (List of String) The ids of the `coder_script` resources this rule applies to (e.g. `coder_script.install.id`).
62+
63+
Optional:
64+
65+
- `after` (List of String) The ids of the `coder_script` resources that must reach `state` before the `run` scripts start.
66+
- `state` (String) The lifecycle state the `after` scripts must reach before the `run` scripts may start. One of `completes` or `starts`.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
resource "coder_agent" "dev" {
2+
os = "linux"
3+
arch = "amd64"
4+
}
5+
6+
resource "coder_script" "clone" {
7+
agent_id = coder_agent.dev.id
8+
display_name = "clone"
9+
script = "git clone ..."
10+
run_on_start = true
11+
}
12+
13+
resource "coder_script" "install" {
14+
agent_id = coder_agent.dev.id
15+
display_name = "install"
16+
script = "make install"
17+
run_on_start = true
18+
}
19+
20+
# install waits until clone completes.
21+
resource "coder_script_order" "startup" {
22+
rule {
23+
run = [coder_script.install.id]
24+
after = [coder_script.clone.id]
25+
state = "completes"
26+
}
27+
}

provider/provider.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ func New() *schema.Provider {
7373
"coder_app": appResource(),
7474
"coder_metadata": metadataResource(),
7575
"coder_script": scriptResource(),
76+
"coder_script_order": scriptOrderResource(),
7677
"coder_env": envResource(),
7778
"coder_devcontainer": devcontainerResource(),
7879
"coder_external_agent": externalAgentResource(),

provider/script_order.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package provider
2+
3+
import (
4+
"context"
5+
6+
"github.com/google/uuid"
7+
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
8+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
9+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
10+
)
11+
12+
func scriptOrderResource() *schema.Resource {
13+
return &schema.Resource{
14+
SchemaVersion: 1,
15+
16+
Description: "Use this resource to declare ordering constraints between `coder_script` resources on an agent. Multiple `coder_script_order` resources are additive; each `rule` block adds edges to the startup dependency graph. Reference scripts by their id, e.g. `coder_script.install.id`.",
17+
CreateContext: func(_ context.Context, rd *schema.ResourceData, _ interface{}) diag.Diagnostics {
18+
rd.SetId(uuid.NewString())
19+
// Validate that every rule declares at least one script to run.
20+
rules, _ := rd.Get("rule").([]interface{})
21+
for i, raw := range rules {
22+
m, _ := raw.(map[string]interface{})
23+
run, _ := m["run"].([]interface{})
24+
if len(run) == 0 {
25+
return diag.Errorf("rule[%d]: \"run\" must contain at least one coder_script id", i)
26+
}
27+
}
28+
return nil
29+
},
30+
ReadContext: schema.NoopContext,
31+
DeleteContext: schema.NoopContext,
32+
Schema: map[string]*schema.Schema{
33+
"rule": {
34+
Type: schema.TypeList,
35+
Description: "Each `rule` block declares that the scripts in `run` must wait for the scripts in `after` to reach `state`.",
36+
ForceNew: true,
37+
Optional: true,
38+
Elem: &schema.Resource{
39+
Schema: map[string]*schema.Schema{
40+
"run": {
41+
Type: schema.TypeList,
42+
Description: "The ids of the `coder_script` resources this rule applies to (e.g. `coder_script.install.id`).",
43+
Required: true,
44+
Elem: &schema.Schema{Type: schema.TypeString},
45+
},
46+
"after": {
47+
Type: schema.TypeList,
48+
Description: "The ids of the `coder_script` resources that must reach `state` before the `run` scripts start.",
49+
Optional: true,
50+
Elem: &schema.Schema{Type: schema.TypeString},
51+
},
52+
"state": {
53+
Type: schema.TypeString,
54+
Description: "The lifecycle state the `after` scripts must reach before the `run` scripts may start. One of `completes` or `starts`.",
55+
Optional: true,
56+
Default: "completes",
57+
ValidateFunc: validation.StringInSlice([]string{"completes", "starts"}, false),
58+
},
59+
},
60+
},
61+
},
62+
},
63+
}
64+
}

provider/script_order_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package provider_test
2+
3+
import (
4+
"regexp"
5+
"testing"
6+
7+
"github.com/stretchr/testify/require"
8+
9+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
10+
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
11+
)
12+
13+
func TestScriptOrder(t *testing.T) {
14+
t.Parallel()
15+
16+
resource.Test(t, resource.TestCase{
17+
ProviderFactories: coderFactory(),
18+
IsUnitTest: true,
19+
Steps: []resource.TestStep{{
20+
Config: `
21+
provider "coder" {
22+
}
23+
resource "coder_script_order" "startup" {
24+
rule {
25+
run = ["coder_script.install.id"]
26+
after = ["coder_script.clone.id"]
27+
state = "completes"
28+
}
29+
}
30+
`,
31+
Check: func(state *terraform.State) error {
32+
require.Len(t, state.Modules, 1)
33+
require.Len(t, state.Modules[0].Resources, 1)
34+
order := state.Modules[0].Resources["coder_script_order.startup"]
35+
require.NotNil(t, order)
36+
t.Logf("script_order attributes: %#v", order.Primary.Attributes)
37+
for key, expected := range map[string]string{
38+
"rule.#": "1",
39+
"rule.0.run.0": "coder_script.install.id",
40+
"rule.0.state": "completes",
41+
} {
42+
require.Equal(t, expected, order.Primary.Attributes[key])
43+
}
44+
return nil
45+
},
46+
}},
47+
})
48+
}
49+
50+
func TestScriptOrderEmptyRun(t *testing.T) {
51+
t.Parallel()
52+
53+
resource.Test(t, resource.TestCase{
54+
ProviderFactories: coderFactory(),
55+
IsUnitTest: true,
56+
Steps: []resource.TestStep{{
57+
Config: `
58+
provider "coder" {
59+
}
60+
resource "coder_script_order" "startup" {
61+
rule {
62+
run = []
63+
}
64+
}
65+
`,
66+
ExpectError: regexp.MustCompile(`"run" must contain at least one coder_script id`),
67+
}},
68+
})
69+
}

0 commit comments

Comments
 (0)