Files
opentf/internal/command/e2etest/encryption_test.go
Klopklopi 08f71e93c5 Encryption improve error messages (#2595)
Signed-off-by: Hugo JOUBERT <hugo.joubert@ippon.fr>
Signed-off-by: Klopklopi <76015884+Klopklopi@users.noreply.github.com>
Signed-off-by: Hugo JOUBERT <hugo.joubert4@gmail.com>
Signed-off-by: Hugo JOUBERT <hugojklop52@gmail.com>
Co-authored-by: Hugo JOUBERT <hugo.joubert@ippon.fr>
Co-authored-by: Hugo JOUBERT <hugo.joubert4@gmail.com>
Co-authored-by: Andrei Ciobanu <andreic9203@gmail.com>
2025-05-07 10:28:28 -04:00

253 lines
6.6 KiB
Go

// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package e2etest
import (
"fmt"
"os"
"path/filepath"
"runtime/debug"
"strings"
"testing"
"github.com/opentofu/opentofu/internal/e2e"
)
type tofuResult struct {
t *testing.T
stdout string
stderr string
err error
}
func (r tofuResult) Success() tofuResult {
if r.stderr != "" {
debug.PrintStack()
r.t.Fatalf("unexpected stderr output:\n%s", r.stderr)
}
if r.err != nil {
debug.PrintStack()
r.t.Fatalf("unexpected error: %s", r.err)
}
return r
}
func (r tofuResult) Failure() tofuResult {
if r.err == nil {
debug.PrintStack()
r.t.Fatal("expected error")
}
return r
}
func SanitizeStderr(msg string) string {
// ANSI escape sequence regex for removing terminal color codes and control characters
msg = stripAnsi(msg)
//Pipe and carriage return replacement in order to correctly sanitze the stderr output
msg = strings.ReplaceAll(
strings.ReplaceAll(msg, "│", ""),
"\n", "",
)
return msg
}
func (r tofuResult) StderrContains(msg string) tofuResult {
stdErrSanitized := SanitizeStderr(r.stderr)
if !strings.Contains(stdErrSanitized, msg) {
debug.PrintStack()
r.t.Fatalf("expected stderr output %q:\n%s", msg, stdErrSanitized)
}
return r
}
func (r tofuResult) Contains(msg string) tofuResult {
if !strings.Contains(r.stdout, msg) {
debug.PrintStack()
r.t.Fatalf("expected output %q:\n%s", msg, r.stdout)
}
return r
}
// This test covers the scenario where a user migrates an existing project
// to having encryption enabled, uses it, then migrates back to encryption
// disabled
func TestEncryptionFlow(t *testing.T) {
// This test reaches out to registry.opentofu.org to download the
// mock provider, so it can only run if network access is allowed
skipIfCannotAccessNetwork(t)
// There is a lot of setup / helpers defined. Actual test logic is below.
fixturePath := filepath.Join("testdata", "encryption-flow")
tf := e2e.NewBinary(t, tofuBin, fixturePath)
// tofu init
_, stderr, err := tf.Run("init")
if err != nil {
t.Errorf("unexpected error: %s", err)
}
if stderr != "" {
t.Errorf("unexpected stderr output:\n%s", stderr)
}
iter := 0
run := func(args ...string) tofuResult {
stdout, stderr, err := tf.Run(args...)
return tofuResult{t, stdout, stderr, err}
}
apply := func() tofuResult {
iter += 1
return run("apply", fmt.Sprintf("-var=iter=%v", iter), "-auto-approve")
}
createPlan := func(planfile string) tofuResult {
iter += 1
return run("plan", fmt.Sprintf("-var=iter=%v", iter), "-out="+planfile)
}
applyPlan := func(planfile string) tofuResult {
return run("apply", "-auto-approve", planfile)
}
requireUnencryptedState := func() {
_, err = tf.LocalState()
if err != nil {
t.Fatalf("expected unencrypted state file: %q", err)
}
}
requireEncryptedState := func() {
_, err = tf.LocalState()
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" {
t.Fatalf("expected encrypted state file: %q", err)
}
}
with := func(path string, fn func()) {
src := tf.Path(path + ".disabled")
dst := tf.Path(path)
err := os.Rename(src, dst)
if err != nil {
t.Fatalf("%s", err.Error())
}
fn()
err = os.Rename(dst, src)
if err != nil {
t.Fatalf("%s", err.Error())
}
}
// Actual test begins HERE
// NOTE: state plans are still readable and tests the encryption state
unencryptedPlan := "unencrypted.tfplan"
encryptedPlan := "encrypted.tfplan"
{
// Everything works before adding encryption configuration
apply().Success()
requireUnencryptedState()
// Check read/write of state file
apply().Success()
requireUnencryptedState()
// Save an unencrypted plan
createPlan(unencryptedPlan).Success()
// Validate unencrypted plan
applyPlan(unencryptedPlan).Success()
requireUnencryptedState()
}
with("required.tf", func() {
// Can't switch directly to encryption, need to migrate
apply().Failure().StderrContains("encountered unencrypted payload without unencrypted method")
requireUnencryptedState()
})
with("migrateto.tf", func() {
// Migrate to using encryption
apply().Success()
requireEncryptedState()
// Make changes and confirm it's still encrypted (even with migration enabled)
apply().Success()
requireEncryptedState()
// Save an encrypted plan
createPlan(encryptedPlan).Success()
// Apply encrypted plan (with migration active)
applyPlan(encryptedPlan).Success()
requireEncryptedState()
// Apply unencrypted plan (with migration active)
applyPlan(unencryptedPlan).StderrContains("Saved plan is stale")
requireEncryptedState()
})
{
// Unconfigured encryption clearly fails on encrypted state
apply().Failure().StderrContains("can not be read without an encryption configuration")
}
with("required.tf", func() {
// Encryption works with fallback removed
apply().Success()
requireEncryptedState()
// Can't apply unencrypted plan
applyPlan(unencryptedPlan).Failure().StderrContains("encountered unencrypted payload without unencrypted method")
requireEncryptedState()
// Apply encrypted plan
applyPlan(encryptedPlan).StderrContains("Saved plan is stale")
requireEncryptedState()
})
with("broken.tf", func() {
// Make sure changes to encryption keys notify the user correctly
apply().Failure().StderrContains("decryption failed for state")
requireEncryptedState()
applyPlan(encryptedPlan).Failure().StderrContains("decryption failed: cipher: message authentication failed")
requireEncryptedState()
})
with("migratefrom.tf", func() {
// Apply migration from encrypted state
apply().Success()
requireUnencryptedState()
// Make changes and confirm it's still encrypted (even with migration enabled)
apply().Success()
requireUnencryptedState()
// Apply unencrypted plan (with migration active)
applyPlan(unencryptedPlan).StderrContains("Saved plan is stale")
requireUnencryptedState()
// Apply encrypted plan (with migration active)
applyPlan(encryptedPlan).StderrContains("Saved plan is stale")
requireUnencryptedState()
})
{
// Back to no encryption configuration with unencrypted state
apply().Success()
requireUnencryptedState()
// Apply unencrypted plan
applyPlan(unencryptedPlan).StderrContains("Saved plan is stale")
requireUnencryptedState()
// Can't apply encrypted plan
applyPlan(encryptedPlan).Failure().StderrContains("the given plan file is encrypted and requires a valid encryption")
requireUnencryptedState()
}
}