From bfa0c5acfe1437007a4f3b52adbe55cef6d28eb8 Mon Sep 17 00:00:00 2001 From: Ammar Bandukwala Date: Fri, 31 Mar 2023 13:20:54 -0500 Subject: [PATCH 1/4] Add metadata block to the agent (#112) --- docs/resources/agent.md | 15 +++++++++++++ provider/agent.go | 43 ++++++++++++++++++++++++++++++++++++ provider/agent_test.go | 48 ++++++++++++++++++++++++++++++++++++++++- 3 files changed, 105 insertions(+), 1 deletion(-) diff --git a/docs/resources/agent.md b/docs/resources/agent.md index 1333d5bb..9ee5c1d0 100644 --- a/docs/resources/agent.md +++ b/docs/resources/agent.md @@ -51,6 +51,7 @@ resource "kubernetes_pod" "dev" { - `dir` (String) The starting directory when a user creates a shell session. Defaults to $HOME. - `env` (Map of String) A mapping of environment variables to set inside the workspace. - `login_before_ready` (Boolean) This option defines whether or not the user can (by default) login to the workspace before it is ready. Ready means that e.g. the startup_script is done and has exited. When enabled, users may see an incomplete workspace when logging in. +- `metadata` (Block List) Each "metadata" block defines a single item consisting of a key/value pair. This feature is in alpha and may break in future releases. (see [below for nested schema](#nestedblock--metadata)) - `motd_file` (String) The path to a file within the workspace containing a message to display to users when they login via SSH. A typical value would be /etc/motd. - `shutdown_script` (String) A script to run before the agent is stopped. The script should exit when it is done to signal that the workspace can be stopped. - `shutdown_script_timeout` (Number) Time in seconds until the agent lifecycle status is marked as timed out during shutdown, this happens when the shutdown script has not completed (exited) in the given time. @@ -64,4 +65,18 @@ resource "kubernetes_pod" "dev" { - `init_script` (String) Run this script on startup of an instance to initialize the agent. - `token` (String, Sensitive) Set the environment variable "CODER_AGENT_TOKEN" with this token to authenticate an agent. + +### Nested Schema for `metadata` + +Required: + +- `interval` (Number) The interval in seconds at which to refresh this metadata item. +- `key` (String) The key of this metadata item. +- `script` (String) The script that retrieves the value of this metadata item. + +Optional: + +- `display_name` (String) The user-facing name of this value. +- `timeout` (Number) The maximum time the command is allowed to run in seconds. + diff --git a/provider/agent.go b/provider/agent.go index 4d628927..c5797081 100644 --- a/provider/agent.go +++ b/provider/agent.go @@ -137,6 +137,49 @@ func agentResource() *schema.Resource { Optional: true, Description: "This option defines whether or not the user can (by default) login to the workspace before it is ready. Ready means that e.g. the startup_script is done and has exited. When enabled, users may see an incomplete workspace when logging in.", }, + "metadata": { + Type: schema.TypeList, + Description: "Each \"metadata\" block defines a single item consisting of a key/value pair. This feature is in alpha and may break in future releases.", + ForceNew: true, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "key": { + Type: schema.TypeString, + Description: "The key of this metadata item.", + ForceNew: true, + Required: true, + }, + "display_name": { + Type: schema.TypeString, + Description: "The user-facing name of this value.", + ForceNew: true, + Optional: true, + }, + "script": { + Type: schema.TypeString, + Description: "The script that retrieves the value of this metadata item.", + ForceNew: true, + Required: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "timeout": { + Type: schema.TypeInt, + Description: "The maximum time the command is allowed to run in seconds.", + ForceNew: true, + Optional: true, + }, + "interval": { + Type: schema.TypeInt, + Description: "The interval in seconds at which to refresh this metadata item. ", + ForceNew: true, + Required: true, + }, + }, + }, + }, }, } } diff --git a/provider/agent_test.go b/provider/agent_test.go index 445ed75b..2069247b 100644 --- a/provider/agent_test.go +++ b/provider/agent_test.go @@ -71,7 +71,7 @@ func TestAgent(t *testing.T) { }) } -func TestAgentInstance(t *testing.T) { +func TestAgent_Instance(t *testing.T) { t.Parallel() resource.Test(t, resource.TestCase{ Providers: map[string]*schema.Provider{ @@ -111,3 +111,49 @@ func TestAgentInstance(t *testing.T) { }}, }) } + +func TestAgent_Metadata(t *testing.T) { + t.Parallel() + resource.Test(t, resource.TestCase{ + Providers: map[string]*schema.Provider{ + "coder": provider.New(), + }, + IsUnitTest: true, + Steps: []resource.TestStep{{ + Config: ` + provider "coder" { + url = "https://example.com" + } + resource "coder_agent" "dev" { + os = "linux" + arch = "amd64" + metadata { + key = "process_count" + display_name = "Process Count" + script = "ps aux | wc -l" + interval = 5 + timeout = 1 + } + } + `, + Check: func(state *terraform.State) error { + require.Len(t, state.Modules, 1) + require.Len(t, state.Modules[0].Resources, 1) + + resource := state.Modules[0].Resources["coder_agent.dev"] + require.NotNil(t, resource) + + t.Logf("resource: %v", resource.Primary.Attributes) + + attr := resource.Primary.Attributes + require.Equal(t, "1", attr["metadata.#"]) + require.Equal(t, "process_count", attr["metadata.0.key"]) + require.Equal(t, "Process Count", attr["metadata.0.display_name"]) + require.Equal(t, "ps aux | wc -l", attr["metadata.0.script"]) + require.Equal(t, "5", attr["metadata.0.interval"]) + require.Equal(t, "1", attr["metadata.0.timeout"]) + return nil + }, + }}, + }) +} From dbc530bacc1127297b2b4004232d9f635fe38746 Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Wed, 17 May 2023 23:30:38 -0500 Subject: [PATCH 2/4] feat: add session token data source (#120) --- docs/data-sources/workspace.md | 1 + provider/workspace.go | 8 ++++++++ provider/workspace_test.go | 5 ++++- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/data-sources/workspace.md b/docs/data-sources/workspace.md index 865a2d98..ffe49eb8 100644 --- a/docs/data-sources/workspace.md +++ b/docs/data-sources/workspace.md @@ -34,6 +34,7 @@ resource "kubernetes_pod" "dev" { - `owner_email` (String) Email address of the workspace owner. - `owner_id` (String) UUID of the workspace owner. - `owner_oidc_access_token` (String) A valid OpenID Connect access token of the workspace owner. This is only available if the workspace owner authenticated with OpenID Connect. If a valid token cannot be obtained, this value will be an empty string. +- `owner_session_token` (String) Session token for interfacing with a Coder deployment. It is regenerated everytime a workspace is started. - `start_count` (Number) A computed count based on "transition" state. If "start", count will equal 1. - `transition` (String) Either "start" or "stop". Use this to start/stop resources with "count". diff --git a/provider/workspace.go b/provider/workspace.go index 797acc60..47f0e799 100644 --- a/provider/workspace.go +++ b/provider/workspace.go @@ -51,6 +51,9 @@ func workspaceDataSource() *schema.Resource { } rd.Set("name", name) + sessionToken := os.Getenv("CODER_WORKSPACE_OWNER_SESSION_TOKEN") + _ = rd.Set("owner_session_token", sessionToken) + id := os.Getenv("CODER_WORKSPACE_ID") if id == "" { id = uuid.NewString() @@ -131,6 +134,11 @@ func workspaceDataSource() *schema.Resource { Computed: true, Description: "Name of the workspace.", }, + "owner_session_token": { + Type: schema.TypeString, + Computed: true, + Description: "Session token for interfacing with a Coder deployment. It is regenerated everytime a workspace is started.", + }, }, } } diff --git a/provider/workspace_test.go b/provider/workspace_test.go index 5473f20f..7662f4bc 100644 --- a/provider/workspace_test.go +++ b/provider/workspace_test.go @@ -3,16 +3,18 @@ package provider_test import ( "testing" - "github.com/coder/terraform-provider-coder/provider" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" "github.com/stretchr/testify/require" + + "github.com/coder/terraform-provider-coder/provider" ) func TestWorkspace(t *testing.T) { t.Setenv("CODER_WORKSPACE_OWNER", "owner123") t.Setenv("CODER_WORKSPACE_OWNER_EMAIL", "owner123@example.com") + t.Setenv("CODER_WORKSPACE_OWNER_SESSION_TOKEN", "abc123") resource.Test(t, resource.TestCase{ Providers: map[string]*schema.Provider{ @@ -39,6 +41,7 @@ func TestWorkspace(t *testing.T) { require.Equal(t, "8080", attribs["access_port"]) require.Equal(t, "owner123", attribs["owner"]) require.Equal(t, "owner123@example.com", attribs["owner_email"]) + require.Equal(t, "abc123", attribs["owner_session_token"]) return nil }, }}, From 946db5afc9a34e1ca76d6d842bb1944a570a4600 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Mon, 29 May 2023 15:58:22 +0200 Subject: [PATCH 3/4] fix: coder_parameter: validation min and max are truly optional (#121) --- provider/decode_test.go | 17 ++++++-- provider/parameter.go | 82 ++++++++++++++++++++++++++++++-------- provider/parameter_test.go | 52 ++++++++++++++++++------ 3 files changed, 119 insertions(+), 32 deletions(-) diff --git a/provider/decode_test.go b/provider/decode_test.go index c7215bd8..ca3feccd 100644 --- a/provider/decode_test.go +++ b/provider/decode_test.go @@ -5,6 +5,7 @@ import ( "github.com/coder/terraform-provider-coder/provider" "github.com/mitchellh/mapstructure" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -18,15 +19,25 @@ func TestDecode(t *testing.T) { aMap := map[string]interface{}{ "name": "Parameter Name", + "type": "number", "display_name": displayName, "legacy_variable": legacyVariable, "legacy_variable_name": legacyVariableName, + "min": nil, + "validation": []map[string]interface{}{ + { + "min": nil, + "max": 5, + }, + }, } var param provider.Parameter err := mapstructure.Decode(aMap, ¶m) require.NoError(t, err) - require.Equal(t, displayName, param.DisplayName) - require.Equal(t, legacyVariable, param.LegacyVariable) - require.Equal(t, legacyVariableName, param.LegacyVariableName) + assert.Equal(t, displayName, param.DisplayName) + assert.Equal(t, legacyVariable, param.LegacyVariable) + assert.Equal(t, legacyVariableName, param.LegacyVariableName) + assert.Equal(t, (*int)(nil), param.Validation[0].Min) + assert.Equal(t, 5, *param.Validation[0].Max) } diff --git a/provider/parameter.go b/provider/parameter.go index 972950a6..a9bbdc65 100644 --- a/provider/parameter.go +++ b/provider/parameter.go @@ -12,10 +12,12 @@ import ( "strconv" "github.com/google/uuid" + "github.com/hashicorp/go-cty/cty" "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" "github.com/mitchellh/mapstructure" + "golang.org/x/xerrors" ) type Option struct { @@ -26,8 +28,8 @@ type Option struct { } type Validation struct { - Min int - Max int + Min *int + Max *int Monotonic string Regex string @@ -62,8 +64,18 @@ func parameterDataSource() *schema.Resource { ReadContext: func(ctx context.Context, rd *schema.ResourceData, i interface{}) diag.Diagnostics { rd.SetId(uuid.NewString()) + fixedValidation, err := fixValidationResourceData(rd.GetRawConfig(), rd.Get("validation")) + if err != nil { + return diag.FromErr(err) + } + + err = rd.Set("validation", fixedValidation) + if err != nil { + return diag.FromErr(err) + } + var parameter Parameter - err := mapstructure.Decode(struct { + err = mapstructure.Decode(struct { Value interface{} Name interface{} DisplayName interface{} @@ -98,7 +110,7 @@ func parameterDataSource() *schema.Resource { }(), Icon: rd.Get("icon"), Option: rd.Get("option"), - Validation: rd.Get("validation"), + Validation: fixedValidation, Optional: func() bool { // This hack allows for checking if the "default" field is present in the .tf file. // If "default" is missing or is "null", then it means that this field is required, @@ -272,17 +284,14 @@ func parameterDataSource() *schema.Resource { Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "min": { - Type: schema.TypeInt, - Optional: true, - Default: 0, - Description: "The minimum of a number parameter.", - RequiredWith: []string{"validation.0.max"}, + Type: schema.TypeInt, + Optional: true, + Description: "The minimum of a number parameter.", }, "max": { - Type: schema.TypeInt, - Optional: true, - Description: "The maximum of a number parameter.", - RequiredWith: []string{"validation.0.min"}, + Type: schema.TypeInt, + Optional: true, + Description: "The maximum of a number parameter.", }, "monotonic": { Type: schema.TypeString, @@ -325,6 +334,45 @@ func parameterDataSource() *schema.Resource { } } +func fixValidationResourceData(rawConfig cty.Value, validation interface{}) (interface{}, error) { + // Read validation from raw config + rawValidation, ok := rawConfig.AsValueMap()["validation"] + if !ok { + return validation, nil // no validation rules, nothing to fix + } + + rawValidationArr := rawValidation.AsValueSlice() + if len(rawValidationArr) == 0 { + return validation, nil // no validation rules, nothing to fix + } + + rawValidationRule := rawValidationArr[0].AsValueMap() + + // Load validation from resource data + vArr, ok := validation.([]interface{}) + if !ok { + return nil, xerrors.New("validation should be an array") + } + + if len(vArr) == 0 { + return validation, nil // no validation rules, nothing to fix + } + + validationRule, ok := vArr[0].(map[string]interface{}) + if !ok { + return nil, xerrors.New("validation rule should be a map") + } + + // Fix the resource data + if rawValidationRule["min"].IsNull() { + validationRule["min"] = nil + } + if rawValidationRule["max"].IsNull() { + validationRule["max"] = nil + } + return vArr, nil +} + func valueIsType(typ, value string) diag.Diagnostics { switch typ { case "number": @@ -353,10 +401,10 @@ func valueIsType(typ, value string) diag.Diagnostics { func (v *Validation) Valid(typ, value string) error { if typ != "number" { - if v.Min != 0 { + if v.Min != nil { return fmt.Errorf("a min cannot be specified for a %s type", typ) } - if v.Max != 0 { + if v.Max != nil { return fmt.Errorf("a max cannot be specified for a %s type", typ) } } @@ -389,10 +437,10 @@ func (v *Validation) Valid(typ, value string) error { if err != nil { return fmt.Errorf("value %q is not a number", value) } - if num < v.Min { + if v.Min != nil && num < *v.Min { return fmt.Errorf("value %d is less than the minimum %d", num, v.Min) } - if num > v.Max { + if v.Max != nil && num > *v.Max { return fmt.Errorf("value %d is more than the maximum %d", num, v.Max) } if v.Monotonic != "" && v.Monotonic != ValidationMonotonicIncreasing && v.Monotonic != ValidationMonotonicDecreasing { diff --git a/provider/parameter_test.go b/provider/parameter_test.go index c349190b..4b2fe9c9 100644 --- a/provider/parameter_test.go +++ b/provider/parameter_test.go @@ -109,6 +109,30 @@ func TestParameter(t *testing.T) { } } `, + }, { + Name: "NumberValidation_Min", + Config: ` + data "coder_parameter" "region" { + name = "Region" + type = "number" + default = 2 + validation { + min = 1 + } + } + `, + }, { + Name: "NumberValidation_Max", + Config: ` + data "coder_parameter" "region" { + name = "Region" + type = "number" + default = 2 + validation { + max = 9 + } + } + `, }, { Name: "DefaultNotNumber", Config: ` @@ -443,18 +467,18 @@ func TestValueValidatesType(t *testing.T) { Regex, RegexError string Min, - Max int + Max *int Monotonic string Error *regexp.Regexp }{{ Name: "StringWithMin", Type: "string", - Min: 1, + Min: ptrNumber(1), Error: regexp.MustCompile("cannot be specified"), }, { Name: "StringWithMax", Type: "string", - Max: 1, + Max: ptrNumber(1), Error: regexp.MustCompile("cannot be specified"), }, { Name: "NonStringWithRegex", @@ -474,13 +498,13 @@ func TestValueValidatesType(t *testing.T) { Name: "NumberBelowMin", Type: "number", Value: "0", - Min: 1, + Min: ptrNumber(1), Error: regexp.MustCompile("is less than the minimum"), }, { Name: "NumberAboveMax", Type: "number", - Value: "1", - Max: 0, + Value: "2", + Max: ptrNumber(1), Error: regexp.MustCompile("is more than the maximum"), }, { Name: "InvalidBool", @@ -498,23 +522,23 @@ func TestValueValidatesType(t *testing.T) { Name: "InvalidMonotonicity", Type: "number", Value: "1", - Min: 0, - Max: 2, + Min: ptrNumber(0), + Max: ptrNumber(2), Monotonic: "foobar", Error: regexp.MustCompile(`number monotonicity can be either "increasing" or "decreasing"`), }, { Name: "IncreasingMonotonicity", Type: "number", Value: "1", - Min: 0, - Max: 2, + Min: ptrNumber(0), + Max: ptrNumber(2), Monotonic: "increasing", }, { Name: "DecreasingMonotonicity", Type: "number", Value: "1", - Min: 0, - Max: 2, + Min: ptrNumber(0), + Max: ptrNumber(2), Monotonic: "decreasing", }, { Name: "ValidListOfStrings", @@ -550,3 +574,7 @@ func TestValueValidatesType(t *testing.T) { }) } } + +func ptrNumber(i int) *int { + return &i +} From 2f64d4c025fda062fb54d11f9034397aea81ca6b Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 30 May 2023 14:01:43 +0200 Subject: [PATCH 4/4] fix: show correct min and max values in validation errors (#122) --- provider/parameter.go | 4 ++-- provider/parameter_test.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/provider/parameter.go b/provider/parameter.go index a9bbdc65..79ea2480 100644 --- a/provider/parameter.go +++ b/provider/parameter.go @@ -438,10 +438,10 @@ func (v *Validation) Valid(typ, value string) error { return fmt.Errorf("value %q is not a number", value) } if v.Min != nil && num < *v.Min { - return fmt.Errorf("value %d is less than the minimum %d", num, v.Min) + return fmt.Errorf("value %d is less than the minimum %d", num, *v.Min) } if v.Max != nil && num > *v.Max { - return fmt.Errorf("value %d is more than the maximum %d", num, v.Max) + return fmt.Errorf("value %d is more than the maximum %d", num, *v.Max) } if v.Monotonic != "" && v.Monotonic != ValidationMonotonicIncreasing && v.Monotonic != ValidationMonotonicDecreasing { return fmt.Errorf("number monotonicity can be either %q or %q", ValidationMonotonicIncreasing, ValidationMonotonicDecreasing) diff --git a/provider/parameter_test.go b/provider/parameter_test.go index 4b2fe9c9..945820a1 100644 --- a/provider/parameter_test.go +++ b/provider/parameter_test.go @@ -499,13 +499,13 @@ func TestValueValidatesType(t *testing.T) { Type: "number", Value: "0", Min: ptrNumber(1), - Error: regexp.MustCompile("is less than the minimum"), + Error: regexp.MustCompile("is less than the minimum 1"), }, { Name: "NumberAboveMax", Type: "number", Value: "2", Max: ptrNumber(1), - Error: regexp.MustCompile("is more than the maximum"), + Error: regexp.MustCompile("is more than the maximum 1"), }, { Name: "InvalidBool", Type: "bool",