terraform/configs/configupgrade/upgrade.go

124 lines
4.0 KiB
Go

package configupgrade
import (
"bytes"
"fmt"
"github.com/hashicorp/terraform/tfdiags"
hcl2 "github.com/hashicorp/hcl2/hcl"
hcl2write "github.com/hashicorp/hcl2/hclwrite"
)
// Upgrade takes some input module sources and produces a new ModuleSources
// that should be equivalent to the input but use the configuration idioms
// associated with the new configuration loader.
//
// The result of this function will probably not be accepted by this function,
// because it will contain constructs that are known only to the new
// loader.
//
// The result may include additional files that were not present in the
// input. The result may also include nil entries for filenames that were
// present in the input, indicating that these files should be deleted.
// In particular, file renames are represented as a new entry accompanied
// by a nil entry for the old name.
//
// If the returned diagnostics contains errors, the caller should not write
// the resulting sources to disk since they will probably be incomplete. If
// only warnings are present then the files may be written to disk. Most
// warnings are also represented as "TF-UPGRADE-TODO:" comments in the
// generated source files so that users can visit them all and decide what to
// do with them.
func (u *Upgrader) Upgrade(input ModuleSources, dir string) (ModuleSources, tfdiags.Diagnostics) {
ret := make(ModuleSources)
var diags tfdiags.Diagnostics
an, err := u.analyze(input)
if err != nil {
diags = diags.Append(err)
return ret, diags
}
an.ModuleDir = dir
for name, src := range input {
ext := fileExt(name)
if ext == "" {
// This should never happen because we ignore files that don't
// have our conventional extensions during LoadModule, but we'll
// silently pass through such files assuming that the caller
// has been tampering with the sources map somehow.
ret[name] = src
continue
}
isJSON := (ext == ".tf.json")
// The legacy loader allowed JSON syntax inside files named just .tf,
// so we'll detect that case and rename them here so that the new
// loader will accept the JSON. However, JSON files are usually
// generated so we'll also generate a warning to the user to update
// whatever program generated the file to use the new name.
if !isJSON {
trimSrc := bytes.TrimSpace(src)
if len(trimSrc) > 0 && (trimSrc[0] == '{' || trimSrc[0] == '[') {
isJSON = true
// Rename in the output
ret[name] = nil // mark for deletion
oldName := name
name = input.UnusedFilename(name + ".json")
ret[name] = src
diags = diags.Append(&hcl2.Diagnostic{
Severity: hcl2.DiagWarning,
Summary: "JSON configuration file was renamed",
Detail: fmt.Sprintf(
"The file %q appears to be in JSON format, so it was renamed to %q. If this file is generated by another program, that program must be updated to use this new name.",
oldName, name,
),
})
continue
}
}
if isJSON {
// We don't do any automatic rewriting for JSON files, since they
// are usually generated and thus it's the generating program that
// needs to be updated, rather than its output.
diags = diags.Append(&hcl2.Diagnostic{
Severity: hcl2.DiagWarning,
Summary: "JSON configuration file was not rewritten",
Detail: fmt.Sprintf(
"The JSON configuration file %q was skipped, because JSON files are assumed to be generated. The program that generated this file may need to be updated for changes to the configuration language.",
name,
),
})
ret[name] = src // unchanged
continue
}
// TODO: Actually rewrite this .tf file.
result, fileDiags := u.upgradeNativeSyntaxFile(name, src, an)
diags = diags.Append(fileDiags)
if fileDiags.HasErrors() {
// Leave unchanged, then.
ret[name] = src
continue
}
ret[name] = hcl2write.Format(result.Content)
}
versionsName := ret.UnusedFilename("versions.tf")
ret[versionsName] = []byte(newVersionConstraint)
return ret, diags
}
const newVersionConstraint = `
terraform {
required_version = ">= 0.12"
}
`