internal/depsfile: SaveLocksToFile implementation

This is an initial implementation of writing locks back to a file on disk.
This initial implementation is incomplete because it does not write the
changes to the new file atomically. We'll revisit that in a later commit
as we return to polish these codepaths, once we've proven out this
package's design by integrating it with Terraform's provider installer.
This commit is contained in:
Martin Atkins 2020-09-04 09:12:26 -07:00
parent 92723661d0
commit 98e2e69abb
3 changed files with 166 additions and 1 deletions

View File

@ -1,6 +1,8 @@
package depsfile
import (
"sort"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/internal/getproviders"
)
@ -51,6 +53,11 @@ func (l *Locks) Provider(addr addrs.Provider) *ProviderLock {
// invalidates any ProviderLock object previously returned from Provider or
// SetProvider for the given provider address.
func (l *Locks) SetProvider(addr addrs.Provider, version getproviders.Version, constraints getproviders.VersionConstraints, hashes map[getproviders.Platform][]string) *ProviderLock {
// Normalize the hash lists into a consistent order.
for _, slice := range hashes {
sort.Strings(slice)
}
new := &ProviderLock{
addr: addr,
version: version,

View File

@ -2,10 +2,16 @@ package depsfile
import (
"fmt"
"io/ioutil"
"os"
"sort"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/gohcl"
"github.com/hashicorp/hcl/v2/hclparse"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/hashicorp/hcl/v2/hclwrite"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/internal/getproviders"
@ -49,7 +55,95 @@ func LoadLocksFromFile(filename string) (*Locks, tfdiags.Diagnostics) {
// temporary files may be temporarily created in the same directory as the
// given filename during the operation.
func SaveLocksToFile(locks *Locks, filename string) tfdiags.Diagnostics {
panic("SaveLocksToFile is not implemented yet")
var diags tfdiags.Diagnostics
// In other uses of the "hclwrite" package we typically try to make
// surgical updates to the author's existing files, preserving their
// block ordering, comments, etc. We intentionally don't do that here
// to reinforce the fact that this file primarily belongs to Terraform,
// and to help ensure that VCS diffs of the file primarily reflect
// changes that actually affect functionality rather than just cosmetic
// changes, by maintaining it in a highly-normalized form.
f := hclwrite.NewEmptyFile()
rootBody := f.Body()
// End-users _may_ edit the lock file in exceptional situations, like
// working around potential dependency selection bugs, but we intend it
// to be primarily maintained automatically by the "terraform init"
// command.
rootBody.AppendUnstructuredTokens(hclwrite.Tokens{
{
Type: hclsyntax.TokenComment,
Bytes: []byte("# This file is maintained automatically by \"terraform init\".\n"),
},
{
Type: hclsyntax.TokenComment,
Bytes: []byte("# Manual edits may be lost in future updates.\n"),
},
})
providers := make([]addrs.Provider, 0, len(locks.providers))
for provider := range locks.providers {
providers = append(providers, provider)
}
sort.Slice(providers, func(i, j int) bool {
return providers[i].LessThan(providers[j])
})
for _, provider := range providers {
lock := locks.providers[provider]
rootBody.AppendNewline()
block := rootBody.AppendNewBlock("provider", []string{lock.addr.String()})
body := block.Body()
body.SetAttributeValue("version", cty.StringVal(lock.version.String()))
if constraintsStr := getproviders.VersionConstraintsString(lock.versionConstraints); constraintsStr != "" {
body.SetAttributeValue("constraints", cty.StringVal(constraintsStr))
}
if len(lock.hashes) != 0 {
platforms := make([]getproviders.Platform, 0, len(lock.hashes))
for platform := range lock.hashes {
platforms = append(platforms, platform)
}
sort.Slice(platforms, func(i, j int) bool {
return platforms[i].LessThan(platforms[j])
})
body.AppendNewline()
hashesBlock := body.AppendNewBlock("hashes", nil)
hashesBody := hashesBlock.Body()
for platform, hashes := range lock.hashes {
vals := make([]cty.Value, len(hashes))
for i := range hashes {
vals[i] = cty.StringVal(hashes[i])
}
var hashList cty.Value
if len(vals) > 0 {
hashList = cty.ListVal(vals)
} else {
hashList = cty.ListValEmpty(cty.String)
}
hashesBody.SetAttributeValue(platform.String(), hashList)
}
}
}
newContent := f.Bytes()
// TODO: Create the content in a new file and atomically pivot it into
// the target, so that there isn't a brief period where an incomplete
// file can be seen at the given location.
// But for now, this gets us started.
err := ioutil.WriteFile(filename, newContent, os.ModePerm)
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Failed to update dependency lock file",
fmt.Sprintf("Error while writing new dependency lock information to %s: %s.", filename, err),
))
return diags
}
return diags
}
func decodeLocksFromHCL(locks *Locks, body hcl.Body) tfdiags.Diagnostics {

View File

@ -162,3 +162,67 @@ func TestLoadLocksFromFile(t *testing.T) {
})
}
}
func TestSaveLocksToFile(t *testing.T) {
locks := NewLocks()
fooProvider := addrs.MustParseProviderSourceString("test/foo")
barProvider := addrs.MustParseProviderSourceString("test/bar")
bazProvider := addrs.MustParseProviderSourceString("test/baz")
oneDotOh := getproviders.MustParseVersion("1.0.0")
oneDotTwo := getproviders.MustParseVersion("1.2.0")
atLeastOneDotOh := getproviders.MustParseVersionConstraints(">= 1.0.0")
pessimisticOneDotOh := getproviders.MustParseVersionConstraints("~> 1")
hashes := map[getproviders.Platform][]string{
{OS: "riscos", Arch: "arm"}: {
"cccccccccccccccccccccccccccccccccccccccccccccccc",
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
},
}
locks.SetProvider(fooProvider, oneDotOh, atLeastOneDotOh, hashes)
locks.SetProvider(barProvider, oneDotTwo, pessimisticOneDotOh, nil)
locks.SetProvider(bazProvider, oneDotTwo, nil, nil)
dir, err := ioutil.TempDir("", "terraform-internal-depsfile-savelockstofile")
if err != nil {
t.Fatal(err.Error())
}
defer os.RemoveAll(dir)
filename := filepath.Join(dir, LockFilePath)
diags := SaveLocksToFile(locks, filename)
if diags.HasErrors() {
t.Fatalf("unexpected errors\n%s", diags.Err().Error())
}
gotContentBytes, err := ioutil.ReadFile(filename)
if err != nil {
t.Fatalf(err.Error())
}
gotContent := string(gotContentBytes)
wantContent := `# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.
provider "registry.terraform.io/test/bar" {
version = "1.2.0"
constraints = "~> 1.0"
}
provider "registry.terraform.io/test/baz" {
version = "1.2.0"
}
provider "registry.terraform.io/test/foo" {
version = "1.0.0"
constraints = ">= 1.0.0"
hashes {
riscos_arm = ["aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", "cccccccccccccccccccccccccccccccccccccccccccccccc"]
}
}
`
if diff := cmp.Diff(wantContent, gotContent); diff != "" {
t.Errorf("wrong result\n%s", diff)
}
}