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 }