mirror of
https://github.com/coder/coder.git
synced 2025-07-09 11:45:56 +00:00
fix(coderd/wsbuilder): correctly evaluate dynamic workspace tag values (#15897)
Relates to https://github.com/coder/coder/issues/15894: - Adds `coderdenttest.NewExternalProvisionerDaemonTerraform` - Adds integration-style test coverage for creating a workspace with `coder_workspace_tags` specified in `main.tf` - Modifies `coderd/wsbuilder` to fetch template version variables and includes them in eval context for evaluating `coder_workspace_tags`
This commit is contained in:
@ -12,9 +12,9 @@ import (
|
|||||||
|
|
||||||
"github.com/hashicorp/hcl/v2"
|
"github.com/hashicorp/hcl/v2"
|
||||||
"github.com/hashicorp/hcl/v2/hclsyntax"
|
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||||
"github.com/zclconf/go-cty/cty"
|
|
||||||
|
|
||||||
"github.com/coder/coder/v2/coderd/rbac/policy"
|
"github.com/coder/coder/v2/coderd/rbac/policy"
|
||||||
|
"github.com/coder/coder/v2/provisioner/terraform/tfparse"
|
||||||
"github.com/coder/coder/v2/provisionersdk"
|
"github.com/coder/coder/v2/provisionersdk"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
@ -64,6 +64,7 @@ type Builder struct {
|
|||||||
templateVersion *database.TemplateVersion
|
templateVersion *database.TemplateVersion
|
||||||
templateVersionJob *database.ProvisionerJob
|
templateVersionJob *database.ProvisionerJob
|
||||||
templateVersionParameters *[]database.TemplateVersionParameter
|
templateVersionParameters *[]database.TemplateVersionParameter
|
||||||
|
templateVersionVariables *[]database.TemplateVersionVariable
|
||||||
templateVersionWorkspaceTags *[]database.TemplateVersionWorkspaceTag
|
templateVersionWorkspaceTags *[]database.TemplateVersionWorkspaceTag
|
||||||
lastBuild *database.WorkspaceBuild
|
lastBuild *database.WorkspaceBuild
|
||||||
lastBuildErr *error
|
lastBuildErr *error
|
||||||
@ -617,6 +618,22 @@ func (b *Builder) getTemplateVersionParameters() ([]database.TemplateVersionPara
|
|||||||
return tvp, nil
|
return tvp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *Builder) getTemplateVersionVariables() ([]database.TemplateVersionVariable, error) {
|
||||||
|
if b.templateVersionVariables != nil {
|
||||||
|
return *b.templateVersionVariables, nil
|
||||||
|
}
|
||||||
|
tvID, err := b.getTemplateVersionID()
|
||||||
|
if err != nil {
|
||||||
|
return nil, xerrors.Errorf("get template version ID to get variables: %w", err)
|
||||||
|
}
|
||||||
|
tvs, err := b.store.GetTemplateVersionVariables(b.ctx, tvID)
|
||||||
|
if err != nil && !xerrors.Is(err, sql.ErrNoRows) {
|
||||||
|
return nil, xerrors.Errorf("get template version %s variables: %w", tvID, err)
|
||||||
|
}
|
||||||
|
b.templateVersionVariables = &tvs
|
||||||
|
return tvs, nil
|
||||||
|
}
|
||||||
|
|
||||||
// verifyNoLegacyParameters verifies that initiator can't start the workspace build
|
// verifyNoLegacyParameters verifies that initiator can't start the workspace build
|
||||||
// if it uses legacy parameters (database.ParameterSchemas).
|
// if it uses legacy parameters (database.ParameterSchemas).
|
||||||
func (b *Builder) verifyNoLegacyParameters() error {
|
func (b *Builder) verifyNoLegacyParameters() error {
|
||||||
@ -678,17 +695,40 @@ func (b *Builder) getProvisionerTags() (map[string]string, error) {
|
|||||||
tags[name] = value
|
tags[name] = value
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 2: Mutate workspace tags
|
// Step 2: Mutate workspace tags:
|
||||||
|
// - Get workspace tags from the template version job
|
||||||
|
// - Get template version variables from the template version as they can be
|
||||||
|
// referenced in workspace tags
|
||||||
|
// - Get parameters from the workspace build as they can also be referenced
|
||||||
|
// in workspace tags
|
||||||
|
// - Evaluate workspace tags given the above inputs
|
||||||
workspaceTags, err := b.getTemplateVersionWorkspaceTags()
|
workspaceTags, err := b.getTemplateVersionWorkspaceTags()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, BuildError{http.StatusInternalServerError, "failed to fetch template version workspace tags", err}
|
return nil, BuildError{http.StatusInternalServerError, "failed to fetch template version workspace tags", err}
|
||||||
}
|
}
|
||||||
|
tvs, err := b.getTemplateVersionVariables()
|
||||||
|
if err != nil {
|
||||||
|
return nil, BuildError{http.StatusInternalServerError, "failed to fetch template version variables", err}
|
||||||
|
}
|
||||||
|
varsM := make(map[string]string)
|
||||||
|
for _, tv := range tvs {
|
||||||
|
// FIXME: do this in Terraform? This is a bit of a hack.
|
||||||
|
if tv.Value == "" {
|
||||||
|
varsM[tv.Name] = tv.DefaultValue
|
||||||
|
} else {
|
||||||
|
varsM[tv.Name] = tv.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
parameterNames, parameterValues, err := b.getParameters()
|
parameterNames, parameterValues, err := b.getParameters()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err // already wrapped BuildError
|
return nil, err // already wrapped BuildError
|
||||||
}
|
}
|
||||||
|
paramsM := make(map[string]string)
|
||||||
|
for i, name := range parameterNames {
|
||||||
|
paramsM[name] = parameterValues[i]
|
||||||
|
}
|
||||||
|
|
||||||
evalCtx := buildParametersEvalContext(parameterNames, parameterValues)
|
evalCtx := tfparse.BuildEvalContext(varsM, paramsM)
|
||||||
for _, workspaceTag := range workspaceTags {
|
for _, workspaceTag := range workspaceTags {
|
||||||
expr, diags := hclsyntax.ParseExpression([]byte(workspaceTag.Value), "expression.hcl", hcl.InitialPos)
|
expr, diags := hclsyntax.ParseExpression([]byte(workspaceTag.Value), "expression.hcl", hcl.InitialPos)
|
||||||
if diags.HasErrors() {
|
if diags.HasErrors() {
|
||||||
@ -701,7 +741,7 @@ func (b *Builder) getProvisionerTags() (map[string]string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Do not use "val.AsString()" as it can panic
|
// Do not use "val.AsString()" as it can panic
|
||||||
str, err := ctyValueString(val)
|
str, err := tfparse.CtyValueString(val)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, BuildError{http.StatusBadRequest, "failed to marshal cty.Value as string", err}
|
return nil, BuildError{http.StatusBadRequest, "failed to marshal cty.Value as string", err}
|
||||||
}
|
}
|
||||||
@ -710,44 +750,6 @@ func (b *Builder) getProvisionerTags() (map[string]string, error) {
|
|||||||
return tags, nil
|
return tags, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildParametersEvalContext(names, values []string) *hcl.EvalContext {
|
|
||||||
m := map[string]cty.Value{}
|
|
||||||
for i, name := range names {
|
|
||||||
m[name] = cty.MapVal(map[string]cty.Value{
|
|
||||||
"value": cty.StringVal(values[i]),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(m) == 0 {
|
|
||||||
return nil // otherwise, panic: must not call MapVal with empty map
|
|
||||||
}
|
|
||||||
|
|
||||||
return &hcl.EvalContext{
|
|
||||||
Variables: map[string]cty.Value{
|
|
||||||
"data": cty.MapVal(map[string]cty.Value{
|
|
||||||
"coder_parameter": cty.MapVal(m),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ctyValueString(val cty.Value) (string, error) {
|
|
||||||
switch val.Type() {
|
|
||||||
case cty.Bool:
|
|
||||||
if val.True() {
|
|
||||||
return "true", nil
|
|
||||||
} else {
|
|
||||||
return "false", nil
|
|
||||||
}
|
|
||||||
case cty.Number:
|
|
||||||
return val.AsBigFloat().String(), nil
|
|
||||||
case cty.String:
|
|
||||||
return val.AsString(), nil
|
|
||||||
default:
|
|
||||||
return "", xerrors.Errorf("only primitive types are supported - bool, number, and string")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Builder) getTemplateVersionWorkspaceTags() ([]database.TemplateVersionWorkspaceTag, error) {
|
func (b *Builder) getTemplateVersionWorkspaceTags() ([]database.TemplateVersionWorkspaceTag, error) {
|
||||||
if b.templateVersionWorkspaceTags != nil {
|
if b.templateVersionWorkspaceTags != nil {
|
||||||
return *b.templateVersionWorkspaceTags, nil
|
return *b.templateVersionWorkspaceTags, nil
|
||||||
|
@ -58,6 +58,7 @@ func TestBuilder_NoOptions(t *testing.T) {
|
|||||||
withTemplate,
|
withTemplate,
|
||||||
withInactiveVersion(nil),
|
withInactiveVersion(nil),
|
||||||
withLastBuildFound,
|
withLastBuildFound,
|
||||||
|
withTemplateVersionVariables(inactiveVersionID, nil),
|
||||||
withRichParameters(nil),
|
withRichParameters(nil),
|
||||||
withParameterSchemas(inactiveJobID, nil),
|
withParameterSchemas(inactiveJobID, nil),
|
||||||
withWorkspaceTags(inactiveVersionID, nil),
|
withWorkspaceTags(inactiveVersionID, nil),
|
||||||
@ -113,6 +114,7 @@ func TestBuilder_Initiator(t *testing.T) {
|
|||||||
withTemplate,
|
withTemplate,
|
||||||
withInactiveVersion(nil),
|
withInactiveVersion(nil),
|
||||||
withLastBuildFound,
|
withLastBuildFound,
|
||||||
|
withTemplateVersionVariables(inactiveVersionID, nil),
|
||||||
withRichParameters(nil),
|
withRichParameters(nil),
|
||||||
withParameterSchemas(inactiveJobID, nil),
|
withParameterSchemas(inactiveJobID, nil),
|
||||||
withWorkspaceTags(inactiveVersionID, nil),
|
withWorkspaceTags(inactiveVersionID, nil),
|
||||||
@ -158,6 +160,7 @@ func TestBuilder_Baggage(t *testing.T) {
|
|||||||
withTemplate,
|
withTemplate,
|
||||||
withInactiveVersion(nil),
|
withInactiveVersion(nil),
|
||||||
withLastBuildFound,
|
withLastBuildFound,
|
||||||
|
withTemplateVersionVariables(inactiveVersionID, nil),
|
||||||
withRichParameters(nil),
|
withRichParameters(nil),
|
||||||
withParameterSchemas(inactiveJobID, nil),
|
withParameterSchemas(inactiveJobID, nil),
|
||||||
withWorkspaceTags(inactiveVersionID, nil),
|
withWorkspaceTags(inactiveVersionID, nil),
|
||||||
@ -195,6 +198,7 @@ func TestBuilder_Reason(t *testing.T) {
|
|||||||
withTemplate,
|
withTemplate,
|
||||||
withInactiveVersion(nil),
|
withInactiveVersion(nil),
|
||||||
withLastBuildFound,
|
withLastBuildFound,
|
||||||
|
withTemplateVersionVariables(inactiveVersionID, nil),
|
||||||
withRichParameters(nil),
|
withRichParameters(nil),
|
||||||
withParameterSchemas(inactiveJobID, nil),
|
withParameterSchemas(inactiveJobID, nil),
|
||||||
withWorkspaceTags(inactiveVersionID, nil),
|
withWorkspaceTags(inactiveVersionID, nil),
|
||||||
@ -232,6 +236,7 @@ func TestBuilder_ActiveVersion(t *testing.T) {
|
|||||||
withTemplate,
|
withTemplate,
|
||||||
withActiveVersion(nil),
|
withActiveVersion(nil),
|
||||||
withLastBuildNotFound,
|
withLastBuildNotFound,
|
||||||
|
withTemplateVersionVariables(activeVersionID, nil),
|
||||||
withParameterSchemas(activeJobID, nil),
|
withParameterSchemas(activeJobID, nil),
|
||||||
withWorkspaceTags(activeVersionID, nil),
|
withWorkspaceTags(activeVersionID, nil),
|
||||||
withProvisionerDaemons([]database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow{}),
|
withProvisionerDaemons([]database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow{}),
|
||||||
@ -296,6 +301,14 @@ func TestWorkspaceBuildWithTags(t *testing.T) {
|
|||||||
Key: "is_debug_build",
|
Key: "is_debug_build",
|
||||||
Value: `data.coder_parameter.is_debug_build.value == "true" ? "in-debug-mode" : "no-debug"`,
|
Value: `data.coder_parameter.is_debug_build.value == "true" ? "in-debug-mode" : "no-debug"`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Key: "variable_tag",
|
||||||
|
Value: `var.tag`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: "another_variable_tag",
|
||||||
|
Value: `var.tag2`,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
richParameters := []database.TemplateVersionParameter{
|
richParameters := []database.TemplateVersionParameter{
|
||||||
@ -307,6 +320,11 @@ func TestWorkspaceBuildWithTags(t *testing.T) {
|
|||||||
{Name: "number_of_oranges", Type: "number", Description: "This is fifth parameter", Mutable: false, DefaultValue: "6", Options: json.RawMessage("[]")},
|
{Name: "number_of_oranges", Type: "number", Description: "This is fifth parameter", Mutable: false, DefaultValue: "6", Options: json.RawMessage("[]")},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
templateVersionVariables := []database.TemplateVersionVariable{
|
||||||
|
{Name: "tag", Description: "This is a variable tag", TemplateVersionID: inactiveVersionID, Type: "string", DefaultValue: "default-value", Value: "my-value"},
|
||||||
|
{Name: "tag2", Description: "This is another variable tag", TemplateVersionID: inactiveVersionID, Type: "string", DefaultValue: "default-value-2", Value: ""},
|
||||||
|
}
|
||||||
|
|
||||||
buildParameters := []codersdk.WorkspaceBuildParameter{
|
buildParameters := []codersdk.WorkspaceBuildParameter{
|
||||||
{Name: "project", Value: "foobar-foobaz"},
|
{Name: "project", Value: "foobar-foobaz"},
|
||||||
{Name: "is_debug_build", Value: "true"},
|
{Name: "is_debug_build", Value: "true"},
|
||||||
@ -321,6 +339,7 @@ func TestWorkspaceBuildWithTags(t *testing.T) {
|
|||||||
withTemplate,
|
withTemplate,
|
||||||
withInactiveVersion(richParameters),
|
withInactiveVersion(richParameters),
|
||||||
withLastBuildFound,
|
withLastBuildFound,
|
||||||
|
withTemplateVersionVariables(inactiveVersionID, templateVersionVariables),
|
||||||
withRichParameters(nil),
|
withRichParameters(nil),
|
||||||
withParameterSchemas(inactiveJobID, nil),
|
withParameterSchemas(inactiveJobID, nil),
|
||||||
withWorkspaceTags(inactiveVersionID, workspaceTags),
|
withWorkspaceTags(inactiveVersionID, workspaceTags),
|
||||||
@ -328,7 +347,7 @@ func TestWorkspaceBuildWithTags(t *testing.T) {
|
|||||||
|
|
||||||
// Outputs
|
// Outputs
|
||||||
expectProvisionerJob(func(job database.InsertProvisionerJobParams) {
|
expectProvisionerJob(func(job database.InsertProvisionerJobParams) {
|
||||||
asrt.Len(job.Tags, 10)
|
asrt.Len(job.Tags, 12)
|
||||||
|
|
||||||
expected := database.StringMap{
|
expected := database.StringMap{
|
||||||
"actually_no": "false",
|
"actually_no": "false",
|
||||||
@ -338,6 +357,8 @@ func TestWorkspaceBuildWithTags(t *testing.T) {
|
|||||||
"project_tag": "foobar-foobaz+12345",
|
"project_tag": "foobar-foobaz+12345",
|
||||||
"team_tag": "godzilla",
|
"team_tag": "godzilla",
|
||||||
"yes_or_no": "true",
|
"yes_or_no": "true",
|
||||||
|
"variable_tag": "my-value",
|
||||||
|
"another_variable_tag": "default-value-2",
|
||||||
|
|
||||||
"scope": "user",
|
"scope": "user",
|
||||||
"version": "inactive",
|
"version": "inactive",
|
||||||
@ -413,6 +434,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
|
|||||||
withTemplate,
|
withTemplate,
|
||||||
withInactiveVersion(richParameters),
|
withInactiveVersion(richParameters),
|
||||||
withLastBuildFound,
|
withLastBuildFound,
|
||||||
|
withTemplateVersionVariables(inactiveVersionID, nil),
|
||||||
withRichParameters(initialBuildParameters),
|
withRichParameters(initialBuildParameters),
|
||||||
withParameterSchemas(inactiveJobID, nil),
|
withParameterSchemas(inactiveJobID, nil),
|
||||||
withWorkspaceTags(inactiveVersionID, nil),
|
withWorkspaceTags(inactiveVersionID, nil),
|
||||||
@ -459,6 +481,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
|
|||||||
withTemplate,
|
withTemplate,
|
||||||
withInactiveVersion(richParameters),
|
withInactiveVersion(richParameters),
|
||||||
withLastBuildFound,
|
withLastBuildFound,
|
||||||
|
withTemplateVersionVariables(inactiveVersionID, nil),
|
||||||
withRichParameters(initialBuildParameters),
|
withRichParameters(initialBuildParameters),
|
||||||
withParameterSchemas(inactiveJobID, nil),
|
withParameterSchemas(inactiveJobID, nil),
|
||||||
withWorkspaceTags(inactiveVersionID, nil),
|
withWorkspaceTags(inactiveVersionID, nil),
|
||||||
@ -511,6 +534,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
|
|||||||
withTemplate,
|
withTemplate,
|
||||||
withInactiveVersion(richParameters),
|
withInactiveVersion(richParameters),
|
||||||
withLastBuildFound,
|
withLastBuildFound,
|
||||||
|
withTemplateVersionVariables(inactiveVersionID, nil),
|
||||||
withRichParameters(nil),
|
withRichParameters(nil),
|
||||||
withParameterSchemas(inactiveJobID, schemas),
|
withParameterSchemas(inactiveJobID, schemas),
|
||||||
withWorkspaceTags(inactiveVersionID, nil),
|
withWorkspaceTags(inactiveVersionID, nil),
|
||||||
@ -542,6 +566,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
|
|||||||
withTemplate,
|
withTemplate,
|
||||||
withInactiveVersion(richParameters),
|
withInactiveVersion(richParameters),
|
||||||
withLastBuildFound,
|
withLastBuildFound,
|
||||||
|
withTemplateVersionVariables(inactiveVersionID, nil),
|
||||||
withRichParameters(initialBuildParameters),
|
withRichParameters(initialBuildParameters),
|
||||||
withParameterSchemas(inactiveJobID, nil),
|
withParameterSchemas(inactiveJobID, nil),
|
||||||
withWorkspaceTags(inactiveVersionID, nil),
|
withWorkspaceTags(inactiveVersionID, nil),
|
||||||
@ -593,6 +618,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
|
|||||||
withTemplate,
|
withTemplate,
|
||||||
withActiveVersion(version2params),
|
withActiveVersion(version2params),
|
||||||
withLastBuildFound,
|
withLastBuildFound,
|
||||||
|
withTemplateVersionVariables(activeVersionID, nil),
|
||||||
withRichParameters(initialBuildParameters),
|
withRichParameters(initialBuildParameters),
|
||||||
withParameterSchemas(activeJobID, nil),
|
withParameterSchemas(activeJobID, nil),
|
||||||
withWorkspaceTags(activeVersionID, nil),
|
withWorkspaceTags(activeVersionID, nil),
|
||||||
@ -655,6 +681,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
|
|||||||
withTemplate,
|
withTemplate,
|
||||||
withActiveVersion(version2params),
|
withActiveVersion(version2params),
|
||||||
withLastBuildFound,
|
withLastBuildFound,
|
||||||
|
withTemplateVersionVariables(activeVersionID, nil),
|
||||||
withRichParameters(initialBuildParameters),
|
withRichParameters(initialBuildParameters),
|
||||||
withParameterSchemas(activeJobID, nil),
|
withParameterSchemas(activeJobID, nil),
|
||||||
withWorkspaceTags(activeVersionID, nil),
|
withWorkspaceTags(activeVersionID, nil),
|
||||||
@ -715,6 +742,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
|
|||||||
withTemplate,
|
withTemplate,
|
||||||
withActiveVersion(version2params),
|
withActiveVersion(version2params),
|
||||||
withLastBuildFound,
|
withLastBuildFound,
|
||||||
|
withTemplateVersionVariables(activeVersionID, nil),
|
||||||
withRichParameters(initialBuildParameters),
|
withRichParameters(initialBuildParameters),
|
||||||
withParameterSchemas(activeJobID, nil),
|
withParameterSchemas(activeJobID, nil),
|
||||||
withWorkspaceTags(activeVersionID, nil),
|
withWorkspaceTags(activeVersionID, nil),
|
||||||
@ -921,6 +949,18 @@ func withParameterSchemas(jobID uuid.UUID, schemas []database.ParameterSchema) f
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func withTemplateVersionVariables(versionID uuid.UUID, params []database.TemplateVersionVariable) func(mTx *dbmock.MockStore) {
|
||||||
|
return func(mTx *dbmock.MockStore) {
|
||||||
|
c := mTx.EXPECT().GetTemplateVersionVariables(gomock.Any(), versionID).
|
||||||
|
Times(1)
|
||||||
|
if len(params) > 0 {
|
||||||
|
c.Return(params, nil)
|
||||||
|
} else {
|
||||||
|
c.Return(nil, sql.ErrNoRows)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func withRichParameters(params []database.WorkspaceBuildParameter) func(mTx *dbmock.MockStore) {
|
func withRichParameters(params []database.WorkspaceBuildParameter) func(mTx *dbmock.MockStore) {
|
||||||
return func(mTx *dbmock.MockStore) {
|
return func(mTx *dbmock.MockStore) {
|
||||||
c := mTx.EXPECT().GetWorkspaceBuildParameters(gomock.Any(), lastBuildID).
|
c := mTx.EXPECT().GetWorkspaceBuildParameters(gomock.Any(), lastBuildID).
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@ -16,7 +17,8 @@ import (
|
|||||||
"github.com/moby/moby/pkg/namesgenerator"
|
"github.com/moby/moby/pkg/namesgenerator"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"golang.org/x/xerrors"
|
|
||||||
|
"cdr.dev/slog"
|
||||||
|
|
||||||
"github.com/coder/coder/v2/coderd/coderdtest"
|
"github.com/coder/coder/v2/coderd/coderdtest"
|
||||||
"github.com/coder/coder/v2/coderd/database"
|
"github.com/coder/coder/v2/coderd/database"
|
||||||
@ -28,6 +30,7 @@ import (
|
|||||||
"github.com/coder/coder/v2/enterprise/coderd/license"
|
"github.com/coder/coder/v2/enterprise/coderd/license"
|
||||||
"github.com/coder/coder/v2/enterprise/dbcrypt"
|
"github.com/coder/coder/v2/enterprise/dbcrypt"
|
||||||
"github.com/coder/coder/v2/provisioner/echo"
|
"github.com/coder/coder/v2/provisioner/echo"
|
||||||
|
"github.com/coder/coder/v2/provisioner/terraform"
|
||||||
"github.com/coder/coder/v2/provisionerd"
|
"github.com/coder/coder/v2/provisionerd"
|
||||||
provisionerdproto "github.com/coder/coder/v2/provisionerd/proto"
|
provisionerdproto "github.com/coder/coder/v2/provisionerd/proto"
|
||||||
"github.com/coder/coder/v2/provisionersdk"
|
"github.com/coder/coder/v2/provisionersdk"
|
||||||
@ -304,14 +307,31 @@ func CreateOrganization(t *testing.T, client *codersdk.Client, opts CreateOrgani
|
|||||||
return org
|
return org
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewExternalProvisionerDaemon runs an external provisioner daemon in a
|
||||||
|
// goroutine and returns a closer to stop it. The echo provisioner is used
|
||||||
|
// here. This is the default provisioner for tests and should be fine for
|
||||||
|
// most use cases. If you need to test terraform-specific behaviors, use
|
||||||
|
// NewExternalProvisionerDaemonTerraform instead.
|
||||||
func NewExternalProvisionerDaemon(t testing.TB, client *codersdk.Client, org uuid.UUID, tags map[string]string) io.Closer {
|
func NewExternalProvisionerDaemon(t testing.TB, client *codersdk.Client, org uuid.UUID, tags map[string]string) io.Closer {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
return newExternalProvisionerDaemon(t, client, org, tags, codersdk.ProvisionerTypeEcho)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewExternalProvisionerDaemonTerraform runs an external provisioner daemon in
|
||||||
|
// a goroutine and returns a closer to stop it. The terraform provisioner is
|
||||||
|
// used here. Avoid using this unless you need to test terraform-specific
|
||||||
|
// behaviors!
|
||||||
|
func NewExternalProvisionerDaemonTerraform(t testing.TB, client *codersdk.Client, org uuid.UUID, tags map[string]string) io.Closer {
|
||||||
|
t.Helper()
|
||||||
|
return newExternalProvisionerDaemon(t, client, org, tags, codersdk.ProvisionerTypeTerraform)
|
||||||
|
}
|
||||||
|
|
||||||
|
// nolint // This function is a helper for tests and should not be linted.
|
||||||
|
func newExternalProvisionerDaemon(t testing.TB, client *codersdk.Client, org uuid.UUID, tags map[string]string, provisionerType codersdk.ProvisionerType) io.Closer {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
// Without this check, the provisioner will silently fail.
|
|
||||||
entitlements, err := client.Entitlements(context.Background())
|
entitlements, err := client.Entitlements(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// AGPL instances will throw this error. They cannot use external
|
|
||||||
// provisioners.
|
|
||||||
t.Errorf("external provisioners requires a license with entitlements. The client failed to fetch the entitlements, is this an enterprise instance of coderd?")
|
t.Errorf("external provisioners requires a license with entitlements. The client failed to fetch the entitlements, is this an enterprise instance of coderd?")
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
return nil
|
return nil
|
||||||
@ -319,42 +339,67 @@ func NewExternalProvisionerDaemon(t testing.TB, client *codersdk.Client, org uui
|
|||||||
|
|
||||||
feature := entitlements.Features[codersdk.FeatureExternalProvisionerDaemons]
|
feature := entitlements.Features[codersdk.FeatureExternalProvisionerDaemons]
|
||||||
if !feature.Enabled || feature.Entitlement != codersdk.EntitlementEntitled {
|
if !feature.Enabled || feature.Entitlement != codersdk.EntitlementEntitled {
|
||||||
require.NoError(t, xerrors.Errorf("external provisioner daemons require an entitled license"))
|
t.Errorf("external provisioner daemons require an entitled license")
|
||||||
|
t.FailNow()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
echoClient, echoServer := drpc.MemTransportPipe()
|
provisionerClient, provisionerSrv := drpc.MemTransportPipe()
|
||||||
ctx, cancelFunc := context.WithCancel(context.Background())
|
ctx, cancelFunc := context.WithCancel(context.Background())
|
||||||
serveDone := make(chan struct{})
|
serveDone := make(chan struct{})
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
_ = echoClient.Close()
|
_ = provisionerClient.Close()
|
||||||
_ = echoServer.Close()
|
_ = provisionerSrv.Close()
|
||||||
cancelFunc()
|
cancelFunc()
|
||||||
<-serveDone
|
<-serveDone
|
||||||
})
|
})
|
||||||
|
|
||||||
|
switch provisionerType {
|
||||||
|
case codersdk.ProvisionerTypeTerraform:
|
||||||
|
// Ensure the Terraform binary is present in the path.
|
||||||
|
// If not, we fail this test rather than downloading it.
|
||||||
|
terraformPath, err := exec.LookPath("terraform")
|
||||||
|
require.NoError(t, err, "terraform binary not found in PATH")
|
||||||
|
t.Logf("using Terraform binary at %s", terraformPath)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
defer close(serveDone)
|
defer close(serveDone)
|
||||||
err := echo.Serve(ctx, &provisionersdk.ServeOptions{
|
assert.NoError(t, terraform.Serve(ctx, &terraform.ServeOptions{
|
||||||
Listener: echoServer,
|
BinaryPath: terraformPath,
|
||||||
|
CachePath: t.TempDir(),
|
||||||
|
ServeOptions: &provisionersdk.ServeOptions{
|
||||||
|
Listener: provisionerSrv,
|
||||||
WorkDirectory: t.TempDir(),
|
WorkDirectory: t.TempDir(),
|
||||||
})
|
},
|
||||||
assert.NoError(t, err)
|
}))
|
||||||
}()
|
}()
|
||||||
|
case codersdk.ProvisionerTypeEcho:
|
||||||
|
go func() {
|
||||||
|
defer close(serveDone)
|
||||||
|
assert.NoError(t, echo.Serve(ctx, &provisionersdk.ServeOptions{
|
||||||
|
Listener: provisionerSrv,
|
||||||
|
WorkDirectory: t.TempDir(),
|
||||||
|
}))
|
||||||
|
}()
|
||||||
|
default:
|
||||||
|
t.Fatalf("unsupported provisioner type: %s", provisionerType)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
daemon := provisionerd.New(func(ctx context.Context) (provisionerdproto.DRPCProvisionerDaemonClient, error) {
|
daemon := provisionerd.New(func(ctx context.Context) (provisionerdproto.DRPCProvisionerDaemonClient, error) {
|
||||||
return client.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{
|
return client.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{
|
||||||
ID: uuid.New(),
|
ID: uuid.New(),
|
||||||
Name: t.Name(),
|
Name: t.Name(),
|
||||||
Organization: org,
|
Organization: org,
|
||||||
Provisioners: []codersdk.ProvisionerType{codersdk.ProvisionerTypeEcho},
|
Provisioners: []codersdk.ProvisionerType{provisionerType},
|
||||||
Tags: tags,
|
Tags: tags,
|
||||||
})
|
})
|
||||||
}, &provisionerd.Options{
|
}, &provisionerd.Options{
|
||||||
Logger: testutil.Logger(t).Named("provisionerd"),
|
Logger: testutil.Logger(t).Named("provisionerd").Leveled(slog.LevelDebug),
|
||||||
UpdateInterval: 250 * time.Millisecond,
|
UpdateInterval: 250 * time.Millisecond,
|
||||||
ForceCancelInterval: 5 * time.Second,
|
ForceCancelInterval: 5 * time.Second,
|
||||||
Connector: provisionerd.LocalProvisioners{
|
Connector: provisionerd.LocalProvisioners{
|
||||||
string(database.ProvisionerTypeEcho): sdkproto.NewDRPCProvisionerClient(echoClient),
|
string(provisionerType): sdkproto.NewDRPCProvisionerClient(provisionerClient),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
closer := coderdtest.NewProvisionerDaemonCloser(daemon)
|
closer := coderdtest.NewProvisionerDaemonCloser(daemon)
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
package coderd_test
|
package coderd_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
@ -1420,6 +1422,182 @@ func TestTemplateDoesNotAllowUserAutostop(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestWorkspaceTagsTerraform tests that a workspace can be created with tags.
|
||||||
|
// This is an end-to-end-style test, meaning that we actually run the
|
||||||
|
// real Terraform provisioner and validate that the workspace is created
|
||||||
|
// successfully. The workspace itself does not specify any resources, and
|
||||||
|
// this is fine.
|
||||||
|
func TestWorkspaceTagsTerraform(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
mainTfTemplate := `
|
||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
coder = {
|
||||||
|
source = "coder/coder"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
provider "coder" {}
|
||||||
|
data "coder_workspace" "me" {}
|
||||||
|
data "coder_workspace_owner" "me" {}
|
||||||
|
%s
|
||||||
|
`
|
||||||
|
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
// tags to apply to the external provisioner
|
||||||
|
provisionerTags map[string]string
|
||||||
|
// tags to apply to the create template version request
|
||||||
|
createTemplateVersionRequestTags map[string]string
|
||||||
|
// the coder_workspace_tags bit of main.tf.
|
||||||
|
// you can add more stuff here if you need
|
||||||
|
tfWorkspaceTags string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no tags",
|
||||||
|
tfWorkspaceTags: ``,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty tags",
|
||||||
|
tfWorkspaceTags: `
|
||||||
|
data "coder_workspace_tags" "tags" {
|
||||||
|
tags = {}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "static tag",
|
||||||
|
provisionerTags: map[string]string{"foo": "bar"},
|
||||||
|
tfWorkspaceTags: `
|
||||||
|
data "coder_workspace_tags" "tags" {
|
||||||
|
tags = {
|
||||||
|
"foo" = "bar"
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tag variable",
|
||||||
|
provisionerTags: map[string]string{"foo": "bar"},
|
||||||
|
tfWorkspaceTags: `
|
||||||
|
variable "foo" {
|
||||||
|
default = "bar"
|
||||||
|
}
|
||||||
|
data "coder_workspace_tags" "tags" {
|
||||||
|
tags = {
|
||||||
|
"foo" = var.foo
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tag param",
|
||||||
|
provisionerTags: map[string]string{"foo": "bar"},
|
||||||
|
tfWorkspaceTags: `
|
||||||
|
data "coder_parameter" "foo" {
|
||||||
|
name = "foo"
|
||||||
|
type = "string"
|
||||||
|
default = "bar"
|
||||||
|
}
|
||||||
|
data "coder_workspace_tags" "tags" {
|
||||||
|
tags = {
|
||||||
|
"foo" = data.coder_parameter.foo.value
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tag param with default from var",
|
||||||
|
provisionerTags: map[string]string{"foo": "bar"},
|
||||||
|
tfWorkspaceTags: `
|
||||||
|
variable "foo" {
|
||||||
|
type = string
|
||||||
|
default = "bar"
|
||||||
|
}
|
||||||
|
data "coder_parameter" "foo" {
|
||||||
|
name = "foo"
|
||||||
|
type = "string"
|
||||||
|
default = var.foo
|
||||||
|
}
|
||||||
|
data "coder_workspace_tags" "tags" {
|
||||||
|
tags = {
|
||||||
|
"foo" = data.coder_parameter.foo.value
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "override no tags",
|
||||||
|
provisionerTags: map[string]string{"foo": "baz"},
|
||||||
|
createTemplateVersionRequestTags: map[string]string{"foo": "baz"},
|
||||||
|
tfWorkspaceTags: ``,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "override empty tags",
|
||||||
|
provisionerTags: map[string]string{"foo": "baz"},
|
||||||
|
createTemplateVersionRequestTags: map[string]string{"foo": "baz"},
|
||||||
|
tfWorkspaceTags: `
|
||||||
|
data "coder_workspace_tags" "tags" {
|
||||||
|
tags = {}
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "does not override static tag",
|
||||||
|
provisionerTags: map[string]string{"foo": "bar"},
|
||||||
|
createTemplateVersionRequestTags: map[string]string{"foo": "baz"},
|
||||||
|
tfWorkspaceTags: `
|
||||||
|
data "coder_workspace_tags" "tags" {
|
||||||
|
tags = {
|
||||||
|
"foo" = "bar"
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
tc := tc
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
ctx := testutil.Context(t, testutil.WaitSuperLong)
|
||||||
|
|
||||||
|
client, owner := coderdenttest.New(t, &coderdenttest.Options{
|
||||||
|
Options: &coderdtest.Options{
|
||||||
|
// We intentionally do not run a built-in provisioner daemon here.
|
||||||
|
IncludeProvisionerDaemon: false,
|
||||||
|
},
|
||||||
|
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||||
|
Features: license.Features{
|
||||||
|
codersdk.FeatureExternalProvisionerDaemons: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
templateAdmin, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleTemplateAdmin())
|
||||||
|
member, memberUser := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
|
||||||
|
|
||||||
|
_ = coderdenttest.NewExternalProvisionerDaemonTerraform(t, client, owner.OrganizationID, tc.provisionerTags)
|
||||||
|
|
||||||
|
// Creating a template as a template admin must succeed
|
||||||
|
templateFiles := map[string]string{"main.tf": fmt.Sprintf(mainTfTemplate, tc.tfWorkspaceTags)}
|
||||||
|
tarBytes := testutil.CreateTar(t, templateFiles)
|
||||||
|
fi, err := templateAdmin.Upload(ctx, "application/x-tar", bytes.NewReader(tarBytes))
|
||||||
|
require.NoError(t, err, "failed to upload file")
|
||||||
|
tv, err := templateAdmin.CreateTemplateVersion(ctx, owner.OrganizationID, codersdk.CreateTemplateVersionRequest{
|
||||||
|
Name: testutil.GetRandomName(t),
|
||||||
|
FileID: fi.ID,
|
||||||
|
StorageMethod: codersdk.ProvisionerStorageMethodFile,
|
||||||
|
Provisioner: codersdk.ProvisionerTypeTerraform,
|
||||||
|
ProvisionerTags: tc.createTemplateVersionRequestTags,
|
||||||
|
})
|
||||||
|
require.NoError(t, err, "failed to create template version")
|
||||||
|
coderdtest.AwaitTemplateVersionJobCompleted(t, templateAdmin, tv.ID)
|
||||||
|
tpl := coderdtest.CreateTemplate(t, templateAdmin, owner.OrganizationID, tv.ID)
|
||||||
|
|
||||||
|
// Creating a workspace as a non-privileged user must succeed
|
||||||
|
ws, err := member.CreateUserWorkspace(ctx, memberUser.Username, codersdk.CreateWorkspaceRequest{
|
||||||
|
TemplateID: tpl.ID,
|
||||||
|
Name: coderdtest.RandomUsername(t),
|
||||||
|
})
|
||||||
|
require.NoError(t, err, "failed to create workspace")
|
||||||
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, member, ws.LatestBuild.ID)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Blocked by autostart requirements
|
// Blocked by autostart requirements
|
||||||
func TestExecutorAutostartBlocked(t *testing.T) {
|
func TestExecutorAutostartBlocked(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
@ -327,13 +327,13 @@ func (p *Parser) CoderParameterDefaults(ctx context.Context, varsDefaults map[st
|
|||||||
// Issue #15795: the "default" value could also be an expression we need
|
// Issue #15795: the "default" value could also be an expression we need
|
||||||
// to evaluate.
|
// to evaluate.
|
||||||
// TODO: should we support coder_parameter default values that reference other coder_parameter data sources?
|
// TODO: should we support coder_parameter default values that reference other coder_parameter data sources?
|
||||||
evalCtx := buildEvalContext(varsDefaults, nil)
|
evalCtx := BuildEvalContext(varsDefaults, nil)
|
||||||
val, diags := expr.Value(evalCtx)
|
val, diags := expr.Value(evalCtx)
|
||||||
if diags.HasErrors() {
|
if diags.HasErrors() {
|
||||||
return nil, xerrors.Errorf("failed to evaluate coder_parameter %q default value %q: %s", dataResource.Name, value, diags.Error())
|
return nil, xerrors.Errorf("failed to evaluate coder_parameter %q default value %q: %s", dataResource.Name, value, diags.Error())
|
||||||
}
|
}
|
||||||
// Do not use "val.AsString()" as it can panic
|
// Do not use "val.AsString()" as it can panic
|
||||||
strVal, err := ctyValueString(val)
|
strVal, err := CtyValueString(val)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, xerrors.Errorf("failed to marshal coder_parameter %q default value %q as string: %s", dataResource.Name, value, err)
|
return nil, xerrors.Errorf("failed to marshal coder_parameter %q default value %q as string: %s", dataResource.Name, value, err)
|
||||||
}
|
}
|
||||||
@ -355,7 +355,7 @@ func evaluateWorkspaceTags(varsDefaults, paramsDefaults, workspaceTags map[strin
|
|||||||
}
|
}
|
||||||
// We only add variables and coder_parameter data sources. Anything else will be
|
// We only add variables and coder_parameter data sources. Anything else will be
|
||||||
// undefined and will raise a Terraform error.
|
// undefined and will raise a Terraform error.
|
||||||
evalCtx := buildEvalContext(varsDefaults, paramsDefaults)
|
evalCtx := BuildEvalContext(varsDefaults, paramsDefaults)
|
||||||
tags := make(map[string]string)
|
tags := make(map[string]string)
|
||||||
for workspaceTagKey, workspaceTagValue := range workspaceTags {
|
for workspaceTagKey, workspaceTagValue := range workspaceTags {
|
||||||
expr, diags := hclsyntax.ParseExpression([]byte(workspaceTagValue), "expression.hcl", hcl.InitialPos)
|
expr, diags := hclsyntax.ParseExpression([]byte(workspaceTagValue), "expression.hcl", hcl.InitialPos)
|
||||||
@ -369,7 +369,7 @@ func evaluateWorkspaceTags(varsDefaults, paramsDefaults, workspaceTags map[strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Do not use "val.AsString()" as it can panic
|
// Do not use "val.AsString()" as it can panic
|
||||||
str, err := ctyValueString(val)
|
str, err := CtyValueString(val)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, xerrors.Errorf("failed to marshal workspace tag key %q value %q as string: %s", workspaceTagKey, workspaceTagValue, err)
|
return nil, xerrors.Errorf("failed to marshal workspace tag key %q value %q as string: %s", workspaceTagKey, workspaceTagValue, err)
|
||||||
}
|
}
|
||||||
@ -395,16 +395,17 @@ func validWorkspaceTagValues(tags map[string]string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildEvalContext(varDefaults map[string]string, paramDefaults map[string]string) *hcl.EvalContext {
|
// BuildEvalContext builds an evaluation context for the given variable and parameter defaults.
|
||||||
|
func BuildEvalContext(vars map[string]string, params map[string]string) *hcl.EvalContext {
|
||||||
varDefaultsM := map[string]cty.Value{}
|
varDefaultsM := map[string]cty.Value{}
|
||||||
for varName, varDefault := range varDefaults {
|
for varName, varDefault := range vars {
|
||||||
varDefaultsM[varName] = cty.MapVal(map[string]cty.Value{
|
varDefaultsM[varName] = cty.MapVal(map[string]cty.Value{
|
||||||
"value": cty.StringVal(varDefault),
|
"value": cty.StringVal(varDefault),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
paramDefaultsM := map[string]cty.Value{}
|
paramDefaultsM := map[string]cty.Value{}
|
||||||
for paramName, paramDefault := range paramDefaults {
|
for paramName, paramDefault := range params {
|
||||||
paramDefaultsM[paramName] = cty.MapVal(map[string]cty.Value{
|
paramDefaultsM[paramName] = cty.MapVal(map[string]cty.Value{
|
||||||
"value": cty.StringVal(paramDefault),
|
"value": cty.StringVal(paramDefault),
|
||||||
})
|
})
|
||||||
@ -496,7 +497,10 @@ func compareSourcePos(x, y tfconfig.SourcePos) bool {
|
|||||||
return x.Line < y.Line
|
return x.Line < y.Line
|
||||||
}
|
}
|
||||||
|
|
||||||
func ctyValueString(val cty.Value) (string, error) {
|
// CtyValueString converts a cty.Value to a string.
|
||||||
|
// It supports only primitive types - bool, number, and string.
|
||||||
|
// As a special case, it also supports map[string]interface{} with key "value".
|
||||||
|
func CtyValueString(val cty.Value) (string, error) {
|
||||||
switch val.Type() {
|
switch val.Type() {
|
||||||
case cty.Bool:
|
case cty.Bool:
|
||||||
if val.True() {
|
if val.True() {
|
||||||
@ -514,7 +518,7 @@ func ctyValueString(val cty.Value) (string, error) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
return "", xerrors.Errorf("map does not have key 'value'")
|
return "", xerrors.Errorf("map does not have key 'value'")
|
||||||
}
|
}
|
||||||
return ctyValueString(valval)
|
return CtyValueString(valval)
|
||||||
default:
|
default:
|
||||||
return "", xerrors.Errorf("only primitive types are supported - bool, number, and string")
|
return "", xerrors.Errorf("only primitive types are supported - bool, number, and string")
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user