mirror of
https://github.com/coder/coder.git
synced 2025-07-03 16:13:58 +00:00
feat(codersdk): export template variable parser (#13984)
This commit is contained in:
272
codersdk/templatevariables.go
Normal file
272
codersdk/templatevariables.go
Normal file
@ -0,0 +1,272 @@
|
||||
package codersdk
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/hashicorp/hcl/v2/hclparse"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
/**
|
||||
* 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()
|
||||
if ctyType.Equals(cty.String) {
|
||||
stringData[attribute.Name] = ctyValue.AsString()
|
||||
} else if ctyType.Equals(cty.Number) {
|
||||
stringData[attribute.Name] = ctyValue.AsBigFloat().String()
|
||||
} else if ctyType.IsTupleType() {
|
||||
// In case of tuples, Coder only supports the list(string) type.
|
||||
var items []string
|
||||
var err error
|
||||
_ = ctyValue.ForEachElement(func(key, 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)
|
||||
} else {
|
||||
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
|
||||
}
|
Reference in New Issue
Block a user