mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
Integrate encryption into plan serialization (#1292)
Signed-off-by: Christian Mesh <christianmesh1@gmail.com>
This commit is contained in:
parent
997e5fa46e
commit
ac3ed86617
@ -19,6 +19,7 @@ import (
|
||||
"github.com/opentofu/opentofu/internal/command/views"
|
||||
"github.com/opentofu/opentofu/internal/configs/configload"
|
||||
"github.com/opentofu/opentofu/internal/configs/configschema"
|
||||
"github.com/opentofu/opentofu/internal/encryption"
|
||||
"github.com/opentofu/opentofu/internal/initwd"
|
||||
"github.com/opentofu/opentofu/internal/plans"
|
||||
"github.com/opentofu/opentofu/internal/plans/planfile"
|
||||
@ -97,7 +98,7 @@ func TestLocalRun_cloudPlan(t *testing.T) {
|
||||
|
||||
planPath := "./testdata/plan-bookmark/bookmark.json"
|
||||
|
||||
planFile, err := planfile.OpenWrapped(planPath)
|
||||
planFile, err := planfile.OpenWrapped(planPath, encryption.PlanEncryptionDisabled())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error reading planfile: %s", err)
|
||||
}
|
||||
@ -180,10 +181,10 @@ func TestLocalRun_stalePlan(t *testing.T) {
|
||||
StateFile: stateFile,
|
||||
Plan: plan,
|
||||
}
|
||||
if err := planfile.Create(planPath, planfileArgs); err != nil {
|
||||
if err := planfile.Create(planPath, planfileArgs, encryption.PlanEncryptionDisabled()); err != nil {
|
||||
t.Fatalf("unexpected error writing planfile: %s", err)
|
||||
}
|
||||
planFile, err := planfile.OpenWrapped(planPath)
|
||||
planFile, err := planfile.OpenWrapped(planPath, encryption.PlanEncryptionDisabled())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error reading planfile: %s", err)
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import (
|
||||
"log"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/backend"
|
||||
"github.com/opentofu/opentofu/internal/encryption"
|
||||
"github.com/opentofu/opentofu/internal/genconfig"
|
||||
"github.com/opentofu/opentofu/internal/logging"
|
||||
"github.com/opentofu/opentofu/internal/plans"
|
||||
@ -172,7 +173,7 @@ func (b *Local) opPlan(
|
||||
StateFile: plannedStateFile,
|
||||
Plan: plan,
|
||||
DependencyLocks: op.DependencyLocks,
|
||||
})
|
||||
}, encryption.PlanEncryptionTODO())
|
||||
if err != nil {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
|
@ -21,6 +21,7 @@ import (
|
||||
"github.com/opentofu/opentofu/internal/command/views"
|
||||
"github.com/opentofu/opentofu/internal/configs/configschema"
|
||||
"github.com/opentofu/opentofu/internal/depsfile"
|
||||
"github.com/opentofu/opentofu/internal/encryption"
|
||||
"github.com/opentofu/opentofu/internal/initwd"
|
||||
"github.com/opentofu/opentofu/internal/plans"
|
||||
"github.com/opentofu/opentofu/internal/plans/planfile"
|
||||
@ -841,11 +842,10 @@ func testPlanState_tainted() *states.State {
|
||||
func testReadPlan(t *testing.T, path string) *plans.Plan {
|
||||
t.Helper()
|
||||
|
||||
p, err := planfile.Open(path)
|
||||
p, err := planfile.Open(path, encryption.PlanEncryptionDisabled())
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
defer p.Close()
|
||||
|
||||
plan, err := p.ReadPlan()
|
||||
if err != nil {
|
||||
|
@ -40,6 +40,7 @@ import (
|
||||
"github.com/opentofu/opentofu/internal/configs/configschema"
|
||||
"github.com/opentofu/opentofu/internal/copy"
|
||||
"github.com/opentofu/opentofu/internal/depsfile"
|
||||
"github.com/opentofu/opentofu/internal/encryption"
|
||||
"github.com/opentofu/opentofu/internal/getproviders"
|
||||
"github.com/opentofu/opentofu/internal/initwd"
|
||||
legacy "github.com/opentofu/opentofu/internal/legacy/tofu"
|
||||
@ -228,7 +229,7 @@ func testPlanFileMatchState(t *testing.T, configSnap *configload.Snapshot, state
|
||||
StateFile: stateFile,
|
||||
Plan: plan,
|
||||
DependencyLocks: depsfile.NewLocks(),
|
||||
})
|
||||
}, encryption.PlanEncryptionDisabled())
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temporary plan file: %s", err)
|
||||
}
|
||||
@ -276,11 +277,10 @@ func testFileEquals(t *testing.T, got, want string) {
|
||||
func testReadPlan(t *testing.T, path string) *plans.Plan {
|
||||
t.Helper()
|
||||
|
||||
f, err := planfile.Open(path)
|
||||
f, err := planfile.Open(path, encryption.PlanEncryptionDisabled())
|
||||
if err != nil {
|
||||
t.Fatalf("error opening plan file %q: %s", path, err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
p, err := f.ReadPlan()
|
||||
if err != nil {
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/encryption"
|
||||
"github.com/opentofu/opentofu/internal/plans/planfile"
|
||||
)
|
||||
|
||||
@ -48,5 +49,5 @@ func (m *Meta) PlanFile(path string) (*planfile.WrappedPlanFile, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return planfile.OpenWrapped(path)
|
||||
return planfile.OpenWrapped(path, encryption.PlanEncryptionTODO())
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ import (
|
||||
"github.com/opentofu/opentofu/internal/command/arguments"
|
||||
"github.com/opentofu/opentofu/internal/command/views"
|
||||
"github.com/opentofu/opentofu/internal/configs"
|
||||
"github.com/opentofu/opentofu/internal/encryption"
|
||||
"github.com/opentofu/opentofu/internal/plans"
|
||||
"github.com/opentofu/opentofu/internal/plans/planfile"
|
||||
"github.com/opentofu/opentofu/internal/states/statefile"
|
||||
@ -272,7 +273,7 @@ func (c *ShowCommand) getPlanFromPath(path string) (*plans.Plan, *cloudplan.Remo
|
||||
var stateFile *statefile.File
|
||||
var config *configs.Config
|
||||
|
||||
pf, err := planfile.OpenWrapped(path)
|
||||
pf, err := planfile.OpenWrapped(path, encryption.PlanEncryptionTODO())
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/encryption"
|
||||
"github.com/opentofu/opentofu/internal/plans"
|
||||
"github.com/opentofu/opentofu/internal/plans/planfile"
|
||||
"github.com/opentofu/opentofu/internal/states"
|
||||
@ -202,11 +203,10 @@ func (b *binary) StateFromFile(filename string) (*states.State, error) {
|
||||
// Plan is a helper for easily reading a plan file from the working directory.
|
||||
func (b *binary) Plan(path string) (*plans.Plan, error) {
|
||||
path = b.Path(path)
|
||||
pr, err := planfile.Open(path)
|
||||
pr, err := planfile.Open(path, encryption.PlanEncryptionDisabled())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer pr.Close()
|
||||
plan, err := pr.ReadPlan()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -67,3 +67,21 @@ func (p planEncryption) DecryptPlan(data []byte) ([]byte, error) {
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func PlanEncryptionDisabled() PlanEncryption {
|
||||
return &planDisabled{}
|
||||
}
|
||||
|
||||
type planDisabled struct{}
|
||||
|
||||
func (s *planDisabled) EncryptPlan(plainPlan []byte) ([]byte, error) {
|
||||
return plainPlan, nil
|
||||
}
|
||||
func (s *planDisabled) DecryptPlan(encryptedPlan []byte) ([]byte, error) {
|
||||
return encryptedPlan, nil
|
||||
}
|
||||
|
||||
// TODO REMOVEME once plan encryption is fully integrated into the codebase
|
||||
func PlanEncryptionTODO() PlanEncryption {
|
||||
return &planDisabled{}
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ import (
|
||||
"github.com/opentofu/opentofu/internal/addrs"
|
||||
"github.com/opentofu/opentofu/internal/configs/configload"
|
||||
"github.com/opentofu/opentofu/internal/depsfile"
|
||||
"github.com/opentofu/opentofu/internal/encryption"
|
||||
"github.com/opentofu/opentofu/internal/getproviders"
|
||||
"github.com/opentofu/opentofu/internal/plans"
|
||||
"github.com/opentofu/opentofu/internal/states"
|
||||
@ -98,12 +99,12 @@ func TestRoundtrip(t *testing.T) {
|
||||
StateFile: stateFileIn,
|
||||
Plan: planIn,
|
||||
DependencyLocks: locksIn,
|
||||
})
|
||||
}, encryption.PlanEncryptionDisabled())
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create plan file: %s", err)
|
||||
}
|
||||
|
||||
wpf, err := OpenWrapped(planFn)
|
||||
wpf, err := OpenWrapped(planFn, encryption.PlanEncryptionDisabled())
|
||||
if err != nil {
|
||||
t.Fatalf("failed to open plan file for reading: %s", err)
|
||||
}
|
||||
@ -181,14 +182,14 @@ func TestRoundtrip(t *testing.T) {
|
||||
func TestWrappedError(t *testing.T) {
|
||||
// Open something that isn't a cloud or local planfile: should error
|
||||
wrongFile := "not a valid zip file"
|
||||
_, err := OpenWrapped(filepath.Join("testdata", "test-config", "root.tf"))
|
||||
_, err := OpenWrapped(filepath.Join("testdata", "test-config", "root.tf"), encryption.PlanEncryptionDisabled())
|
||||
if !strings.Contains(err.Error(), wrongFile) {
|
||||
t.Fatalf("expected %q, got %q", wrongFile, err)
|
||||
}
|
||||
|
||||
// Open something that doesn't exist: should error
|
||||
missingFile := "no such file or directory"
|
||||
_, err = OpenWrapped(filepath.Join("testdata", "absent.tfplan"))
|
||||
_, err = OpenWrapped(filepath.Join("testdata", "absent.tfplan"), encryption.PlanEncryptionDisabled())
|
||||
if !strings.Contains(err.Error(), missingFile) {
|
||||
t.Fatalf("expected %q, got %q", missingFile, err)
|
||||
}
|
||||
@ -196,7 +197,7 @@ func TestWrappedError(t *testing.T) {
|
||||
|
||||
func TestWrappedCloud(t *testing.T) {
|
||||
// Loading valid cloud plan results in a wrapped cloud plan
|
||||
wpf, err := OpenWrapped(filepath.Join("testdata", "cloudplan.json"))
|
||||
wpf, err := OpenWrapped(filepath.Join("testdata", "cloudplan.json"), encryption.PlanEncryptionDisabled())
|
||||
if err != nil {
|
||||
t.Fatalf("failed to open valid cloud plan: %s", err)
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ import (
|
||||
"github.com/opentofu/opentofu/internal/configs"
|
||||
"github.com/opentofu/opentofu/internal/configs/configload"
|
||||
"github.com/opentofu/opentofu/internal/depsfile"
|
||||
"github.com/opentofu/opentofu/internal/encryption"
|
||||
"github.com/opentofu/opentofu/internal/plans"
|
||||
"github.com/opentofu/opentofu/internal/states/statefile"
|
||||
"github.com/opentofu/opentofu/internal/tfdiags"
|
||||
@ -50,15 +51,25 @@ func (e *ErrUnusableLocalPlan) Unwrap() error {
|
||||
// be used to access the individual portions of the file for further
|
||||
// processing.
|
||||
type Reader struct {
|
||||
zip *zip.ReadCloser
|
||||
zip *zip.Reader
|
||||
}
|
||||
|
||||
// Open creates a Reader for the file at the given filename, or returns an error
|
||||
// if the file doesn't seem to be a planfile. NOTE: Most commands that accept a
|
||||
// plan file should use OpenWrapped instead, so they can support both local and
|
||||
// cloud plan files.
|
||||
func Open(filename string) (*Reader, error) {
|
||||
r, err := zip.OpenReader(filename)
|
||||
func Open(filename string, enc encryption.PlanEncryption) (*Reader, error) {
|
||||
raw, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
decrypted, diags := enc.DecryptPlan(raw)
|
||||
if diags != nil {
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
r, err := zip.NewReader(bytes.NewReader(decrypted), int64(len(decrypted)))
|
||||
if err != nil {
|
||||
// To give a better error message, we'll sniff to see if this looks
|
||||
// like our old plan format from versions prior to 0.12.
|
||||
@ -115,7 +126,6 @@ func (r *Reader) ReadPlan() (*plans.Plan, error) {
|
||||
if err != nil {
|
||||
return nil, errUnusable(fmt.Errorf("failed to retrieve plan from plan file: %w", err))
|
||||
}
|
||||
defer pr.Close()
|
||||
|
||||
// There's a slight mismatch in how plans.Plan is modeled vs. how
|
||||
// the underlying plan file format works, because the "tfplan" embedded
|
||||
@ -190,7 +200,7 @@ func (r *Reader) ReadPrevStateFile() (*statefile.File, error) {
|
||||
// This is a lower-level alternative to ReadConfig that just extracts the
|
||||
// source files, without attempting to parse them.
|
||||
func (r *Reader) ReadConfigSnapshot() (*configload.Snapshot, error) {
|
||||
return readConfigSnapshot(&r.zip.Reader)
|
||||
return readConfigSnapshot(r.zip)
|
||||
}
|
||||
|
||||
// ReadConfig reads the configuration embedded in the plan file.
|
||||
@ -262,8 +272,3 @@ func (r *Reader) ReadDependencyLocks() (*depsfile.Locks, tfdiags.Diagnostics) {
|
||||
))
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
// Close closes the file, after which no other operations may be performed.
|
||||
func (r *Reader) Close() error {
|
||||
return r.zip.Close()
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/cloud/cloudplan"
|
||||
"github.com/opentofu/opentofu/internal/encryption"
|
||||
)
|
||||
|
||||
// WrappedPlanFile is a sum type that represents a saved plan, loaded from a
|
||||
@ -76,9 +77,9 @@ func NewWrappedCloud(c *cloudplan.SavedPlanBookmark) *WrappedPlanFile {
|
||||
// returns an error if the file doesn't seem to be a plan file of either kind.
|
||||
// Most consumers should use this and switch behaviors based on the kind of plan
|
||||
// they expected, rather than directly using Open.
|
||||
func OpenWrapped(filename string) (*WrappedPlanFile, error) {
|
||||
func OpenWrapped(filename string, enc encryption.PlanEncryption) (*WrappedPlanFile, error) {
|
||||
// First, try to load it as a local planfile.
|
||||
local, localErr := Open(filename)
|
||||
local, localErr := Open(filename, enc)
|
||||
if localErr == nil {
|
||||
return &WrappedPlanFile{local: local}, nil
|
||||
}
|
||||
|
@ -7,12 +7,14 @@ package planfile
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/configs/configload"
|
||||
"github.com/opentofu/opentofu/internal/depsfile"
|
||||
"github.com/opentofu/opentofu/internal/encryption"
|
||||
"github.com/opentofu/opentofu/internal/plans"
|
||||
"github.com/opentofu/opentofu/internal/states/statefile"
|
||||
)
|
||||
@ -53,15 +55,9 @@ type CreateArgs struct {
|
||||
// state file in addition to the plan itself, so that OpenTofu can detect
|
||||
// if the world has changed since the plan was created and thus refuse to
|
||||
// apply it.
|
||||
func Create(filename string, args CreateArgs) error {
|
||||
f, err := os.Create(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
zw := zip.NewWriter(f)
|
||||
defer zw.Close()
|
||||
func Create(filename string, args CreateArgs, enc encryption.PlanEncryption) error {
|
||||
buff := bytes.NewBuffer(make([]byte, 0))
|
||||
zw := zip.NewWriter(buff)
|
||||
|
||||
// tfplan file
|
||||
{
|
||||
@ -140,5 +136,12 @@ func Create(filename string, args CreateArgs) error {
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
// Finish zip file
|
||||
zw.Close()
|
||||
// Encrypt payload
|
||||
encrypted, err := enc.EncryptPlan(buff.Bytes())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(filename, encrypted, 0644)
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ import (
|
||||
"github.com/opentofu/opentofu/internal/configs/configload"
|
||||
"github.com/opentofu/opentofu/internal/configs/configschema"
|
||||
"github.com/opentofu/opentofu/internal/configs/hcl2shim"
|
||||
"github.com/opentofu/opentofu/internal/encryption"
|
||||
"github.com/opentofu/opentofu/internal/plans"
|
||||
"github.com/opentofu/opentofu/internal/plans/planfile"
|
||||
"github.com/opentofu/opentofu/internal/providers"
|
||||
@ -716,12 +717,12 @@ func contextOptsForPlanViaFile(t *testing.T, configSnap *configload.Snapshot, pl
|
||||
PreviousRunStateFile: prevStateFile,
|
||||
StateFile: stateFile,
|
||||
Plan: plan,
|
||||
})
|
||||
}, encryption.PlanEncryptionDisabled())
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
pr, err := planfile.Open(filename)
|
||||
pr, err := planfile.Open(filename, encryption.PlanEncryptionDisabled())
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user