github.com/opentofu/opentofu@v1.7.1/internal/command/e2etest/encryption_test.go (about)

     1  package e2etest
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"path/filepath"
     7  	"runtime/debug"
     8  	"strings"
     9  	"testing"
    10  
    11  	"github.com/opentofu/opentofu/internal/e2e"
    12  )
    13  
    14  type tofuResult struct {
    15  	t *testing.T
    16  
    17  	stdout string
    18  	stderr string
    19  	err    error
    20  }
    21  
    22  func (r tofuResult) Success() tofuResult {
    23  	if r.stderr != "" {
    24  		debug.PrintStack()
    25  		r.t.Fatalf("unexpected stderr output:\n%s", r.stderr)
    26  	}
    27  	if r.err != nil {
    28  		debug.PrintStack()
    29  		r.t.Fatalf("unexpected error: %s", r.err)
    30  	}
    31  
    32  	return r
    33  }
    34  
    35  func (r tofuResult) Failure() tofuResult {
    36  	if r.err == nil {
    37  		debug.PrintStack()
    38  		r.t.Fatal("expected error")
    39  	}
    40  	return r
    41  }
    42  
    43  func (r tofuResult) StderrContains(msg string) tofuResult {
    44  	if !strings.Contains(r.stderr, msg) {
    45  		debug.PrintStack()
    46  		r.t.Fatalf("expected stderr output %q:\n%s", msg, r.stderr)
    47  	}
    48  	return r
    49  }
    50  
    51  // This test covers the scenario where a user migrates an existing project
    52  // to having encryption enabled, uses it, then migrates back to encryption
    53  // disabled
    54  func TestEncryptionFlow(t *testing.T) {
    55  
    56  	// This test reaches out to registry.opentofu.org to download the
    57  	// mock provider, so it can only run if network access is allowed
    58  	skipIfCannotAccessNetwork(t)
    59  
    60  	// There is a lot of setup / helpers defined.  Actual test logic is below.
    61  
    62  	fixturePath := filepath.Join("testdata", "encryption-flow")
    63  	tf := e2e.NewBinary(t, tofuBin, fixturePath)
    64  
    65  	// tofu init
    66  	_, stderr, err := tf.Run("init")
    67  	if err != nil {
    68  		t.Errorf("unexpected error: %s", err)
    69  	}
    70  	if stderr != "" {
    71  		t.Errorf("unexpected stderr output:\n%s", stderr)
    72  	}
    73  
    74  	iter := 0
    75  
    76  	run := func(args ...string) tofuResult {
    77  		stdout, stderr, err := tf.Run(args...)
    78  		return tofuResult{t, stdout, stderr, err}
    79  	}
    80  	apply := func() tofuResult {
    81  		iter += 1
    82  		return run("apply", fmt.Sprintf("-var=iter=%v", iter), "-auto-approve")
    83  	}
    84  
    85  	createPlan := func(planfile string) tofuResult {
    86  		iter += 1
    87  		return run("plan", fmt.Sprintf("-var=iter=%v", iter), "-out="+planfile)
    88  	}
    89  	applyPlan := func(planfile string) tofuResult {
    90  		return run("apply", "-auto-approve", planfile)
    91  	}
    92  
    93  	requireUnencryptedState := func() {
    94  		_, err = tf.LocalState()
    95  		if err != nil {
    96  			t.Fatalf("expected unencrypted state file: %q", err)
    97  		}
    98  	}
    99  	requireEncryptedState := func() {
   100  		_, err = tf.LocalState()
   101  		if err == nil || err.Error() != "Error reading statefile: Unsupported state file format: This state file is encrypted and can not be read without an encryption configuration" {
   102  			t.Fatalf("expected encrypted state file: %q", err)
   103  		}
   104  	}
   105  
   106  	with := func(path string, fn func()) {
   107  		src := tf.Path(path + ".disabled")
   108  		dst := tf.Path(path)
   109  
   110  		err := os.Rename(src, dst)
   111  		if err != nil {
   112  			t.Fatalf(err.Error())
   113  		}
   114  
   115  		fn()
   116  
   117  		err = os.Rename(dst, src)
   118  		if err != nil {
   119  			t.Fatalf(err.Error())
   120  		}
   121  	}
   122  
   123  	// Actual test begins HERE
   124  	// NOTE: state plans are still readable and tests the encryption state
   125  
   126  	unencryptedPlan := "unencrypted.tfplan"
   127  	encryptedPlan := "encrypted.tfplan"
   128  
   129  	{
   130  		// Everything works before adding encryption configuration
   131  		apply().Success()
   132  		requireUnencryptedState()
   133  		// Check read/write of state file
   134  		apply().Success()
   135  		requireUnencryptedState()
   136  
   137  		// Save an unencrypted plan
   138  		createPlan(unencryptedPlan).Success()
   139  		// Validate unencrypted plan
   140  		applyPlan(unencryptedPlan).Success()
   141  		requireUnencryptedState()
   142  	}
   143  
   144  	with("required.tf", func() {
   145  		// Can't switch directly to encryption, need to migrate
   146  		apply().Failure().StderrContains("encountered unencrypted payload without unencrypted method")
   147  		requireUnencryptedState()
   148  	})
   149  
   150  	with("migrateto.tf", func() {
   151  		// Migrate to using encryption
   152  		apply().Success()
   153  		requireEncryptedState()
   154  		// Make changes and confirm it's still encrypted (even with migration enabled)
   155  		apply().Success()
   156  		requireEncryptedState()
   157  
   158  		// Save an encrypted plan
   159  		createPlan(encryptedPlan).Success()
   160  
   161  		// Apply encrypted plan (with migration active)
   162  		applyPlan(encryptedPlan).Success()
   163  		requireEncryptedState()
   164  		// Apply unencrypted plan (with migration active)
   165  		applyPlan(unencryptedPlan).StderrContains("Saved plan is stale")
   166  		requireEncryptedState()
   167  	})
   168  
   169  	{
   170  		// Unconfigured encryption clearly fails on encrypted state
   171  		apply().Failure().StderrContains("can not be read without an encryption configuration")
   172  	}
   173  
   174  	with("required.tf", func() {
   175  		// Encryption works with fallback removed
   176  		apply().Success()
   177  		requireEncryptedState()
   178  
   179  		// Can't apply unencrypted plan
   180  		applyPlan(unencryptedPlan).Failure().StderrContains("encountered unencrypted payload without unencrypted method")
   181  		requireEncryptedState()
   182  
   183  		// Apply encrypted plan
   184  		applyPlan(encryptedPlan).StderrContains("Saved plan is stale")
   185  		requireEncryptedState()
   186  	})
   187  
   188  	with("broken.tf", func() {
   189  		// Make sure changes to encryption keys notify the user correctly
   190  		apply().Failure().StderrContains("decryption failed for state")
   191  		requireEncryptedState()
   192  
   193  		applyPlan(encryptedPlan).Failure().StderrContains("decryption failed: cipher: message authentication failed")
   194  		requireEncryptedState()
   195  	})
   196  
   197  	with("migratefrom.tf", func() {
   198  		// Apply migration from encrypted state
   199  		apply().Success()
   200  		requireUnencryptedState()
   201  		// Make changes and confirm it's still encrypted (even with migration enabled)
   202  		apply().Success()
   203  		requireUnencryptedState()
   204  
   205  		// Apply unencrypted plan (with migration active)
   206  		applyPlan(unencryptedPlan).StderrContains("Saved plan is stale")
   207  		requireUnencryptedState()
   208  
   209  		// Apply encrypted plan (with migration active)
   210  		applyPlan(encryptedPlan).StderrContains("Saved plan is stale")
   211  		requireUnencryptedState()
   212  	})
   213  
   214  	{
   215  		// Back to no encryption configuration with unencrypted state
   216  		apply().Success()
   217  		requireUnencryptedState()
   218  
   219  		// Apply unencrypted plan
   220  		applyPlan(unencryptedPlan).StderrContains("Saved plan is stale")
   221  		requireUnencryptedState()
   222  		// Can't apply encrypted plan
   223  		applyPlan(encryptedPlan).Failure().StderrContains("the given plan file is encrypted and requires a valid encryption")
   224  		requireUnencryptedState()
   225  	}
   226  }