package configupgrade import ( "bytes" "fmt" "github.com/hashicorp/terraform/tfdiags" hcl2 "github.com/hashicorp/hcl/v2" hcl2write "github.com/hashicorp/hcl/v2/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" } `