mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat(provisioner/terraform/tfparse): implement WorkspaceTagDefaultsFromFile (#15236)
Relates to https://github.com/coder/coder/issues/15087 and https://github.com/coder/coder/issues/15427 Adds functionality to `provisioner/terraform/tfparse` to extract the default values for a `coder_workspace_tags` data source from a given file.
This commit is contained in:
@@ -21,17 +21,17 @@ func (s *server) Parse(sess *provisionersdk.Session, _ *proto.ParseRequest, _ <-
|
||||
defer span.End()
|
||||
|
||||
// Load the module and print any parse errors.
|
||||
module, diags := tfconfig.LoadModule(sess.WorkDirectory)
|
||||
parser, diags := tfparse.New(sess.WorkDirectory, tfparse.WithLogger(s.logger.Named("tfparse")))
|
||||
if diags.HasErrors() {
|
||||
return provisionersdk.ParseErrorf("load module: %s", formatDiagnostics(sess.WorkDirectory, diags))
|
||||
}
|
||||
|
||||
workspaceTags, err := tfparse.WorkspaceTags(ctx, s.logger, module)
|
||||
workspaceTags, err := parser.WorkspaceTags(ctx)
|
||||
if err != nil {
|
||||
return provisionersdk.ParseErrorf("can't load workspace tags: %v", err)
|
||||
}
|
||||
|
||||
templateVariables, err := tfparse.LoadTerraformVariables(module)
|
||||
templateVariables, err := parser.TemplateVariables()
|
||||
if err != nil {
|
||||
return provisionersdk.ParseErrorf("can't load template variables: %v", err)
|
||||
}
|
||||
|
||||
@@ -1,182 +0,0 @@
|
||||
package tfparse
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/coder/coder/v2/provisionersdk/proto"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/hclparse"
|
||||
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||
"github.com/hashicorp/terraform-config-inspect/tfconfig"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"cdr.dev/slog"
|
||||
)
|
||||
|
||||
// WorkspaceTags extracts tags from coder_workspace_tags data sources defined in module.
|
||||
func WorkspaceTags(ctx context.Context, logger slog.Logger, module *tfconfig.Module) (map[string]string, error) {
|
||||
workspaceTags := map[string]string{}
|
||||
|
||||
for _, dataResource := range module.DataResources {
|
||||
if dataResource.Type != "coder_workspace_tags" {
|
||||
logger.Debug(ctx, "skip resource as it is not a coder_workspace_tags", "resource_name", dataResource.Name, "resource_type", dataResource.Type)
|
||||
continue
|
||||
}
|
||||
|
||||
var file *hcl.File
|
||||
var diags hcl.Diagnostics
|
||||
parser := hclparse.NewParser()
|
||||
|
||||
if !strings.HasSuffix(dataResource.Pos.Filename, ".tf") {
|
||||
logger.Debug(ctx, "only .tf files can be parsed", "filename", dataResource.Pos.Filename)
|
||||
continue
|
||||
}
|
||||
// We know in which HCL file is the data resource defined.
|
||||
file, diags = parser.ParseHCLFile(dataResource.Pos.Filename)
|
||||
if diags.HasErrors() {
|
||||
return nil, xerrors.Errorf("can't parse the resource file: %s", diags.Error())
|
||||
}
|
||||
|
||||
// Parse root to find "coder_workspace_tags".
|
||||
content, _, diags := file.Body.PartialContent(rootTemplateSchema)
|
||||
if diags.HasErrors() {
|
||||
return nil, xerrors.Errorf("can't parse the resource file: %s", diags.Error())
|
||||
}
|
||||
|
||||
// Iterate over blocks to locate the exact "coder_workspace_tags" data resource.
|
||||
for _, block := range content.Blocks {
|
||||
if !slices.Equal(block.Labels, []string{"coder_workspace_tags", dataResource.Name}) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse "coder_workspace_tags" to find all key-value tags.
|
||||
resContent, _, diags := block.Body.PartialContent(coderWorkspaceTagsSchema)
|
||||
if diags.HasErrors() {
|
||||
return nil, xerrors.Errorf(`can't parse the resource coder_workspace_tags: %s`, diags.Error())
|
||||
}
|
||||
|
||||
if resContent == nil {
|
||||
continue // workspace tags are not present
|
||||
}
|
||||
|
||||
if _, ok := resContent.Attributes["tags"]; !ok {
|
||||
return nil, xerrors.Errorf(`"tags" attribute is required by coder_workspace_tags`)
|
||||
}
|
||||
|
||||
expr := resContent.Attributes["tags"].Expr
|
||||
tagsExpr, ok := expr.(*hclsyntax.ObjectConsExpr)
|
||||
if !ok {
|
||||
return nil, xerrors.Errorf(`"tags" attribute is expected to be a key-value map`)
|
||||
}
|
||||
|
||||
// Parse key-value entries in "coder_workspace_tags"
|
||||
for _, tagItem := range tagsExpr.Items {
|
||||
key, err := previewFileContent(tagItem.KeyExpr.Range())
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("can't preview the resource file: %v", err)
|
||||
}
|
||||
key = strings.Trim(key, `"`)
|
||||
|
||||
value, err := previewFileContent(tagItem.ValueExpr.Range())
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("can't preview the resource file: %v", err)
|
||||
}
|
||||
|
||||
logger.Info(ctx, "workspace tag found", "key", key, "value", value)
|
||||
|
||||
if _, ok := workspaceTags[key]; ok {
|
||||
return nil, xerrors.Errorf(`workspace tag %q is defined multiple times`, key)
|
||||
}
|
||||
workspaceTags[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
return workspaceTags, nil
|
||||
}
|
||||
|
||||
var rootTemplateSchema = &hcl.BodySchema{
|
||||
Blocks: []hcl.BlockHeaderSchema{
|
||||
{
|
||||
Type: "data",
|
||||
LabelNames: []string{"type", "name"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var coderWorkspaceTagsSchema = &hcl.BodySchema{
|
||||
Attributes: []hcl.AttributeSchema{
|
||||
{
|
||||
Name: "tags",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func previewFileContent(fileRange hcl.Range) (string, error) {
|
||||
body, err := os.ReadFile(fileRange.Filename)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(fileRange.SliceBytes(body)), nil
|
||||
}
|
||||
|
||||
// LoadTerraformVariables extracts all Terraform variables from module and converts them
|
||||
// to template variables. The variables are sorted by source position.
|
||||
func LoadTerraformVariables(module *tfconfig.Module) ([]*proto.TemplateVariable, error) {
|
||||
// Sort variables by (filename, line) to make the ordering consistent
|
||||
variables := make([]*tfconfig.Variable, 0, len(module.Variables))
|
||||
for _, v := range module.Variables {
|
||||
variables = append(variables, v)
|
||||
}
|
||||
sort.Slice(variables, func(i, j int) bool {
|
||||
return compareSourcePos(variables[i].Pos, variables[j].Pos)
|
||||
})
|
||||
|
||||
var templateVariables []*proto.TemplateVariable
|
||||
for _, v := range variables {
|
||||
mv, err := convertTerraformVariable(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
templateVariables = append(templateVariables, mv)
|
||||
}
|
||||
return templateVariables, nil
|
||||
}
|
||||
|
||||
// convertTerraformVariable converts a Terraform variable to a template-wide variable, processed by Coder.
|
||||
func convertTerraformVariable(variable *tfconfig.Variable) (*proto.TemplateVariable, error) {
|
||||
var defaultData string
|
||||
if variable.Default != nil {
|
||||
var valid bool
|
||||
defaultData, valid = variable.Default.(string)
|
||||
if !valid {
|
||||
defaultDataRaw, err := json.Marshal(variable.Default)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("parse variable %q default: %w", variable.Name, err)
|
||||
}
|
||||
defaultData = string(defaultDataRaw)
|
||||
}
|
||||
}
|
||||
|
||||
return &proto.TemplateVariable{
|
||||
Name: variable.Name,
|
||||
Description: variable.Description,
|
||||
Type: variable.Type,
|
||||
DefaultValue: defaultData,
|
||||
// variable.Required is always false. Empty string is a valid default value, so it doesn't enforce required to be "true".
|
||||
Required: variable.Default == nil,
|
||||
Sensitive: variable.Sensitive,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func compareSourcePos(x, y tfconfig.SourcePos) bool {
|
||||
if x.Filename != y.Filename {
|
||||
return x.Filename < y.Filename
|
||||
}
|
||||
return x.Line < y.Line
|
||||
}
|
||||
@@ -0,0 +1,526 @@
|
||||
package tfparse
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"os"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/coder/coder/v2/archive"
|
||||
"github.com/coder/coder/v2/provisionersdk"
|
||||
"github.com/coder/coder/v2/provisionersdk/proto"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/hclparse"
|
||||
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||
"github.com/hashicorp/terraform-config-inspect/tfconfig"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
"golang.org/x/exp/maps"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"cdr.dev/slog"
|
||||
)
|
||||
|
||||
// NOTE: This is duplicated from coderd but we can't import it here without
|
||||
// introducing a circular dependency
|
||||
const maxFileSizeBytes = 10 * (10 << 20) // 10 MB
|
||||
|
||||
// parseHCLFiler is the actual interface of *hclparse.Parser we use
|
||||
// to parse HCL. This is extracted to an interface so we can more
|
||||
// easily swap this out for an alternative implementation later on.
|
||||
type parseHCLFiler interface {
|
||||
ParseHCLFile(filename string) (*hcl.File, hcl.Diagnostics)
|
||||
}
|
||||
|
||||
// Parser parses a Terraform module on disk.
|
||||
type Parser struct {
|
||||
logger slog.Logger
|
||||
underlying parseHCLFiler
|
||||
module *tfconfig.Module
|
||||
workdir string
|
||||
}
|
||||
|
||||
// Option is an option for a new instance of Parser.
|
||||
type Option func(*Parser)
|
||||
|
||||
// WithLogger sets the logger to be used by Parser
|
||||
func WithLogger(logger slog.Logger) Option {
|
||||
return func(p *Parser) {
|
||||
p.logger = logger
|
||||
}
|
||||
}
|
||||
|
||||
// New returns a new instance of Parser, as well as any diagnostics
|
||||
// encountered while parsing the module.
|
||||
func New(workdir string, opts ...Option) (*Parser, tfconfig.Diagnostics) {
|
||||
p := Parser{
|
||||
logger: slog.Make(),
|
||||
underlying: hclparse.NewParser(),
|
||||
workdir: workdir,
|
||||
module: nil,
|
||||
}
|
||||
for _, o := range opts {
|
||||
o(&p)
|
||||
}
|
||||
|
||||
var diags tfconfig.Diagnostics
|
||||
if p.module == nil {
|
||||
m, ds := tfconfig.LoadModule(workdir)
|
||||
diags = ds
|
||||
p.module = m
|
||||
}
|
||||
|
||||
return &p, diags
|
||||
}
|
||||
|
||||
// WorkspaceTags looks for all coder_workspace_tags datasource in the module
|
||||
// and returns the raw values for the tags. Use
|
||||
func (p *Parser) WorkspaceTags(ctx context.Context) (map[string]string, error) {
|
||||
tags := map[string]string{}
|
||||
var skipped []string
|
||||
for _, dataResource := range p.module.DataResources {
|
||||
if dataResource.Type != "coder_workspace_tags" {
|
||||
skipped = append(skipped, strings.Join([]string{"data", dataResource.Type, dataResource.Name}, "."))
|
||||
continue
|
||||
}
|
||||
|
||||
var file *hcl.File
|
||||
var diags hcl.Diagnostics
|
||||
|
||||
if !strings.HasSuffix(dataResource.Pos.Filename, ".tf") {
|
||||
continue
|
||||
}
|
||||
// We know in which HCL file is the data resource defined.
|
||||
file, diags = p.underlying.ParseHCLFile(dataResource.Pos.Filename)
|
||||
if diags.HasErrors() {
|
||||
return nil, xerrors.Errorf("can't parse the resource file: %s", diags.Error())
|
||||
}
|
||||
|
||||
// Parse root to find "coder_workspace_tags".
|
||||
content, _, diags := file.Body.PartialContent(rootTemplateSchema)
|
||||
if diags.HasErrors() {
|
||||
return nil, xerrors.Errorf("can't parse the resource file: %s", diags.Error())
|
||||
}
|
||||
|
||||
// Iterate over blocks to locate the exact "coder_workspace_tags" data resource.
|
||||
for _, block := range content.Blocks {
|
||||
if !slices.Equal(block.Labels, []string{"coder_workspace_tags", dataResource.Name}) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse "coder_workspace_tags" to find all key-value tags.
|
||||
resContent, _, diags := block.Body.PartialContent(coderWorkspaceTagsSchema)
|
||||
if diags.HasErrors() {
|
||||
return nil, xerrors.Errorf(`can't parse the resource coder_workspace_tags: %s`, diags.Error())
|
||||
}
|
||||
|
||||
if resContent == nil {
|
||||
continue // workspace tags are not present
|
||||
}
|
||||
|
||||
if _, ok := resContent.Attributes["tags"]; !ok {
|
||||
return nil, xerrors.Errorf(`"tags" attribute is required by coder_workspace_tags`)
|
||||
}
|
||||
|
||||
expr := resContent.Attributes["tags"].Expr
|
||||
tagsExpr, ok := expr.(*hclsyntax.ObjectConsExpr)
|
||||
if !ok {
|
||||
return nil, xerrors.Errorf(`"tags" attribute is expected to be a key-value map`)
|
||||
}
|
||||
|
||||
// Parse key-value entries in "coder_workspace_tags"
|
||||
for _, tagItem := range tagsExpr.Items {
|
||||
key, err := previewFileContent(tagItem.KeyExpr.Range())
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("can't preview the resource file: %v", err)
|
||||
}
|
||||
key = strings.Trim(key, `"`)
|
||||
|
||||
value, err := previewFileContent(tagItem.ValueExpr.Range())
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("can't preview the resource file: %v", err)
|
||||
}
|
||||
|
||||
if _, ok := tags[key]; ok {
|
||||
return nil, xerrors.Errorf(`workspace tag %q is defined multiple times`, key)
|
||||
}
|
||||
tags[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
p.logger.Debug(ctx, "found workspace tags", slog.F("tags", maps.Keys(tags)), slog.F("skipped", skipped))
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
func (p *Parser) WorkspaceTagDefaults(ctx context.Context) (map[string]string, error) {
|
||||
// This only gets us the expressions. We need to evaluate them.
|
||||
// Example: var.region -> "us"
|
||||
tags, err := p.WorkspaceTags(ctx)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("extract workspace tags: %w", err)
|
||||
}
|
||||
|
||||
// To evaluate the expressions, we need to load the default values for
|
||||
// variables and parameters.
|
||||
varsDefaults, err := p.VariableDefaults(ctx)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("load variable defaults: %w", err)
|
||||
}
|
||||
paramsDefaults, err := p.CoderParameterDefaults(ctx)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("load parameter defaults: %w", err)
|
||||
}
|
||||
|
||||
// Evaluate the tags expressions given the inputs.
|
||||
// This will resolve any variables or parameters to their default
|
||||
// values.
|
||||
evalTags, err := evaluateWorkspaceTags(varsDefaults, paramsDefaults, tags)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("eval provisioner tags: %w", err)
|
||||
}
|
||||
|
||||
// Ensure that none of the tag values are empty after evaluation.
|
||||
for k, v := range evalTags {
|
||||
if len(strings.TrimSpace(v)) > 0 {
|
||||
continue
|
||||
}
|
||||
return nil, xerrors.Errorf("provisioner tag %q evaluated to an empty value, please set a default value", k)
|
||||
}
|
||||
return evalTags, nil
|
||||
}
|
||||
|
||||
// TemplateVariables returns all of the Terraform variables in the module
|
||||
// as TemplateVariables.
|
||||
func (p *Parser) TemplateVariables() ([]*proto.TemplateVariable, error) {
|
||||
// Sort variables by (filename, line) to make the ordering consistent
|
||||
variables := make([]*tfconfig.Variable, 0, len(p.module.Variables))
|
||||
for _, v := range p.module.Variables {
|
||||
variables = append(variables, v)
|
||||
}
|
||||
sort.Slice(variables, func(i, j int) bool {
|
||||
return compareSourcePos(variables[i].Pos, variables[j].Pos)
|
||||
})
|
||||
|
||||
var templateVariables []*proto.TemplateVariable
|
||||
for _, v := range variables {
|
||||
mv, err := convertTerraformVariable(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
templateVariables = append(templateVariables, mv)
|
||||
}
|
||||
return templateVariables, nil
|
||||
}
|
||||
|
||||
// WriteArchive is a helper function to write a in-memory archive
|
||||
// with the given mimetype to disk. Only zip and tar archives
|
||||
// are currently supported.
|
||||
func WriteArchive(bs []byte, mimetype string, path string) error {
|
||||
// Check if we need to convert the file first!
|
||||
var rdr io.Reader
|
||||
switch mimetype {
|
||||
case "application/x-tar":
|
||||
rdr = bytes.NewReader(bs)
|
||||
case "application/zip":
|
||||
if zr, err := zip.NewReader(bytes.NewReader(bs), int64(len(bs))); err != nil {
|
||||
return xerrors.Errorf("read zip file: %w", err)
|
||||
} else if tarBytes, err := archive.CreateTarFromZip(zr, maxFileSizeBytes); err != nil {
|
||||
return xerrors.Errorf("convert zip to tar: %w", err)
|
||||
} else {
|
||||
rdr = bytes.NewReader(tarBytes)
|
||||
}
|
||||
default:
|
||||
return xerrors.Errorf("unsupported mimetype: %s", mimetype)
|
||||
}
|
||||
|
||||
// Untar the file into the temporary directory
|
||||
if err := provisionersdk.Untar(path, rdr); err != nil {
|
||||
return xerrors.Errorf("untar: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// VariableDefaults returns the default values for all variables passed to it.
|
||||
func (p *Parser) VariableDefaults(ctx context.Context) (map[string]string, error) {
|
||||
// iterate through vars to get the default values for all
|
||||
// variables.
|
||||
m := make(map[string]string)
|
||||
for _, v := range p.module.Variables {
|
||||
if v == nil {
|
||||
continue
|
||||
}
|
||||
sv, err := interfaceToString(v.Default)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("can't convert variable default value to string: %v", err)
|
||||
}
|
||||
m[v.Name] = strings.Trim(sv, `"`)
|
||||
}
|
||||
p.logger.Debug(ctx, "found default values for variables", slog.F("defaults", m))
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// CoderParameterDefaults returns the default values of all coder_parameter data sources
|
||||
// in the parsed module.
|
||||
func (p *Parser) CoderParameterDefaults(ctx context.Context) (map[string]string, error) {
|
||||
defaultsM := make(map[string]string)
|
||||
var (
|
||||
skipped []string
|
||||
file *hcl.File
|
||||
diags hcl.Diagnostics
|
||||
)
|
||||
|
||||
for _, dataResource := range p.module.DataResources {
|
||||
if dataResource == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if dataResource.Type != "coder_parameter" {
|
||||
skipped = append(skipped, strings.Join([]string{"data", dataResource.Type, dataResource.Name}, "."))
|
||||
continue
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(dataResource.Pos.Filename, ".tf") {
|
||||
continue
|
||||
}
|
||||
|
||||
// We know in which HCL file is the data resource defined.
|
||||
// NOTE: hclparse.Parser will cache multiple successive calls to parse the same file.
|
||||
file, diags = p.underlying.ParseHCLFile(dataResource.Pos.Filename)
|
||||
if diags.HasErrors() {
|
||||
return nil, xerrors.Errorf("can't parse the resource file %q: %s", dataResource.Pos.Filename, diags.Error())
|
||||
}
|
||||
|
||||
// Parse root to find "coder_parameter".
|
||||
content, _, diags := file.Body.PartialContent(rootTemplateSchema)
|
||||
if diags.HasErrors() {
|
||||
return nil, xerrors.Errorf("can't parse the resource file: %s", diags.Error())
|
||||
}
|
||||
|
||||
// Iterate over blocks to locate the exact "coder_parameter" data resource.
|
||||
for _, block := range content.Blocks {
|
||||
if !slices.Equal(block.Labels, []string{"coder_parameter", dataResource.Name}) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse "coder_parameter" to find the default value.
|
||||
resContent, _, diags := block.Body.PartialContent(coderParameterSchema)
|
||||
if diags.HasErrors() {
|
||||
return nil, xerrors.Errorf(`can't parse the coder_parameter: %s`, diags.Error())
|
||||
}
|
||||
|
||||
if _, ok := resContent.Attributes["default"]; !ok {
|
||||
defaultsM[dataResource.Name] = ""
|
||||
} else {
|
||||
expr := resContent.Attributes["default"].Expr
|
||||
value, err := previewFileContent(expr.Range())
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("can't preview the resource file: %v", err)
|
||||
}
|
||||
defaultsM[dataResource.Name] = strings.Trim(value, `"`)
|
||||
}
|
||||
}
|
||||
}
|
||||
p.logger.Debug(ctx, "found default values for parameters", slog.F("defaults", defaultsM), slog.F("skipped", skipped))
|
||||
return defaultsM, nil
|
||||
}
|
||||
|
||||
// evaluateWorkspaceTags evaluates the given workspaceTags based on the given
|
||||
// default values for variables and coder_parameter data sources.
|
||||
func evaluateWorkspaceTags(varsDefaults, paramsDefaults, workspaceTags map[string]string) (map[string]string, error) {
|
||||
// Filter only allowed data sources for preflight check.
|
||||
// This is not strictly required but provides a friendlier error.
|
||||
if err := validWorkspaceTagValues(workspaceTags); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// We only add variables and coder_parameter data sources. Anything else will be
|
||||
// undefined and will raise a Terraform error.
|
||||
evalCtx := buildEvalContext(varsDefaults, paramsDefaults)
|
||||
tags := make(map[string]string)
|
||||
for workspaceTagKey, workspaceTagValue := range workspaceTags {
|
||||
expr, diags := hclsyntax.ParseExpression([]byte(workspaceTagValue), "expression.hcl", hcl.InitialPos)
|
||||
if diags.HasErrors() {
|
||||
return nil, xerrors.Errorf("failed to parse workspace tag key %q value %q: %s", workspaceTagKey, workspaceTagValue, diags.Error())
|
||||
}
|
||||
|
||||
val, diags := expr.Value(evalCtx)
|
||||
if diags.HasErrors() {
|
||||
return nil, xerrors.Errorf("failed to evaluate workspace tag key %q value %q: %s", workspaceTagKey, workspaceTagValue, diags.Error())
|
||||
}
|
||||
|
||||
// Do not use "val.AsString()" as it can panic
|
||||
str, err := ctyValueString(val)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("failed to marshal workspace tag key %q value %q as string: %s", workspaceTagKey, workspaceTagValue, err)
|
||||
}
|
||||
tags[workspaceTagKey] = str
|
||||
}
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
// validWorkspaceTagValues returns an error if any value of the given tags map
|
||||
// evaluates to a datasource other than "coder_parameter".
|
||||
// This only serves to provide a friendly error if a user attempts to reference
|
||||
// a data source other than "coder_parameter" in "coder_workspace_tags".
|
||||
func validWorkspaceTagValues(tags map[string]string) error {
|
||||
for _, v := range tags {
|
||||
parts := strings.SplitN(v, ".", 3)
|
||||
if len(parts) != 3 {
|
||||
continue
|
||||
}
|
||||
if parts[0] == "data" && parts[1] != "coder_parameter" {
|
||||
return xerrors.Errorf("invalid workspace tag value %q: only the \"coder_parameter\" data source is supported here", v)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildEvalContext(varDefaults map[string]string, paramDefaults map[string]string) *hcl.EvalContext {
|
||||
varDefaultsM := map[string]cty.Value{}
|
||||
for varName, varDefault := range varDefaults {
|
||||
varDefaultsM[varName] = cty.MapVal(map[string]cty.Value{
|
||||
"value": cty.StringVal(varDefault),
|
||||
})
|
||||
}
|
||||
|
||||
paramDefaultsM := map[string]cty.Value{}
|
||||
for paramName, paramDefault := range paramDefaults {
|
||||
paramDefaultsM[paramName] = cty.MapVal(map[string]cty.Value{
|
||||
"value": cty.StringVal(paramDefault),
|
||||
})
|
||||
}
|
||||
|
||||
evalCtx := &hcl.EvalContext{
|
||||
Variables: map[string]cty.Value{},
|
||||
// NOTE: we do not currently support function execution here.
|
||||
// The default function map for Terraform is not exposed, so we would essentially
|
||||
// have to re-implement or copy the entire map or a subset thereof.
|
||||
// ref: https://github.com/hashicorp/terraform/blob/e044e569c5bc81f82e9a4d7891f37c6fbb0a8a10/internal/lang/functions.go#L54
|
||||
Functions: nil,
|
||||
}
|
||||
if len(varDefaultsM) != 0 {
|
||||
evalCtx.Variables["var"] = cty.MapVal(varDefaultsM)
|
||||
}
|
||||
if len(paramDefaultsM) != 0 {
|
||||
evalCtx.Variables["data"] = cty.MapVal(map[string]cty.Value{
|
||||
"coder_parameter": cty.MapVal(paramDefaultsM),
|
||||
})
|
||||
}
|
||||
|
||||
return evalCtx
|
||||
}
|
||||
|
||||
var rootTemplateSchema = &hcl.BodySchema{
|
||||
Blocks: []hcl.BlockHeaderSchema{
|
||||
{
|
||||
Type: "data",
|
||||
LabelNames: []string{"type", "name"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var coderWorkspaceTagsSchema = &hcl.BodySchema{
|
||||
Attributes: []hcl.AttributeSchema{
|
||||
{
|
||||
Name: "tags",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var coderParameterSchema = &hcl.BodySchema{
|
||||
Attributes: []hcl.AttributeSchema{
|
||||
{
|
||||
Name: "default",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func previewFileContent(fileRange hcl.Range) (string, error) {
|
||||
body, err := os.ReadFile(fileRange.Filename)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(fileRange.SliceBytes(body)), nil
|
||||
}
|
||||
|
||||
// convertTerraformVariable converts a Terraform variable to a template-wide variable, processed by Coder.
|
||||
func convertTerraformVariable(variable *tfconfig.Variable) (*proto.TemplateVariable, error) {
|
||||
var defaultData string
|
||||
if variable.Default != nil {
|
||||
var valid bool
|
||||
defaultData, valid = variable.Default.(string)
|
||||
if !valid {
|
||||
defaultDataRaw, err := json.Marshal(variable.Default)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("parse variable %q default: %w", variable.Name, err)
|
||||
}
|
||||
defaultData = string(defaultDataRaw)
|
||||
}
|
||||
}
|
||||
|
||||
return &proto.TemplateVariable{
|
||||
Name: variable.Name,
|
||||
Description: variable.Description,
|
||||
Type: variable.Type,
|
||||
DefaultValue: defaultData,
|
||||
// variable.Required is always false. Empty string is a valid default value, so it doesn't enforce required to be "true".
|
||||
Required: variable.Default == nil,
|
||||
Sensitive: variable.Sensitive,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func compareSourcePos(x, y tfconfig.SourcePos) bool {
|
||||
if x.Filename != y.Filename {
|
||||
return x.Filename < y.Filename
|
||||
}
|
||||
return x.Line < y.Line
|
||||
}
|
||||
|
||||
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
|
||||
// We may also have a map[string]interface{} with key "value".
|
||||
case cty.Map(cty.String):
|
||||
valval, ok := val.AsValueMap()["value"]
|
||||
if !ok {
|
||||
return "", xerrors.Errorf("map does not have key 'value'")
|
||||
}
|
||||
return ctyValueString(valval)
|
||||
default:
|
||||
return "", xerrors.Errorf("only primitive types are supported - bool, number, and string")
|
||||
}
|
||||
}
|
||||
|
||||
func interfaceToString(i interface{}) (string, error) {
|
||||
switch v := i.(type) {
|
||||
case nil:
|
||||
return "", nil
|
||||
case string:
|
||||
return v, nil
|
||||
case []byte:
|
||||
return string(v), nil
|
||||
case int:
|
||||
return strconv.FormatInt(int64(v), 10), nil
|
||||
case float64:
|
||||
return strconv.FormatFloat(v, 'f', -1, 64), nil
|
||||
case bool:
|
||||
return strconv.FormatBool(v), nil
|
||||
default:
|
||||
return "", xerrors.Errorf("unsupported type %T", v)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,500 @@
|
||||
package tfparse_test
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"log"
|
||||
"testing"
|
||||
|
||||
"cdr.dev/slog"
|
||||
"cdr.dev/slog/sloggers/sloghuman"
|
||||
"cdr.dev/slog/sloggers/slogtest"
|
||||
"github.com/coder/coder/v2/archive"
|
||||
"github.com/coder/coder/v2/provisioner/terraform/tfparse"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_WorkspaceTagDefaultsFromFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
files map[string]string
|
||||
expectTags map[string]string
|
||||
expectError string
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
files: map[string]string{},
|
||||
expectTags: map[string]string{},
|
||||
expectError: "",
|
||||
},
|
||||
{
|
||||
name: "single text file",
|
||||
files: map[string]string{
|
||||
"file.txt": `
|
||||
hello world`,
|
||||
},
|
||||
expectTags: map[string]string{},
|
||||
expectError: "",
|
||||
},
|
||||
{
|
||||
name: "main.tf with no workspace_tags",
|
||||
files: map[string]string{
|
||||
"main.tf": `
|
||||
provider "foo" {}
|
||||
resource "foo_bar" "baz" {}
|
||||
variable "region" {
|
||||
type = string
|
||||
default = "us"
|
||||
}
|
||||
data "base" "ours" {
|
||||
all = true
|
||||
}
|
||||
data "coder_parameter" "az" {
|
||||
name = "az"
|
||||
type = "string"
|
||||
default = "a"
|
||||
}`,
|
||||
},
|
||||
expectTags: map[string]string{},
|
||||
expectError: "",
|
||||
},
|
||||
{
|
||||
name: "main.tf with empty workspace tags",
|
||||
files: map[string]string{
|
||||
"main.tf": `
|
||||
provider "foo" {}
|
||||
resource "foo_bar" "baz" {}
|
||||
variable "region" {
|
||||
type = string
|
||||
default = "us"
|
||||
}
|
||||
data "base" "ours" {
|
||||
all = true
|
||||
}
|
||||
data "coder_parameter" "az" {
|
||||
name = "az"
|
||||
type = "string"
|
||||
default = "a"
|
||||
}
|
||||
data "coder_workspace_tags" "tags" {}`,
|
||||
},
|
||||
expectTags: map[string]string{},
|
||||
expectError: `"tags" attribute is required by coder_workspace_tags`,
|
||||
},
|
||||
{
|
||||
name: "main.tf with valid workspace tags",
|
||||
files: map[string]string{
|
||||
"main.tf": `
|
||||
provider "foo" {}
|
||||
resource "foo_bar" "baz" {}
|
||||
variable "region" {
|
||||
type = string
|
||||
default = "us"
|
||||
}
|
||||
data "base" "ours" {
|
||||
all = true
|
||||
}
|
||||
data "coder_parameter" "az" {
|
||||
name = "az"
|
||||
type = "string"
|
||||
default = "a"
|
||||
}
|
||||
data "coder_workspace_tags" "tags" {
|
||||
tags = {
|
||||
"platform" = "kubernetes",
|
||||
"cluster" = "${"devel"}${"opers"}"
|
||||
"region" = var.region
|
||||
"az" = data.coder_parameter.az.value
|
||||
}
|
||||
}`,
|
||||
},
|
||||
expectTags: map[string]string{"platform": "kubernetes", "cluster": "developers", "region": "us", "az": "a"},
|
||||
expectError: "",
|
||||
},
|
||||
{
|
||||
name: "main.tf with multiple valid workspace tags",
|
||||
files: map[string]string{
|
||||
"main.tf": `
|
||||
provider "foo" {}
|
||||
resource "foo_bar" "baz" {}
|
||||
variable "region" {
|
||||
type = string
|
||||
default = "us"
|
||||
}
|
||||
variable "region2" {
|
||||
type = string
|
||||
default = "eu"
|
||||
}
|
||||
data "base" "ours" {
|
||||
all = true
|
||||
}
|
||||
data "coder_parameter" "az" {
|
||||
name = "az"
|
||||
type = "string"
|
||||
default = "a"
|
||||
}
|
||||
data "coder_parameter" "az2" {
|
||||
name = "az2"
|
||||
type = "string"
|
||||
default = "b"
|
||||
}
|
||||
data "coder_workspace_tags" "tags" {
|
||||
tags = {
|
||||
"platform" = "kubernetes",
|
||||
"cluster" = "${"devel"}${"opers"}"
|
||||
"region" = var.region
|
||||
"az" = data.coder_parameter.az.value
|
||||
}
|
||||
}
|
||||
data "coder_workspace_tags" "more_tags" {
|
||||
tags = {
|
||||
"foo" = "bar"
|
||||
}
|
||||
}`,
|
||||
},
|
||||
expectTags: map[string]string{"platform": "kubernetes", "cluster": "developers", "region": "us", "az": "a", "foo": "bar"},
|
||||
expectError: "",
|
||||
},
|
||||
{
|
||||
name: "main.tf with missing parameter default value for workspace tags",
|
||||
files: map[string]string{
|
||||
"main.tf": `
|
||||
provider "foo" {}
|
||||
resource "foo_bar" "baz" {}
|
||||
variable "region" {
|
||||
type = string
|
||||
default = "us"
|
||||
}
|
||||
data "base" "ours" {
|
||||
all = true
|
||||
}
|
||||
data "coder_parameter" "az" {
|
||||
name = "az"
|
||||
type = "string"
|
||||
}
|
||||
data "coder_workspace_tags" "tags" {
|
||||
tags = {
|
||||
"platform" = "kubernetes",
|
||||
"cluster" = "${"devel"}${"opers"}"
|
||||
"region" = var.region
|
||||
"az" = data.coder_parameter.az.value
|
||||
}
|
||||
}`,
|
||||
},
|
||||
expectError: `provisioner tag "az" evaluated to an empty value, please set a default value`,
|
||||
},
|
||||
{
|
||||
name: "main.tf with missing parameter default value outside workspace tags",
|
||||
files: map[string]string{
|
||||
"main.tf": `
|
||||
provider "foo" {}
|
||||
resource "foo_bar" "baz" {}
|
||||
variable "region" {
|
||||
type = string
|
||||
default = "us"
|
||||
}
|
||||
data "base" "ours" {
|
||||
all = true
|
||||
}
|
||||
data "coder_parameter" "az" {
|
||||
name = "az"
|
||||
type = "string"
|
||||
default = "a"
|
||||
}
|
||||
data "coder_parameter" "notaz" {
|
||||
name = "notaz"
|
||||
type = "string"
|
||||
}
|
||||
data "coder_workspace_tags" "tags" {
|
||||
tags = {
|
||||
"platform" = "kubernetes",
|
||||
"cluster" = "${"devel"}${"opers"}"
|
||||
"region" = var.region
|
||||
"az" = data.coder_parameter.az.value
|
||||
}
|
||||
}`,
|
||||
},
|
||||
expectTags: map[string]string{"platform": "kubernetes", "cluster": "developers", "region": "us", "az": "a"},
|
||||
expectError: ``,
|
||||
},
|
||||
{
|
||||
name: "main.tf with missing variable default value outside workspace tags",
|
||||
files: map[string]string{
|
||||
"main.tf": `
|
||||
provider "foo" {}
|
||||
resource "foo_bar" "baz" {}
|
||||
variable "region" {
|
||||
type = string
|
||||
default = "us"
|
||||
}
|
||||
variable "notregion" {
|
||||
type = string
|
||||
}
|
||||
data "base" "ours" {
|
||||
all = true
|
||||
}
|
||||
data "coder_parameter" "az" {
|
||||
name = "az"
|
||||
type = "string"
|
||||
default = "a"
|
||||
}
|
||||
data "coder_workspace_tags" "tags" {
|
||||
tags = {
|
||||
"platform" = "kubernetes",
|
||||
"cluster" = "${"devel"}${"opers"}"
|
||||
"region" = var.region
|
||||
"az" = data.coder_parameter.az.value
|
||||
}
|
||||
}`,
|
||||
},
|
||||
expectTags: map[string]string{"platform": "kubernetes", "cluster": "developers", "region": "us", "az": "a"},
|
||||
expectError: ``,
|
||||
},
|
||||
{
|
||||
name: "main.tf with disallowed data source for workspace tags",
|
||||
files: map[string]string{
|
||||
"main.tf": `
|
||||
provider "foo" {}
|
||||
resource "foo_bar" "baz" {
|
||||
name = "foobar"
|
||||
}
|
||||
variable "region" {
|
||||
type = string
|
||||
default = "us"
|
||||
}
|
||||
data "base" "ours" {
|
||||
all = true
|
||||
}
|
||||
data "coder_parameter" "az" {
|
||||
name = "az"
|
||||
type = "string"
|
||||
default = "a"
|
||||
}
|
||||
data "local_file" "hostname" {
|
||||
filename = "/etc/hostname"
|
||||
}
|
||||
data "coder_workspace_tags" "tags" {
|
||||
tags = {
|
||||
"platform" = "kubernetes",
|
||||
"cluster" = "${"devel"}${"opers"}"
|
||||
"region" = var.region
|
||||
"az" = data.coder_parameter.az.value
|
||||
"hostname" = data.local_file.hostname.content
|
||||
}
|
||||
}`,
|
||||
},
|
||||
expectTags: nil,
|
||||
expectError: `invalid workspace tag value "data.local_file.hostname.content": only the "coder_parameter" data source is supported here`,
|
||||
},
|
||||
{
|
||||
name: "main.tf with disallowed resource for workspace tags",
|
||||
files: map[string]string{
|
||||
"main.tf": `
|
||||
provider "foo" {}
|
||||
resource "foo_bar" "baz" {
|
||||
name = "foobar"
|
||||
}
|
||||
variable "region" {
|
||||
type = string
|
||||
default = "us"
|
||||
}
|
||||
data "base" "ours" {
|
||||
all = true
|
||||
}
|
||||
data "coder_parameter" "az" {
|
||||
name = "az"
|
||||
type = "string"
|
||||
default = "a"
|
||||
}
|
||||
data "coder_workspace_tags" "tags" {
|
||||
tags = {
|
||||
"platform" = "kubernetes",
|
||||
"cluster" = "${"devel"}${"opers"}"
|
||||
"region" = var.region
|
||||
"az" = data.coder_parameter.az.value
|
||||
"foobarbaz" = foo_bar.baz.name
|
||||
}
|
||||
}`,
|
||||
},
|
||||
expectTags: nil,
|
||||
// TODO: this error isn't great, but it has the desired effect.
|
||||
expectError: `There is no variable named "foo_bar"`,
|
||||
},
|
||||
{
|
||||
name: "main.tf with functions in workspace tags",
|
||||
files: map[string]string{
|
||||
"main.tf": `
|
||||
provider "foo" {}
|
||||
resource "foo_bar" "baz" {
|
||||
name = "foobar"
|
||||
}
|
||||
variable "region" {
|
||||
type = string
|
||||
default = "region.us"
|
||||
}
|
||||
data "base" "ours" {
|
||||
all = true
|
||||
}
|
||||
data "coder_parameter" "az" {
|
||||
name = "az"
|
||||
type = "string"
|
||||
default = "az.a"
|
||||
}
|
||||
data "coder_workspace_tags" "tags" {
|
||||
tags = {
|
||||
"platform" = "kubernetes",
|
||||
"cluster" = "${"devel"}${"opers"}"
|
||||
"region" = try(split(".", var.region)[1], "placeholder")
|
||||
"az" = try(split(".", data.coder_parameter.az.value)[1], "placeholder")
|
||||
}
|
||||
}`,
|
||||
},
|
||||
expectTags: nil,
|
||||
expectError: `Function calls not allowed; Functions may not be called here.`,
|
||||
},
|
||||
} {
|
||||
tc := tc
|
||||
t.Run(tc.name+"/tar", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
tar := createTar(t, tc.files)
|
||||
logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug)
|
||||
tmpDir := t.TempDir()
|
||||
tfparse.WriteArchive(tar, "application/x-tar", tmpDir)
|
||||
parser, diags := tfparse.New(tmpDir, tfparse.WithLogger(logger))
|
||||
require.NoError(t, diags.Err())
|
||||
tags, err := parser.WorkspaceTagDefaults(ctx)
|
||||
if tc.expectError != "" {
|
||||
require.NotNil(t, err)
|
||||
require.Contains(t, err.Error(), tc.expectError)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.expectTags, tags)
|
||||
}
|
||||
})
|
||||
t.Run(tc.name+"/zip", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
zip := createZip(t, tc.files)
|
||||
logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug)
|
||||
tmpDir := t.TempDir()
|
||||
tfparse.WriteArchive(zip, "application/zip", tmpDir)
|
||||
parser, diags := tfparse.New(tmpDir, tfparse.WithLogger(logger))
|
||||
require.NoError(t, diags.Err())
|
||||
tags, err := parser.WorkspaceTagDefaults(ctx)
|
||||
if tc.expectError != "" {
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), tc.expectError)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.expectTags, tags)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func createTar(t testing.TB, files map[string]string) []byte {
|
||||
var buffer bytes.Buffer
|
||||
writer := tar.NewWriter(&buffer)
|
||||
for path, content := range files {
|
||||
err := writer.WriteHeader(&tar.Header{
|
||||
Name: path,
|
||||
Size: int64(len(content)),
|
||||
Uid: 65534, // nobody
|
||||
Gid: 65534, // nogroup
|
||||
Mode: 0o666, // -rw-rw-rw-
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = writer.Write([]byte(content))
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
err := writer.Flush()
|
||||
require.NoError(t, err)
|
||||
return buffer.Bytes()
|
||||
}
|
||||
|
||||
func createZip(t testing.TB, files map[string]string) []byte {
|
||||
ta := createTar(t, files)
|
||||
tr := tar.NewReader(bytes.NewReader(ta))
|
||||
za, err := archive.CreateZipFromTar(tr, int64(len(ta)))
|
||||
require.NoError(t, err)
|
||||
return za
|
||||
}
|
||||
|
||||
// Last run results:
|
||||
// goos: linux
|
||||
// goarch: amd64
|
||||
// pkg: github.com/coder/coder/v2/provisioner/terraform/tfparse
|
||||
// cpu: AMD EPYC 7502P 32-Core Processor
|
||||
// BenchmarkWorkspaceTagDefaultsFromFile/Tar-16 1922 847236 ns/op 176257 B/op 1073 allocs/op
|
||||
// BenchmarkWorkspaceTagDefaultsFromFile/Zip-16 1273 946910 ns/op 225293 B/op 1130 allocs/op
|
||||
// PASS
|
||||
func BenchmarkWorkspaceTagDefaultsFromFile(b *testing.B) {
|
||||
files := map[string]string{
|
||||
"main.tf": `
|
||||
provider "foo" {}
|
||||
resource "foo_bar" "baz" {}
|
||||
variable "region" {
|
||||
type = string
|
||||
default = "us"
|
||||
}
|
||||
data "coder_parameter" "az" {
|
||||
name = "az"
|
||||
type = "string"
|
||||
default = "a"
|
||||
}
|
||||
data "coder_workspace_tags" "tags" {
|
||||
tags = {
|
||||
"platform" = "kubernetes",
|
||||
"cluster" = "${"devel"}${"opers"}"
|
||||
"region" = var.region
|
||||
"az" = data.coder_parameter.az.value
|
||||
}
|
||||
}`,
|
||||
}
|
||||
tarFile := createTar(b, files)
|
||||
zipFile := createZip(b, files)
|
||||
logger := discardLogger(b)
|
||||
b.ResetTimer()
|
||||
b.Run("Tar", func(b *testing.B) {
|
||||
ctx := context.Background()
|
||||
for i := 0; i < b.N; i++ {
|
||||
tmpDir := b.TempDir()
|
||||
tfparse.WriteArchive(tarFile, "application/x-tar", tmpDir)
|
||||
parser, diags := tfparse.New(tmpDir, tfparse.WithLogger(logger))
|
||||
require.NoError(b, diags.Err())
|
||||
_, err := parser.WorkspaceTags(ctx)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("Zip", func(b *testing.B) {
|
||||
ctx := context.Background()
|
||||
for i := 0; i < b.N; i++ {
|
||||
tmpDir := b.TempDir()
|
||||
tfparse.WriteArchive(zipFile, "application/zip", tmpDir)
|
||||
parser, diags := tfparse.New(tmpDir, tfparse.WithLogger(logger))
|
||||
require.NoError(b, diags.Err())
|
||||
_, err := parser.WorkspaceTags(ctx)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func discardLogger(_ testing.TB) slog.Logger {
|
||||
l := slog.Make(sloghuman.Sink(io.Discard))
|
||||
log.SetOutput(slog.Stdlib(context.Background(), l, slog.LevelInfo).Writer())
|
||||
return l
|
||||
}
|
||||
Reference in New Issue
Block a user