mirror of
https://github.com/coder/coder.git
synced 2025-07-13 21:36:50 +00:00
feat(provisioner): add support for .tf.json templates (#7835)
Co-authored-by: Colin Adler <colin1adler@gmail.com>
This commit is contained in:
@ -27,29 +27,44 @@ func rawRichParameterNames(workdir string) ([]string, error) {
|
|||||||
|
|
||||||
var coderParameterNames []string
|
var coderParameterNames []string
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
if !strings.HasSuffix(entry.Name(), ".tf") {
|
if !strings.HasSuffix(entry.Name(), ".tf") && !strings.HasSuffix(entry.Name(), ".tf.json") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
hclFilepath := path.Join(workdir, entry.Name())
|
var (
|
||||||
parser := hclparse.NewParser()
|
parsedTF *hcl.File
|
||||||
parsedHCL, diags := parser.ParseHCLFile(hclFilepath)
|
diags hcl.Diagnostics
|
||||||
|
tfFilepath = path.Join(workdir, entry.Name())
|
||||||
|
parser = hclparse.NewParser()
|
||||||
|
)
|
||||||
|
|
||||||
|
// Support .tf.json files.
|
||||||
|
// Warning: since JSON parsing in Go automatically sorts maps
|
||||||
|
// alphabetically, we can't preserve the original order of parameters
|
||||||
|
// like in HCL.
|
||||||
|
if strings.HasSuffix(entry.Name(), ".tf.json") {
|
||||||
|
parsedTF, diags = parser.ParseJSONFile(tfFilepath)
|
||||||
|
} else {
|
||||||
|
parsedTF, diags = parser.ParseHCLFile(tfFilepath)
|
||||||
|
}
|
||||||
|
|
||||||
if diags.HasErrors() {
|
if diags.HasErrors() {
|
||||||
return nil, hcl.Diagnostics{
|
return nil, hcl.Diagnostics{
|
||||||
{
|
{
|
||||||
Severity: hcl.DiagError,
|
Severity: hcl.DiagError,
|
||||||
Summary: "Failed to parse HCL file",
|
Summary: "Failed to parse HCL file",
|
||||||
Detail: fmt.Sprintf("parser.ParseHCLFile can't parse %q file", hclFilepath),
|
Detail: fmt.Sprintf("parser.ParseHCLFile can't parse %q file", tfFilepath),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
content, _, _ := parsedHCL.Body.PartialContent(terraformWithCoderParametersSchema)
|
content, _, _ := parsedTF.Body.PartialContent(terraformWithCoderParametersSchema)
|
||||||
for _, block := range content.Blocks {
|
for _, block := range content.Blocks {
|
||||||
if block.Type == "data" && block.Labels[0] == "coder_parameter" && len(block.Labels) == 2 {
|
if block.Type == "data" && block.Labels[0] == "coder_parameter" && len(block.Labels) == 2 {
|
||||||
coderParameterNames = append(coderParameterNames, block.Labels[1])
|
coderParameterNames = append(coderParameterNames, block.Labels[1])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return coderParameterNames, nil
|
return coderParameterNames, nil
|
||||||
}
|
}
|
||||||
|
@ -103,7 +103,7 @@ func loadEnabledFeatures(moduleDir string) (map[string]bool, hcl.Diagnostics) {
|
|||||||
|
|
||||||
var found bool
|
var found bool
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
if !strings.HasSuffix(entry.Name(), ".tf") {
|
if !strings.HasSuffix(entry.Name(), ".tf") && !strings.HasSuffix(entry.Name(), ".tf.json") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,7 +131,12 @@ func parseFeatures(hclFilepath string) (map[string]bool, bool, hcl.Diagnostics)
|
|||||||
}
|
}
|
||||||
|
|
||||||
parser := hclparse.NewParser()
|
parser := hclparse.NewParser()
|
||||||
parsedHCL, diags := parser.ParseHCLFile(hclFilepath)
|
var parsedHCL *hcl.File
|
||||||
|
if strings.HasSuffix(hclFilepath, ".tf.json") {
|
||||||
|
parsedHCL, diags = parser.ParseJSONFile(hclFilepath)
|
||||||
|
} else {
|
||||||
|
parsedHCL, diags = parser.ParseHCLFile(hclFilepath)
|
||||||
|
}
|
||||||
if diags.HasErrors() {
|
if diags.HasErrors() {
|
||||||
return flags, false, diags
|
return flags, false, diags
|
||||||
}
|
}
|
||||||
|
@ -254,6 +254,31 @@ func TestProvision(t *testing.T) {
|
|||||||
},
|
},
|
||||||
Apply: true,
|
Apply: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "single-resource-json",
|
||||||
|
Files: map[string]string{
|
||||||
|
"main.tf.json": `{
|
||||||
|
"resource": {
|
||||||
|
"null_resource": {
|
||||||
|
"A": [
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
Response: &proto.Provision_Response{
|
||||||
|
Type: &proto.Provision_Response_Complete{
|
||||||
|
Complete: &proto.Provision_Complete{
|
||||||
|
Resources: []*proto.Resource{{
|
||||||
|
Name: "A",
|
||||||
|
Type: "null_resource",
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Apply: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "bad-syntax-1",
|
Name: "bad-syntax-1",
|
||||||
Files: map[string]string{
|
Files: map[string]string{
|
||||||
@ -349,6 +374,88 @@ func TestProvision(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "rich-parameter-with-value-json",
|
||||||
|
Files: map[string]string{
|
||||||
|
"main.tf.json": `{
|
||||||
|
"data": {
|
||||||
|
"coder_parameter": {
|
||||||
|
"example": [
|
||||||
|
{
|
||||||
|
"default": "foobar",
|
||||||
|
"name": "Example",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sample": [
|
||||||
|
{
|
||||||
|
"default": "foobaz",
|
||||||
|
"name": "Sample",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"resource": {
|
||||||
|
"null_resource": {
|
||||||
|
"example": [
|
||||||
|
{
|
||||||
|
"triggers": {
|
||||||
|
"misc": "${data.coder_parameter.example.value}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"terraform": [
|
||||||
|
{
|
||||||
|
"required_providers": [
|
||||||
|
{
|
||||||
|
"coder": {
|
||||||
|
"source": "coder/coder",
|
||||||
|
"version": "0.6.20"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
Request: &proto.Provision_Plan{
|
||||||
|
RichParameterValues: []*proto.RichParameterValue{
|
||||||
|
{
|
||||||
|
Name: "Example",
|
||||||
|
Value: "foobaz",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Sample",
|
||||||
|
Value: "foofoo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Response: &proto.Provision_Response{
|
||||||
|
Type: &proto.Provision_Response_Complete{
|
||||||
|
Complete: &proto.Provision_Complete{
|
||||||
|
Parameters: []*proto.RichParameter{
|
||||||
|
{
|
||||||
|
Name: "Example",
|
||||||
|
Type: "string",
|
||||||
|
DefaultValue: "foobar",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Sample",
|
||||||
|
Type: "string",
|
||||||
|
DefaultValue: "foobaz",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Resources: []*proto.Resource{{
|
||||||
|
Name: "example",
|
||||||
|
Type: "null_resource",
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "git-auth",
|
Name: "git-auth",
|
||||||
Files: map[string]string{
|
Files: map[string]string{
|
||||||
|
@ -17,15 +17,17 @@ const (
|
|||||||
TemplateArchiveLimit = 1 << 20
|
TemplateArchiveLimit = 1 << 20
|
||||||
)
|
)
|
||||||
|
|
||||||
func dirHasExt(dir string, ext string) (bool, error) {
|
func dirHasExt(dir string, exts ...string) (bool, error) {
|
||||||
dirEnts, err := os.ReadDir(dir)
|
dirEnts, err := os.ReadDir(dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, fi := range dirEnts {
|
for _, fi := range dirEnts {
|
||||||
if strings.HasSuffix(fi.Name(), ext) {
|
for _, ext := range exts {
|
||||||
return true, nil
|
if strings.HasSuffix(fi.Name(), ext) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,8 +40,8 @@ func Tar(w io.Writer, directory string, limit int64) error {
|
|||||||
w = xio.NewLimitWriter(w, limit-1)
|
w = xio.NewLimitWriter(w, limit-1)
|
||||||
tarWriter := tar.NewWriter(w)
|
tarWriter := tar.NewWriter(w)
|
||||||
|
|
||||||
const tfExt = ".tf"
|
tfExts := []string{".tf", ".tf.json"}
|
||||||
hasTf, err := dirHasExt(directory, tfExt)
|
hasTf, err := dirHasExt(directory, tfExts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -53,7 +55,7 @@ func Tar(w io.Writer, directory string, limit int64) error {
|
|||||||
// useless.
|
// useless.
|
||||||
return xerrors.Errorf(
|
return xerrors.Errorf(
|
||||||
"%s is not a valid template since it has no %s files",
|
"%s is not a valid template since it has no %s files",
|
||||||
absPath, tfExt,
|
absPath, tfExts,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,6 +59,15 @@ func TestTar(t *testing.T) {
|
|||||||
err = provisionersdk.Tar(io.Discard, dir, 1024)
|
err = provisionersdk.Tar(io.Discard, dir, 1024)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
})
|
})
|
||||||
|
t.Run("ValidJSON", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
dir := t.TempDir()
|
||||||
|
file, err := os.CreateTemp(dir, "*.tf.json")
|
||||||
|
require.NoError(t, err)
|
||||||
|
_ = file.Close()
|
||||||
|
err = provisionersdk.Tar(io.Discard, dir, 1024)
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
t.Run("HiddenFiles", func(t *testing.T) {
|
t.Run("HiddenFiles", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
|
Reference in New Issue
Block a user