plans/planfile: Include dependency locks in saved plan files
We recently removed the legacy way we used to track the SHA256 hashes of individual provider executables as part of a plans.Plan, because these days we want to track the checksums of entire provider packages rather than just the executable. In order to achieve that new goal, we can save a copy of the dependency lock information inside the plan file. This follows our existing precedent of using exactly the same serialization formats we'd normally use for this information, and thus we can reuse the existing models and serializers and be confident we won't lose any detail in the round-trip. As of this commit there's not yet anything actually making use of this mechanism. In a subsequent commit we'll teach the main callers that write and read plan files to include and expect (respectively) dependency information, verifying that the available providers still match by the time we're applying the plan.
This commit is contained in:
parent
3f85591998
commit
702413702c
|
@ -7,7 +7,10 @@ import (
|
|||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/addrs"
|
||||
"github.com/hashicorp/terraform/internal/configs/configload"
|
||||
"github.com/hashicorp/terraform/internal/depsfile"
|
||||
"github.com/hashicorp/terraform/internal/getproviders"
|
||||
"github.com/hashicorp/terraform/internal/plans"
|
||||
"github.com/hashicorp/terraform/internal/states"
|
||||
"github.com/hashicorp/terraform/internal/states/statefile"
|
||||
|
@ -71,6 +74,16 @@ func TestRoundtrip(t *testing.T) {
|
|||
PriorState: stateFileIn.State,
|
||||
}
|
||||
|
||||
locksIn := depsfile.NewLocks()
|
||||
locksIn.SetProvider(
|
||||
addrs.NewDefaultProvider("boop"),
|
||||
getproviders.MustParseVersion("1.0.0"),
|
||||
getproviders.MustParseVersionConstraints(">= 1.0.0"),
|
||||
[]getproviders.Hash{
|
||||
getproviders.MustParseHash("fake:hello"),
|
||||
},
|
||||
)
|
||||
|
||||
workDir, err := ioutil.TempDir("", "tf-planfile")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -82,6 +95,7 @@ func TestRoundtrip(t *testing.T) {
|
|||
PreviousRunStateFile: prevStateFileIn,
|
||||
StateFile: stateFileIn,
|
||||
Plan: planIn,
|
||||
DependencyLocks: locksIn,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create plan file: %s", err)
|
||||
|
@ -141,4 +155,16 @@ func TestRoundtrip(t *testing.T) {
|
|||
t.Errorf("when reading config: %s", diags.Err())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ReadDependencyLocks", func(t *testing.T) {
|
||||
locksOut, diags := pr.ReadDependencyLocks()
|
||||
if diags.HasErrors() {
|
||||
t.Fatalf("when reading config: %s", diags.Err())
|
||||
}
|
||||
got := locksOut.AllProviders()
|
||||
want := locksIn.AllProviders()
|
||||
if diff := cmp.Diff(want, got, cmp.AllowUnexported(depsfile.ProviderLock{})); diff != "" {
|
||||
t.Errorf("provider locks did not survive round-trip\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
|
||||
"github.com/hashicorp/terraform/internal/configs"
|
||||
"github.com/hashicorp/terraform/internal/configs/configload"
|
||||
"github.com/hashicorp/terraform/internal/depsfile"
|
||||
"github.com/hashicorp/terraform/internal/plans"
|
||||
"github.com/hashicorp/terraform/internal/states/statefile"
|
||||
"github.com/hashicorp/terraform/internal/tfdiags"
|
||||
|
@ -15,6 +16,7 @@ import (
|
|||
|
||||
const tfstateFilename = "tfstate"
|
||||
const tfstatePreviousFilename = "tfstate-prev"
|
||||
const dependencyLocksFilename = ".terraform.lock.hcl" // matches the conventional name in an input configuration
|
||||
|
||||
// Reader is the main type used to read plan files. Create a Reader by calling
|
||||
// Open.
|
||||
|
@ -190,6 +192,50 @@ func (r *Reader) ReadConfig() (*configs.Config, tfdiags.Diagnostics) {
|
|||
return config, diags
|
||||
}
|
||||
|
||||
// ReadDependencyLocks reads the dependency lock information embedded in
|
||||
// the plan file.
|
||||
//
|
||||
// Some test codepaths create plan files without dependency lock information,
|
||||
// but all main codepaths should populate this. If reading a file without
|
||||
// the dependency information, this will return error diagnostics.
|
||||
func (r *Reader) ReadDependencyLocks() (*depsfile.Locks, tfdiags.Diagnostics) {
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
for _, file := range r.zip.File {
|
||||
if file.Name == dependencyLocksFilename {
|
||||
r, err := file.Open()
|
||||
if err != nil {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Failed to read dependency locks from plan file",
|
||||
fmt.Sprintf("Couldn't read the dependency lock information embedded in the plan file: %s.", err),
|
||||
))
|
||||
return nil, diags
|
||||
}
|
||||
src, err := ioutil.ReadAll(r)
|
||||
if err != nil {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Failed to read dependency locks from plan file",
|
||||
fmt.Sprintf("Couldn't read the dependency lock information embedded in the plan file: %s.", err),
|
||||
))
|
||||
return nil, diags
|
||||
}
|
||||
locks, moreDiags := depsfile.LoadLocksFromBytes(src, "<saved-plan>")
|
||||
diags = diags.Append(moreDiags)
|
||||
return locks, diags
|
||||
}
|
||||
}
|
||||
|
||||
// If we fall out here then this is a file without dependency information.
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Saved plan has no dependency lock information",
|
||||
"The specified saved plan file does not include any dependency lock information. This is a bug in the previous run of Terraform that created this file.",
|
||||
))
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
// Close closes the file, after which no other operations may be performed.
|
||||
func (r *Reader) Close() error {
|
||||
return r.zip.Close()
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/configs/configload"
|
||||
"github.com/hashicorp/terraform/internal/depsfile"
|
||||
"github.com/hashicorp/terraform/internal/plans"
|
||||
"github.com/hashicorp/terraform/internal/states/statefile"
|
||||
)
|
||||
|
@ -33,6 +34,11 @@ type CreateArgs struct {
|
|||
// Plan records the plan itself, which is the main artifact inside a
|
||||
// saved plan file.
|
||||
Plan *plans.Plan
|
||||
|
||||
// DependencyLocks records the dependency lock information that we
|
||||
// checked prior to creating the plan, so we can make sure that all of the
|
||||
// same dependencies are still available when applying the plan.
|
||||
DependencyLocks *depsfile.Locks
|
||||
}
|
||||
|
||||
// Create creates a new plan file with the given filename, overwriting any
|
||||
|
@ -108,5 +114,26 @@ func Create(filename string, args CreateArgs) error {
|
|||
}
|
||||
}
|
||||
|
||||
// .terraform.lock.hcl file, containing dependency lock information
|
||||
if args.DependencyLocks != nil { // (this was a later addition, so not all callers set it, but main callers should)
|
||||
src, diags := depsfile.SaveLocksToBytes(args.DependencyLocks)
|
||||
if diags.HasErrors() {
|
||||
return fmt.Errorf("failed to write embedded dependency lock file: %s", diags.Err().Error())
|
||||
}
|
||||
|
||||
w, err := zw.CreateHeader(&zip.FileHeader{
|
||||
Name: dependencyLocksFilename,
|
||||
Method: zip.Deflate,
|
||||
Modified: time.Now(),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create embedded dependency lock file: %s", err)
|
||||
}
|
||||
_, err = w.Write(src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write embedded dependency lock file: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue