github.com/opentofu/opentofu@v1.7.1/internal/plans/planfile/writer.go (about)

     1  // Copyright (c) The OpenTofu Authors
     2  // SPDX-License-Identifier: MPL-2.0
     3  // Copyright (c) 2023 HashiCorp, Inc.
     4  // SPDX-License-Identifier: MPL-2.0
     5  
     6  package planfile
     7  
     8  import (
     9  	"archive/zip"
    10  	"bytes"
    11  	"fmt"
    12  	"os"
    13  	"time"
    14  
    15  	"github.com/opentofu/opentofu/internal/configs/configload"
    16  	"github.com/opentofu/opentofu/internal/depsfile"
    17  	"github.com/opentofu/opentofu/internal/encryption"
    18  	"github.com/opentofu/opentofu/internal/plans"
    19  	"github.com/opentofu/opentofu/internal/states/statefile"
    20  )
    21  
    22  type CreateArgs struct {
    23  	// ConfigSnapshot is a snapshot of the configuration that the plan
    24  	// was created from.
    25  	ConfigSnapshot *configload.Snapshot
    26  
    27  	// PreviousRunStateFile is a representation of the state snapshot we used
    28  	// as the original input when creating this plan, containing the same
    29  	// information as recorded at the end of the previous apply except for
    30  	// upgrading managed resource instance data to the provider's latest
    31  	// schema versions.
    32  	PreviousRunStateFile *statefile.File
    33  
    34  	// BaseStateFile is a representation of the state snapshot we used to
    35  	// create the plan, which is the result of asking the providers to refresh
    36  	// all previously-stored objects to match the current situation in the
    37  	// remote system. (If this plan was created with refreshing disabled,
    38  	// this should be the same as PreviousRunStateFile.)
    39  	StateFile *statefile.File
    40  
    41  	// Plan records the plan itself, which is the main artifact inside a
    42  	// saved plan file.
    43  	Plan *plans.Plan
    44  
    45  	// DependencyLocks records the dependency lock information that we
    46  	// checked prior to creating the plan, so we can make sure that all of the
    47  	// same dependencies are still available when applying the plan.
    48  	DependencyLocks *depsfile.Locks
    49  }
    50  
    51  // Create creates a new plan file with the given filename, overwriting any
    52  // file that might already exist there.
    53  //
    54  // A plan file contains both a snapshot of the configuration and of the latest
    55  // state file in addition to the plan itself, so that OpenTofu can detect
    56  // if the world has changed since the plan was created and thus refuse to
    57  // apply it.
    58  func Create(filename string, args CreateArgs, enc encryption.PlanEncryption) error {
    59  	buff := bytes.NewBuffer(make([]byte, 0))
    60  	zw := zip.NewWriter(buff)
    61  
    62  	// tfplan file
    63  	{
    64  		w, err := zw.CreateHeader(&zip.FileHeader{
    65  			Name:     tfplanFilename,
    66  			Method:   zip.Deflate,
    67  			Modified: time.Now(),
    68  		})
    69  		if err != nil {
    70  			return fmt.Errorf("failed to create tfplan file: %w", err)
    71  		}
    72  		err = writeTfplan(args.Plan, w)
    73  		if err != nil {
    74  			return fmt.Errorf("failed to write plan: %w", err)
    75  		}
    76  	}
    77  
    78  	// tfstate file
    79  	{
    80  		w, err := zw.CreateHeader(&zip.FileHeader{
    81  			Name:     tfstateFilename,
    82  			Method:   zip.Deflate,
    83  			Modified: time.Now(),
    84  		})
    85  		if err != nil {
    86  			return fmt.Errorf("failed to create embedded tfstate file: %w", err)
    87  		}
    88  		err = statefile.Write(args.StateFile, w, encryption.StateEncryptionDisabled())
    89  		if err != nil {
    90  			return fmt.Errorf("failed to write state snapshot: %w", err)
    91  		}
    92  	}
    93  
    94  	// tfstate-prev file
    95  	{
    96  		w, err := zw.CreateHeader(&zip.FileHeader{
    97  			Name:     tfstatePreviousFilename,
    98  			Method:   zip.Deflate,
    99  			Modified: time.Now(),
   100  		})
   101  		if err != nil {
   102  			return fmt.Errorf("failed to create embedded tfstate-prev file: %w", err)
   103  		}
   104  		err = statefile.Write(args.PreviousRunStateFile, w, encryption.StateEncryptionDisabled())
   105  		if err != nil {
   106  			return fmt.Errorf("failed to write previous state snapshot: %w", err)
   107  		}
   108  	}
   109  
   110  	// tfconfig directory
   111  	{
   112  		err := writeConfigSnapshot(args.ConfigSnapshot, zw)
   113  		if err != nil {
   114  			return fmt.Errorf("failed to write config snapshot: %w", err)
   115  		}
   116  	}
   117  
   118  	// .terraform.lock.hcl file, containing dependency lock information
   119  	if args.DependencyLocks != nil { // (this was a later addition, so not all callers set it, but main callers should)
   120  		src, diags := depsfile.SaveLocksToBytes(args.DependencyLocks)
   121  		if diags.HasErrors() {
   122  			return fmt.Errorf("failed to write embedded dependency lock file: %w", diags.Err())
   123  		}
   124  
   125  		w, err := zw.CreateHeader(&zip.FileHeader{
   126  			Name:     dependencyLocksFilename,
   127  			Method:   zip.Deflate,
   128  			Modified: time.Now(),
   129  		})
   130  		if err != nil {
   131  			return fmt.Errorf("failed to create embedded dependency lock file: %w", err)
   132  		}
   133  		_, err = w.Write(src)
   134  		if err != nil {
   135  			return fmt.Errorf("failed to write embedded dependency lock file: %w", err)
   136  		}
   137  	}
   138  
   139  	// Finish zip file
   140  	zw.Close()
   141  	// Encrypt payload
   142  	encrypted, err := enc.EncryptPlan(buff.Bytes())
   143  	if err != nil {
   144  		return err
   145  	}
   146  	return os.WriteFile(filename, encrypted, 0644)
   147  }