diff --git a/docs/resources/script_order.md b/docs/resources/script_order.md new file mode 100644 index 00000000..cf11e850 --- /dev/null +++ b/docs/resources/script_order.md @@ -0,0 +1,66 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "coder_script_order Resource - terraform-provider-coder" +subcategory: "" +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. +--- + +# coder_script_order (Resource) + +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`. + +## Example Usage + +```terraform +resource "coder_agent" "dev" { + os = "linux" + arch = "amd64" +} + +resource "coder_script" "clone" { + agent_id = coder_agent.dev.id + display_name = "clone" + script = "git clone ..." + run_on_start = true +} + +resource "coder_script" "install" { + agent_id = coder_agent.dev.id + display_name = "install" + script = "make install" + run_on_start = true +} + +# install waits until clone completes. +resource "coder_script_order" "startup" { + rule { + run = [coder_script.install.id] + after = [coder_script.clone.id] + state = "completes" + } +} +``` + + +## Schema + +### Optional + +- `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)) + +### Read-Only + +- `id` (String) The ID of this resource. + + +### Nested Schema for `rule` + +Required: + +- `run` (List of String) The ids of the `coder_script` resources this rule applies to (e.g. `coder_script.install.id`). + +Optional: + +- `after` (List of String) The ids of the `coder_script` resources that must reach `state` before the `run` scripts start. +- `state` (String) The lifecycle state the `after` scripts must reach before the `run` scripts may start. One of `completes` or `starts`. diff --git a/examples/resources/coder_script_order/resource.tf b/examples/resources/coder_script_order/resource.tf new file mode 100644 index 00000000..dc770061 --- /dev/null +++ b/examples/resources/coder_script_order/resource.tf @@ -0,0 +1,27 @@ +resource "coder_agent" "dev" { + os = "linux" + arch = "amd64" +} + +resource "coder_script" "clone" { + agent_id = coder_agent.dev.id + display_name = "clone" + script = "git clone ..." + run_on_start = true +} + +resource "coder_script" "install" { + agent_id = coder_agent.dev.id + display_name = "install" + script = "make install" + run_on_start = true +} + +# install waits until clone completes. +resource "coder_script_order" "startup" { + rule { + run = [coder_script.install.id] + after = [coder_script.clone.id] + state = "completes" + } +} diff --git a/provider/provider.go b/provider/provider.go index 7e4451b8..5c6ce671 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -73,6 +73,7 @@ func New() *schema.Provider { "coder_app": appResource(), "coder_metadata": metadataResource(), "coder_script": scriptResource(), + "coder_script_order": scriptOrderResource(), "coder_env": envResource(), "coder_devcontainer": devcontainerResource(), "coder_external_agent": externalAgentResource(), diff --git a/provider/script_order.go b/provider/script_order.go new file mode 100644 index 00000000..3e937cdb --- /dev/null +++ b/provider/script_order.go @@ -0,0 +1,64 @@ +package provider + +import ( + "context" + + "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +func scriptOrderResource() *schema.Resource { + return &schema.Resource{ + SchemaVersion: 1, + + 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`.", + CreateContext: func(_ context.Context, rd *schema.ResourceData, _ interface{}) diag.Diagnostics { + rd.SetId(uuid.NewString()) + // Validate that every rule declares at least one script to run. + rules, _ := rd.Get("rule").([]interface{}) + for i, raw := range rules { + m, _ := raw.(map[string]interface{}) + run, _ := m["run"].([]interface{}) + if len(run) == 0 { + return diag.Errorf("rule[%d]: \"run\" must contain at least one coder_script id", i) + } + } + return nil + }, + ReadContext: schema.NoopContext, + DeleteContext: schema.NoopContext, + Schema: map[string]*schema.Schema{ + "rule": { + Type: schema.TypeList, + Description: "Each `rule` block declares that the scripts in `run` must wait for the scripts in `after` to reach `state`.", + ForceNew: true, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "run": { + Type: schema.TypeList, + Description: "The ids of the `coder_script` resources this rule applies to (e.g. `coder_script.install.id`).", + Required: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "after": { + Type: schema.TypeList, + Description: "The ids of the `coder_script` resources that must reach `state` before the `run` scripts start.", + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "state": { + Type: schema.TypeString, + Description: "The lifecycle state the `after` scripts must reach before the `run` scripts may start. One of `completes` or `starts`.", + Optional: true, + Default: "completes", + ValidateFunc: validation.StringInSlice([]string{"completes", "starts"}, false), + }, + }, + }, + }, + }, + } +} diff --git a/provider/script_order_test.go b/provider/script_order_test.go new file mode 100644 index 00000000..a5526931 --- /dev/null +++ b/provider/script_order_test.go @@ -0,0 +1,69 @@ +package provider_test + +import ( + "regexp" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestScriptOrder(t *testing.T) { + t.Parallel() + + resource.Test(t, resource.TestCase{ + ProviderFactories: coderFactory(), + IsUnitTest: true, + Steps: []resource.TestStep{{ + Config: ` + provider "coder" { + } + resource "coder_script_order" "startup" { + rule { + run = ["coder_script.install.id"] + after = ["coder_script.clone.id"] + state = "completes" + } + } + `, + Check: func(state *terraform.State) error { + require.Len(t, state.Modules, 1) + require.Len(t, state.Modules[0].Resources, 1) + order := state.Modules[0].Resources["coder_script_order.startup"] + require.NotNil(t, order) + t.Logf("script_order attributes: %#v", order.Primary.Attributes) + for key, expected := range map[string]string{ + "rule.#": "1", + "rule.0.run.0": "coder_script.install.id", + "rule.0.state": "completes", + } { + require.Equal(t, expected, order.Primary.Attributes[key]) + } + return nil + }, + }}, + }) +} + +func TestScriptOrderEmptyRun(t *testing.T) { + t.Parallel() + + resource.Test(t, resource.TestCase{ + ProviderFactories: coderFactory(), + IsUnitTest: true, + Steps: []resource.TestStep{{ + Config: ` + provider "coder" { + } + resource "coder_script_order" "startup" { + rule { + run = [] + } + } + `, + ExpectError: regexp.MustCompile(`"run" must contain at least one coder_script id`), + }}, + }) +}