Files
coder/codersdk/templatevariables.go
T
Spike Curtis bddb808b25 chore: arrange imports in a standard way (#21452)
Fixes all our Go file imports to match the preferred spec that we've _mostly_ been using. For example:

```
import (
	"context"
	"time"

	"github.com/prometheus/client_golang/prometheus"
	"golang.org/x/xerrors"
	"gopkg.in/natefinch/lumberjack.v2"

	"cdr.dev/slog/v3"
	"github.com/coder/coder/v2/codersdk/agentsdk"
	"github.com/coder/serpent"
)
```

3 groups: standard library, 3rd partly libs, Coder libs.

This PR makes the change across the codebase. The PR in the stack above modifies our formatting to maintain this state of affairs, and is a separate PR so it's possible to review that one in detail.
2026-01-08 15:24:11 +04:00

273 lines
6.7 KiB
Go

package codersdk
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"github.com/hashicorp/hcl/v2/hclparse"
"github.com/zclconf/go-cty/cty"
"golang.org/x/xerrors"
"gopkg.in/yaml.v3"
)
/**
* DiscoverVarsFiles function loads vars files in a predefined order:
* 1. terraform.tfvars
* 2. terraform.tfvars.json
* 3. *.auto.tfvars
* 4. *.auto.tfvars.json
*/
func DiscoverVarsFiles(workDir string) ([]string, error) {
var found []string
fi, err := os.Stat(filepath.Join(workDir, "terraform.tfvars"))
if err == nil {
found = append(found, filepath.Join(workDir, fi.Name()))
} else if !os.IsNotExist(err) {
return nil, err
}
fi, err = os.Stat(filepath.Join(workDir, "terraform.tfvars.json"))
if err == nil {
found = append(found, filepath.Join(workDir, fi.Name()))
} else if !os.IsNotExist(err) {
return nil, err
}
dirEntries, err := os.ReadDir(workDir)
if err != nil {
return nil, err
}
for _, dirEntry := range dirEntries {
if strings.HasSuffix(dirEntry.Name(), ".auto.tfvars") || strings.HasSuffix(dirEntry.Name(), ".auto.tfvars.json") {
found = append(found, filepath.Join(workDir, dirEntry.Name()))
}
}
return found, nil
}
func ParseUserVariableValues(varsFiles []string, variablesFile string, commandLineVariables []string) ([]VariableValue, error) {
fromVars, err := parseVariableValuesFromVarsFiles(varsFiles)
if err != nil {
return nil, err
}
fromFile, err := parseVariableValuesFromFile(variablesFile)
if err != nil {
return nil, err
}
fromCommandLine, err := parseVariableValuesFromCommandLine(commandLineVariables)
if err != nil {
return nil, err
}
return CombineVariableValues(fromVars, fromFile, fromCommandLine), nil
}
func parseVariableValuesFromVarsFiles(varsFiles []string) ([]VariableValue, error) {
var parsed []VariableValue
for _, varsFile := range varsFiles {
content, err := os.ReadFile(varsFile)
if err != nil {
return nil, err
}
var t []VariableValue
ext := filepath.Ext(varsFile)
switch ext {
case ".tfvars":
t, err = parseVariableValuesFromHCL(content)
if err != nil {
return nil, xerrors.Errorf("unable to parse HCL content: %w", err)
}
case ".json":
t, err = parseVariableValuesFromJSON(content)
if err != nil {
return nil, xerrors.Errorf("unable to parse JSON content: %w", err)
}
default:
return nil, xerrors.Errorf("unexpected tfvars format: %s", ext)
}
parsed = append(parsed, t...)
}
return parsed, nil
}
func parseVariableValuesFromHCL(content []byte) ([]VariableValue, error) {
parser := hclparse.NewParser()
hclFile, diags := parser.ParseHCL(content, "file.hcl")
if diags.HasErrors() {
return nil, diags
}
attrs, diags := hclFile.Body.JustAttributes()
if diags.HasErrors() {
return nil, diags
}
stringData := map[string]string{}
for _, attribute := range attrs {
ctyValue, diags := attribute.Expr.Value(nil)
if diags.HasErrors() {
return nil, diags
}
ctyType := ctyValue.Type()
switch {
case ctyType.Equals(cty.String):
stringData[attribute.Name] = ctyValue.AsString()
case ctyType.Equals(cty.Number):
stringData[attribute.Name] = ctyValue.AsBigFloat().String()
case ctyType.IsTupleType():
// In case of tuples, Coder only supports the list(string) type.
var items []string
var err error
_ = ctyValue.ForEachElement(func(_, val cty.Value) (stop bool) {
if !val.Type().Equals(cty.String) {
err = xerrors.Errorf("unsupported tuple item type: %s ", val.GoString())
return true
}
items = append(items, val.AsString())
return false
})
if err != nil {
return nil, err
}
m, err := json.Marshal(items)
if err != nil {
return nil, err
}
stringData[attribute.Name] = string(m)
default:
return nil, xerrors.Errorf("unsupported value type (name: %s): %s", attribute.Name, ctyType.GoString())
}
}
return convertMapIntoVariableValues(stringData), nil
}
// parseVariableValuesFromJSON converts the .tfvars.json content into template variables.
// The function visits only root-level properties as template variables do not support nested
// structures.
func parseVariableValuesFromJSON(content []byte) ([]VariableValue, error) {
var data map[string]interface{}
err := json.Unmarshal(content, &data)
if err != nil {
return nil, err
}
stringData := map[string]string{}
for key, value := range data {
switch value.(type) {
case string, int, bool:
stringData[key] = fmt.Sprintf("%v", value)
default:
m, err := json.Marshal(value)
if err != nil {
return nil, err
}
stringData[key] = string(m)
}
}
return convertMapIntoVariableValues(stringData), nil
}
func convertMapIntoVariableValues(m map[string]string) []VariableValue {
var parsed []VariableValue
for key, value := range m {
parsed = append(parsed, VariableValue{
Name: key,
Value: value,
})
}
sort.Slice(parsed, func(i, j int) bool {
return parsed[i].Name < parsed[j].Name
})
return parsed
}
func parseVariableValuesFromFile(variablesFile string) ([]VariableValue, error) {
var values []VariableValue
if variablesFile == "" {
return values, nil
}
variablesMap, err := createVariablesMapFromFile(variablesFile)
if err != nil {
return nil, err
}
for name, value := range variablesMap {
values = append(values, VariableValue{
Name: name,
Value: value,
})
}
return values, nil
}
// Reads a YAML file and populates a string -> string map.
// Throws an error if the file name is empty.
func createVariablesMapFromFile(variablesFile string) (map[string]string, error) {
if variablesFile == "" {
return nil, xerrors.Errorf("variable file name is not specified")
}
variablesMap := make(map[string]string)
variablesFileContents, err := os.ReadFile(variablesFile)
if err != nil {
return nil, err
}
err = yaml.Unmarshal(variablesFileContents, &variablesMap)
if err != nil {
return nil, err
}
return variablesMap, nil
}
func parseVariableValuesFromCommandLine(variables []string) ([]VariableValue, error) {
var values []VariableValue
for _, keyValue := range variables {
split := strings.SplitN(keyValue, "=", 2)
if len(split) < 2 {
return nil, xerrors.Errorf("format key=value expected, but got %s", keyValue)
}
values = append(values, VariableValue{
Name: split[0],
Value: split[1],
})
}
return values, nil
}
func CombineVariableValues(valuesSets ...[]VariableValue) []VariableValue {
combinedValues := make(map[string]string)
for _, values := range valuesSets {
for _, v := range values {
combinedValues[v.Name] = v.Value
}
}
var result []VariableValue
for name, value := range combinedValues {
result = append(result, VariableValue{Name: name, Value: value})
}
sort.Slice(result, func(i, j int) bool {
return result[i].Name < result[j].Name
})
return result
}