diff --git a/cli/clidisplay/resources.go b/cli/clidisplay/resources.go index e19c3e3..f7eab6e 100644 --- a/cli/clidisplay/resources.go +++ b/cli/clidisplay/resources.go @@ -43,7 +43,6 @@ func WorkspaceTags(writer io.Writer, tags types.TagBlocks) hcl.Diagnostics { func Parameters(writer io.Writer, params []types.Parameter, files map[string]*hcl.File) { tableWriter := table.NewWriter() - // tableWriter.SetTitle("Parameters") tableWriter.SetStyle(table.StyleLight) tableWriter.Style().Options.SeparateColumns = false row := table.Row{"Parameter"} @@ -54,20 +53,14 @@ func Parameters(writer io.Writer, params []types.Parameter, files map[string]*hc if p.FormType == provider.ParameterFormTypeMultiSelect { _ = json.Unmarshal([]byte(strVal), &selections) } - // value := p.Value.Value - // - // if value.IsNull() { - // strVal = "null" - // } else if !p.Value.Value.IsKnown() { - // strVal = "unknown" - // } else if value.Type().Equals(cty.String) { - // strVal = value.AsString() - // } else { - // strVal = value.GoString() - //} + + dp := p.DisplayName + if p.DisplayName == "" { + dp = p.Name + } tableWriter.AppendRow(table.Row{ - fmt.Sprintf("(%s) %s: %s\n%s", p.DisplayName, p.Name, p.Description, formatOptions(selections, p.Options)), + fmt.Sprintf("(%s) %s: %s\n%s", dp, p.Name, p.Description, formatOptions(selections, p.Options)), }) if hcl.Diagnostics(p.Diagnostics).HasErrors() { diff --git a/extract/parameter.go b/extract/parameter.go index 198173c..222807d 100644 --- a/extract/parameter.go +++ b/extract/parameter.go @@ -50,10 +50,12 @@ func ParameterFromBlock(block *terraform.Block) (*types.Parameter, hcl.Diagnosti pVal := richParameterValue(block) - def := types.StringLiteral("") + requiredValue := true + def := types.NullString() defAttr := block.GetAttribute("default") if !defAttr.IsNil() { def = types.ToHCLString(block, defAttr) + requiredValue = false } ftmeta := optionalString(block, "styling") @@ -77,7 +79,7 @@ func ParameterFromBlock(block *terraform.Block) (*types.Parameter, hcl.Diagnosti Icon: optionalString(block, "icon"), Options: make([]*types.ParameterOption, 0), Validations: make([]*types.ParameterValidation, 0), - Required: optionalBoolean(block, "required"), + Required: requiredValue, DisplayName: optionalString(block, "display_name"), Order: optionalInteger(block, "order"), Ephemeral: optionalBoolean(block, "ephemeral"), @@ -138,40 +140,10 @@ func ParameterFromBlock(block *terraform.Block) (*types.Parameter, hcl.Diagnosti p.Validations = append(p.Validations, &valid) } - ctyType, err := p.CtyType() - if err != nil { - paramTypeDiag := &hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: fmt.Sprintf("Invalid parameter type %q", p.Type), - Detail: err.Error(), - Context: &block.HCLBlock().DefRange, - } - - if attr := block.GetAttribute("type"); attr != nil && !attr.IsNil() { - paramTypeDiag.Subject = &attr.HCLAttribute().Range - paramTypeDiag.Expression = attr.HCLAttribute().Expr - paramTypeDiag.EvalContext = block.Context().Inner() - } - diags = diags.Append(paramTypeDiag) - p.FormType = provider.ParameterFormTypeError - } - - if ctyType != cty.NilType && pVal.Value.Type().Equals(cty.String) { - // TODO: Wish we could support more types, but only string types are - // allowed. - //nolint:gocritic // string type asserted - valStr := pVal.Value.AsString() - // Apply validations to the parameter value - for _, v := range p.Validations { - if err := v.Valid(string(pType), valStr); err != nil { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: fmt.Sprintf("Paramater validation failed for value %q", valStr), - Detail: err.Error(), - Expression: pVal.ValueExpr, - }) - } - } + if !diags.HasErrors() { + // Only do this validation if the parameter is valid, as if some errors + // exist, then this is likely to fail be excess information. + diags = diags.Extend(p.Valid(p.Value)) } usageDiags := ParameterUsageDiagnostics(p) @@ -189,7 +161,9 @@ func ParameterFromBlock(block *terraform.Block) (*types.Parameter, hcl.Diagnosti func ParameterUsageDiagnostics(p types.Parameter) hcl.Diagnostics { valErr := "The value of a parameter is required to be sourced (default or input) for the parameter to function." var diags hcl.Diagnostics - if !p.Value.Valid() { + if p.Value.Value.IsNull() { + // Allow null values + } else if !p.Value.Valid() { diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Parameter value is not valid", @@ -244,7 +218,6 @@ func ParameterValidationFromBlock(block *terraform.Block) (types.ParameterValida Min: nullableInteger(block, "min"), Max: nullableInteger(block, "max"), Monotonic: nullableString(block, "monotonic"), - Invalid: nullableBoolean(block, "invalid"), } return p, diags @@ -477,6 +450,14 @@ func richParameterValue(block *terraform.Block) types.HCLString { val, diags := valRef.Value(block.Context().Inner()) source := hclext.CreateDotReferenceFromTraversal(valRef.Traversal) + + // If no value attribute exists, then the value is `null`. + if diags.HasErrors() && diags[0].Summary == "Unsupported attribute" { + s := types.NullString() + s.Source = &source + return s + } + return types.HCLString{ Value: val, ValueDiags: diags, diff --git a/extract/state.go b/extract/state.go index 048ae85..5e1bf7b 100644 --- a/extract/state.go +++ b/extract/state.go @@ -74,7 +74,7 @@ func ParameterFromState(block *tfjson.StateResource) (types.Parameter, error) { Icon: st.optionalString("icon"), Options: options, Validations: validations, - Required: st.optionalBool("required"), + Required: !st.optionalBool("optional"), DisplayName: st.optionalString("display_name"), Order: st.optionalInteger("order"), Ephemeral: st.optionalBool("ephemeral"), diff --git a/go.mod b/go.mod index 634edb1..51c118b 100644 --- a/go.mod +++ b/go.mod @@ -7,15 +7,18 @@ require ( github.com/aquasecurity/trivy v0.58.2 github.com/coder/guts v1.0.2-0.20250227211802-139809366a22 github.com/coder/serpent v0.10.0 - github.com/coder/terraform-provider-coder/v2 v2.4.0-pre1.0.20250417100258-c86bb5c3ddcd + github.com/coder/terraform-provider-coder/v2 v2.4.0-pre1.0.20250506184715-e011f733bf27 github.com/coder/websocket v1.8.13 github.com/go-chi/chi v4.1.2+incompatible + github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 github.com/hashicorp/go-version v1.7.0 github.com/hashicorp/hc-install v0.9.2 github.com/hashicorp/hcl/v2 v2.23.0 github.com/hashicorp/terraform-exec v0.23.0 github.com/hashicorp/terraform-json v0.24.0 + github.com/hashicorp/terraform-plugin-sdk/v2 v2.36.1 github.com/jedib0t/go-pretty/v6 v6.6.7 + github.com/quasilyte/go-ruleguard/dsl v0.3.22 github.com/stretchr/testify v1.10.0 github.com/zclconf/go-cty v1.16.2 golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da @@ -69,7 +72,6 @@ require ( github.com/googleapis/gax-go/v2 v2.14.1 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect - github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 // indirect github.com/hashicorp/go-getter v1.7.8 // indirect github.com/hashicorp/go-hclog v1.6.3 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect @@ -79,7 +81,6 @@ require ( github.com/hashicorp/logutils v1.0.0 // indirect github.com/hashicorp/terraform-plugin-go v0.26.0 // indirect github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect - github.com/hashicorp/terraform-plugin-sdk/v2 v2.36.1 // indirect github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24 // indirect github.com/klauspost/compress v1.17.11 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect @@ -98,7 +99,6 @@ require ( github.com/pion/udp v0.1.4 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/quasilyte/go-ruleguard/dsl v0.3.22 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect github.com/samber/lo v1.49.1 // indirect diff --git a/go.sum b/go.sum index 3cdb559..4e42b90 100644 --- a/go.sum +++ b/go.sum @@ -718,8 +718,8 @@ github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0 h1:3A0ES21Ke+FxEM8CXx github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0/go.mod h1:5UuS2Ts+nTToAMeOjNlnHFkPahrtDkmpydBen/3wgZc= github.com/coder/serpent v0.10.0 h1:ofVk9FJXSek+SmL3yVE3GoArP83M+1tX+H7S4t8BSuM= github.com/coder/serpent v0.10.0/go.mod h1:cZFW6/fP+kE9nd/oRkEHJpG6sXCtQ+AX7WMMEHv0Y3Q= -github.com/coder/terraform-provider-coder/v2 v2.4.0-pre1.0.20250417100258-c86bb5c3ddcd h1:FsIG6Fd0YOEK7D0Hl/CJywRA+Y6Gd5RQbSIa2L+/BmE= -github.com/coder/terraform-provider-coder/v2 v2.4.0-pre1.0.20250417100258-c86bb5c3ddcd/go.mod h1:56/KdGYaA+VbwXJbTI8CA57XPfnuTxN8rjxbR34PbZw= +github.com/coder/terraform-provider-coder/v2 v2.4.0-pre1.0.20250506184715-e011f733bf27 h1:CLJwMqst39+wfFehYQzVOiG5uXUtC5fbAZ3/EpxOWos= +github.com/coder/terraform-provider-coder/v2 v2.4.0-pre1.0.20250506184715-e011f733bf27/go.mod h1:2kaBpn5k9ZWtgKq5k4JbkVZG9DzEqR4mJSmpdshcO+s= github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE= github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= diff --git a/preview_test.go b/preview_test.go index 5bba3a9..a72dfab 100644 --- a/preview_test.go +++ b/preview_test.go @@ -211,6 +211,16 @@ func Test_Extract(t *testing.T) { unknownTags: []string{}, params: map[string]assertParam{}, }, + { + name: "empty default", + dir: "emptydefault", + expTags: map[string]string{}, + input: preview.Input{}, + unknownTags: []string{}, + params: map[string]assertParam{ + "word": ap(), + }, + }, { name: "many modules", dir: "manymodules", diff --git a/site/src/types/preview.ts b/site/src/types/preview.ts index 0d19c4d..fd8fa38 100644 --- a/site/src/types/preview.ts +++ b/site/src/types/preview.ts @@ -73,7 +73,6 @@ export interface ParameterValidation { readonly validation_min: number | null; readonly validation_max: number | null; readonly validation_monotonic: string | null; - readonly validation_invalid: boolean | null; } // From web/session.go diff --git a/testdata/emptydefault/main.tf b/testdata/emptydefault/main.tf new file mode 100644 index 0000000..baa398e --- /dev/null +++ b/testdata/emptydefault/main.tf @@ -0,0 +1,26 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + version = "2.4.0-pre0" + } + } +} + +data "coder_parameter" "word" { + name = "word" + description = "Select something" + type = "string" + order = 1 + # No default selected + + option { + name = "Bird" + value = "bird" + description = "An animal that can fly." + } + option { + name = "Boat" + value = "boat" + } +} diff --git a/testdata/emptydefault/skipe2e b/testdata/emptydefault/skipe2e new file mode 100644 index 0000000..bc530d8 --- /dev/null +++ b/testdata/emptydefault/skipe2e @@ -0,0 +1 @@ +Skipping until https://github.com/coder/terraform-provider-coder/pull/381 is merged and released diff --git a/types/convert.go b/types/convert.go new file mode 100644 index 0000000..3d395e5 --- /dev/null +++ b/types/convert.go @@ -0,0 +1,90 @@ +package types + +import ( + "github.com/aquasecurity/trivy/pkg/iac/terraform" + hcty "github.com/hashicorp/go-cty/cty" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + + "github.com/coder/terraform-provider-coder/v2/provider" +) + +func providerValidations(vals []*ParameterValidation) []provider.Validation { + cpy := make([]provider.Validation, 0, len(vals)) + for _, val := range vals { + cpy = append(cpy, providerValidation(val)) + } + return cpy +} + +func providerValidation(v *ParameterValidation) provider.Validation { + return provider.Validation{ + Min: int(orZero(v.Min)), + MinDisabled: v.Min == nil, + Max: int(orZero(v.Max)), + MaxDisabled: v.Max == nil, + Monotonic: orZero(v.Monotonic), + Regex: orZero(v.Regex), + Error: v.Error, + } +} + +func providerOptions(opts []*ParameterOption) []provider.Option { + cpy := make([]provider.Option, 0, len(opts)) + for _, opt := range opts { + cpy = append(cpy, providerOption(opt)) + } + return cpy +} + +func providerOption(opt *ParameterOption) provider.Option { + return provider.Option{ + Name: opt.Name, + Description: opt.Description, + Value: opt.Value.AsString(), + Icon: opt.Icon, + } +} + +func hclDiagnostics(diagnostics diag.Diagnostics, source *terraform.Block) hcl.Diagnostics { + cpy := make(hcl.Diagnostics, 0, len(diagnostics)) + for _, d := range diagnostics { + cpy = append(cpy, hclDiagnostic(d, source)) + } + return cpy +} + +func hclDiagnostic(d diag.Diagnostic, source *terraform.Block) *hcl.Diagnostic { + sev := hcl.DiagInvalid + switch d.Severity { + case diag.Error: + sev = hcl.DiagError + case diag.Warning: + sev = hcl.DiagWarning + } + + // This is an imperfect way to finding the source code of the error. There is 2 + // different `cty` types at place here, the hashicorp fork and the original. So a + // more general solution is difficult. This is good enough for now to add more + // context to an error. + var subject *hcl.Range + if len(d.AttributePath) == 1 && source != nil { + if attr, ok := d.AttributePath[0].(hcty.GetAttrStep); ok { + src := source.GetAttribute(attr.Name) + if src != nil { + subject = &(src.HCLAttribute().Range) + } + } + } + + return &hcl.Diagnostic{ + Severity: sev, + Summary: d.Summary, + Detail: d.Detail, + Subject: subject, + Context: nil, + Expression: nil, + EvalContext: nil, + Extra: nil, + } +} diff --git a/types/enum.go b/types/enum.go index ab29945..190668b 100644 --- a/types/enum.go +++ b/types/enum.go @@ -3,9 +3,12 @@ package types import ( "fmt" "strings" + + "github.com/coder/terraform-provider-coder/v2/provider" ) -type ParameterType string +// TODO: Just use the provider type directly. +type ParameterType provider.OptionType const ( ParameterTypeString ParameterType = "string" diff --git a/types/parameter.go b/types/parameter.go index f1f07b0..ab15a6f 100644 --- a/types/parameter.go +++ b/types/parameter.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/aquasecurity/trivy/pkg/iac/terraform" + "github.com/hashicorp/hcl/v2" "github.com/zclconf/go-cty/cty" "github.com/coder/terraform-provider-coder/v2/provider" @@ -73,7 +74,6 @@ type ParameterValidation struct { Min *int64 `json:"validation_min"` Max *int64 `json:"validation_max"` Monotonic *string `json:"validation_monotonic"` - Invalid *bool `json:"validation_invalid"` } type ParameterStyling struct { @@ -82,23 +82,6 @@ type ParameterStyling struct { Label *string `json:"label,omitempty"` } -// Valid takes the type of the value and the value itself and returns an error -// if the value is invalid. -func (v ParameterValidation) Valid(typ string, value string) error { - // TODO: Validate typ is the enum? - // Use the provider.Validation struct to validate the value to be - // consistent with the provider. - return (&provider.Validation{ - Min: int(orZero(v.Min)), - MinDisabled: v.Min == nil, - Max: int(orZero(v.Max)), - MaxDisabled: v.Max == nil, - Monotonic: orZero(v.Monotonic), - Regex: orZero(v.Regex), - Error: v.Error, - }).Valid(typ, value) -} - type ParameterOption struct { Name string `json:"name"` Description string `json:"description"` @@ -106,6 +89,45 @@ type ParameterOption struct { Icon string `json:"icon"` } +func (r *ParameterData) Valid(value HCLString) hcl.Diagnostics { + var defPtr *string + + if r.DefaultValue.Valid() && r.DefaultValue.IsKnown() { + def := r.DefaultValue.AsString() + defPtr = &def + } + + var valuePtr *string + // TODO: What to do if it is not valid? + if value.Valid() { + val := value.AsString() + valuePtr = &val + } + + _, diag := (&provider.Parameter{ + Name: r.Name, + DisplayName: r.DisplayName, + Description: r.Description, + Type: provider.OptionType(r.Type), + FormType: r.FormType, + Mutable: r.Mutable, + Default: defPtr, + Icon: r.Icon, + Option: providerOptions(r.Options), + Validation: providerValidations(r.Validations), + Optional: !r.Required, + Order: int(r.Order), + Ephemeral: r.Ephemeral, + }).ValidateInput(valuePtr, nil) // TODO: Pass in previous value + + if diag.HasError() { + // TODO: We can take the attr path and decorate the error with + // source information. + return hclDiagnostics(diag, r.Source) + } + return nil +} + // CtyType returns the cty.Type for the ParameterData. // A fixed set of types are supported. func (r *ParameterData) CtyType() (cty.Type, error) { diff --git a/types/value.go b/types/value.go index 37f862b..e26087b 100644 --- a/types/value.go +++ b/types/value.go @@ -73,6 +73,14 @@ func StringLiteral(s string) HCLString { } } +func NullString() HCLString { + v := cty.NullVal(cty.String) + return HCLString{ + Value: v, + ValueExpr: &hclsyntax.LiteralValueExpr{Val: v}, + } +} + // AsString is a safe function. It will always return a string. // The caller should check if this value is Valid and known before // calling this function.