- Notifications
You must be signed in to change notification settings - Fork1k
feat: load variables from tfvars files#11549
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Uh oh!
There was an error while loading.Please reload this page.
Changes fromall commits
3f4994d
9076dd6
b245d5e
6097029
56ec679
667ffd6
ac6bc4e
9ad5003
c19242b
ec65ea3
2c9e7ba
029b2df
ae28cfe
e537684
6acbbee
85c0a76
710f025
cdf2ce6
0494c7d
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading.Please reload this page.
Jump to
Uh oh!
There was an error while loading.Please reload this page.
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -107,6 +107,18 @@ func (r *RootCmd) templateCreate() *clibase.Cmd { | ||
message := uploadFlags.templateMessage(inv) | ||
var varsFiles []string | ||
if !uploadFlags.stdin() { | ||
varsFiles, err = DiscoverVarsFiles(uploadFlags.directory) | ||
if err != nil { | ||
return err | ||
} | ||
Comment on lines +112 to +115 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. I could imagine a case where someone had tfvars files lying around unused before, worked around the issue, and left them there. Now we're going to auto-discover them. I'm not sure what this will break. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. Ok, I changed the code to print a message alerting about the presence of tfvars 👍 | ||
if len(varsFiles) > 0 { | ||
_, _ = fmt.Fprintln(inv.Stdout, "Auto-discovered Terraform tfvars files. Make sure to review and clean up any unused files.") | ||
} | ||
} | ||
// Confirm upload of the directory. | ||
resp, err := uploadFlags.upload(inv, client) | ||
if err != nil { | ||
@@ -119,6 +131,7 @@ func (r *RootCmd) templateCreate() *clibase.Cmd { | ||
} | ||
userVariableValues, err := ParseUserVariableValues( | ||
varsFiles, | ||
variablesFile, | ||
commandLineVariables) | ||
if err != nil { | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,16 +1,65 @@ | ||
package cli | ||
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" | ||
"github.com/coder/coder/v2/codersdk" | ||
) | ||
/** | ||
* 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) ([]codersdk.VariableValue, error) { | ||
fromVars, err := parseVariableValuesFromVarsFiles(varsFiles) | ||
if err != nil { | ||
return nil, err | ||
} | ||
fromFile, err := parseVariableValuesFromFile(variablesFile) | ||
if err != nil { | ||
return nil, err | ||
@@ -21,7 +70,131 @@ func ParseUserVariableValues(variablesFile string, commandLineVariables []string | ||
return nil, err | ||
} | ||
return combineVariableValues(fromVars, fromFile, fromCommandLine), nil | ||
} | ||
func parseVariableValuesFromVarsFiles(varsFiles []string) ([]codersdk.VariableValue, error) { | ||
var parsed []codersdk.VariableValue | ||
for _, varsFile := range varsFiles { | ||
content, err := os.ReadFile(varsFile) | ||
if err != nil { | ||
return nil, err | ||
} | ||
var t []codersdk.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) ([]codersdk.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() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. I immediately saw this, thought 🤔 "y no switch", and then saw how annoying this is. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. Yes, unfortunately the API isn't too friendly :) | ||
// 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) ([]codersdk.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) []codersdk.VariableValue { | ||
var parsed []codersdk.VariableValue | ||
for key, value := range m { | ||
parsed = append(parsed, codersdk.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) ([]codersdk.VariableValue, error) { | ||
@@ -94,5 +267,8 @@ func combineVariableValues(valuesSets ...[]codersdk.VariableValue) []codersdk.Va | ||
result = append(result, codersdk.VariableValue{Name: name, Value: value}) | ||
} | ||
sort.Slice(result, func(i, j int) bool { | ||
return result[i].Name < result[j].Name | ||
}) | ||
return result | ||
} |
Uh oh!
There was an error while loading.Please reload this page.