Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion provider/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"crypto/sha256"
"encoding/hex"
"fmt"
"net/url"
"path/filepath"
"reflect"
"strings"
Expand Down Expand Up @@ -81,6 +82,20 @@ func agentResource() *schema.Resource {
return nil
},
Schema: map[string]*schema.Schema{
"name": {
Type: schema.TypeString,
Description: "The name of the agent. When using `for_each` or `count`, this can be set to a unique value (e.g., `each.key`) to avoid name collisions. Defaults to the Terraform resource block name.",
ForceNew: true,
Optional: true,
ValidateFunc: validation.StringIsNotEmpty,
},
"coder_access_url": {
Type: schema.TypeString,
Description: "Override the Coder access URL that the agent uses to phone home. Useful for networking scenarios where the agent needs to connect via an internal DNS name instead of the public URL.",
ForceNew: true,
Optional: true,
ValidateFunc: validation.StringIsNotEmpty,
},
"api_key_scope": {
Type: schema.TypeString,
Optional: true,
Expand Down Expand Up @@ -491,7 +506,18 @@ func updateInitScript(resourceData *schema.ResourceData, i interface{}) diag.Dia
if !valid {
return diag.Errorf("arch was unexpected type %q", reflect.TypeOf(resourceData.Get("arch")))
}
accessURL, err := config.URL.Parse("/")

// Use custom coder_access_url if provided, otherwise use the provider config URL
accessURL := config.URL
if customURL, ok := resourceData.Get("coder_access_url").(string); ok && customURL != "" {
parsedCustomURL, err := url.Parse(customURL)
if err != nil {
return diag.Errorf("parse custom coder_access_url: %s", err)
}
accessURL = parsedCustomURL
}

accessURL, err := accessURL.Parse("/")
if err != nil {
return diag.Errorf("parse access url: %s", err)
}
Expand Down
7 changes: 7 additions & 0 deletions provider/devcontainer.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ func devcontainerResource() *schema.Resource {
ReadContext: schema.NoopContext,
DeleteContext: schema.NoopContext,
Schema: map[string]*schema.Schema{
"name": {
Type: schema.TypeString,
Description: "The name of the sub-agent. When using `for_each` or `count`, this can be set to a unique value (e.g., `each.key`) to avoid name collisions. Defaults to the Terraform resource block name.",
ForceNew: true,
Optional: true,
ValidateFunc: validation.StringIsNotEmpty,
},
"agent_id": {
Type: schema.TypeString,
Description: "The `id` property of a `coder_agent` resource to associate with.",
Expand Down
6 changes: 6 additions & 0 deletions provider/formtype.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const (
OptionTypeNumber OptionType = "number"
OptionTypeBoolean OptionType = "bool"
OptionTypeListString OptionType = "list(string)"
OptionTypeKeyValue OptionType = "keyvalue"
)

func OptionTypes() []OptionType {
Expand All @@ -28,6 +29,7 @@ func OptionTypes() []OptionType {
OptionTypeNumber,
OptionTypeBoolean,
OptionTypeListString,
OptionTypeKeyValue,
}
}

Expand Down Expand Up @@ -106,6 +108,10 @@ var formTypeTruthTable = map[OptionType]map[bool][]ParameterFormType{
true: {ParameterFormTypeRadio, ParameterFormTypeMultiSelect},
false: {ParameterFormTypeTagSelect},
},
OptionTypeKeyValue: {
true: {ParameterFormTypeRadio, ParameterFormTypeDropdown},
false: {ParameterFormTypeInput, ParameterFormTypeTextArea},
},
}

// ValidateFormType handles the truth table for the valid set of `type` and
Expand Down
61 changes: 46 additions & 15 deletions provider/parameter.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,11 @@ func parameterDataSource() *schema.Resource {
Option: rd.Get("option"),
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,
// and user must provide a value for it.
// Check if user explicitly set optional in the config
if userOptional, ok := rd.Get("optional").(bool); ok {
return userOptional
}
// Otherwise, fall back to: optional if default is provided
val := !rd.GetRawConfig().AsValueMap()["default"].IsNull()
rd.Set("optional", val)
return val
Expand All @@ -135,8 +137,9 @@ func parameterDataSource() *schema.Resource {
return diag.Errorf("parameter can't be immutable and ephemeral")
}

if !parameter.Optional && parameter.Ephemeral {
return diag.Errorf("ephemeral parameter requires the default property")
// Ephemeral parameters require either a default value or to be explicitly marked as optional
if parameter.Ephemeral && parameter.Default == nil && !parameter.Optional {
return diag.Errorf("ephemeral parameter requires the default property or optional = true")
}

var input *string
Expand Down Expand Up @@ -314,8 +317,9 @@ func parameterDataSource() *schema.Resource {
},
"optional": {
Type: schema.TypeBool,
Optional: true,
Computed: true,
Description: "Whether this value is optional.",
Description: "Whether this value is optional. If set to true, the parameter does not require a default value and can be left empty during workspace creation.",
},
"order": {
Type: schema.TypeInt,
Expand Down Expand Up @@ -383,6 +387,12 @@ func valueIsType(typ OptionType, value string) error {
if err != nil {
return err
}
case OptionTypeKeyValue:
var kv map[string]string
err := json.Unmarshal([]byte(value), &kv)
if err != nil {
return fmt.Errorf("%q is not a valid key-value object", value)
}
case OptionTypeString:
// Anything is a string!
default:
Expand Down Expand Up @@ -438,22 +448,37 @@ func (v *Parameter) ValidateInput(input *string, previous *string) (string, diag
forcedValue = *value
}

d := v.validValue(forcedValue, previous, optionType, optionValues, valuePath)
if d.HasError() {
return "", d
}

err = valueIsType(v.Type, forcedValue)
if err != nil {
// For optional parameters without a value, skip type validation
// This allows number (and other types) to be optional without requiring a default
if !v.Optional && forcedValue == "" {
return "", diag.Diagnostics{
{
Severity: diag.Error,
Summary: fmt.Sprintf("Parameter value is not of type %q", v.Type),
Detail: err.Error(),
Summary: fmt.Sprintf("Parameter %q is required but no value was provided", v.Name),
Detail: "Either provide a value or set a default value for this parameter",
},
}
}

// Only validate type if a value is provided
if forcedValue != "" {
d := v.validValue(forcedValue, previous, optionType, optionValues, valuePath)
if d.HasError() {
return "", d
}

err = valueIsType(v.Type, forcedValue)
if err != nil {
return "", diag.Diagnostics{
{
Severity: diag.Error,
Summary: fmt.Sprintf("Parameter value is not of type %q", v.Type),
Detail: err.Error(),
},
}
}
}

return forcedValue, nil
}

Expand Down Expand Up @@ -669,6 +694,12 @@ func (v *Validation) Valid(typ OptionType, value string, previous *string) error
if err != nil {
return fmt.Errorf("value %q is not valid list of strings", value)
}
case OptionTypeKeyValue:
var kv map[string]string
err := json.Unmarshal([]byte(value), &kv)
if err != nil {
return fmt.Errorf("value %q is not a valid key-value object", value)
}
}
return nil
}
Expand Down
Loading