Skip to content

Commit 41122c6

Browse files
dylanratcliffeactions-user
authored andcommitted
Added better error when the user gives us a binary plan file (#2845)
GitOrigin-RevId: e4fe4c5010c5d4708f4d98a2659bc078710907a7
1 parent 9db6ee1 commit 41122c6

File tree

3 files changed

+139
-3
lines changed

3 files changed

+139
-3
lines changed

tfutils/plan_mapper.go

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,14 +107,18 @@ func (r *PlanMappingResult) numStatus(status MapStatus) int {
107107
}
108108

109109
func MappedItemDiffsFromPlanFile(ctx context.Context, fileName string, scope string, lf log.Fields) (*PlanMappingResult, error) {
110-
// read results from `terraform show -json ${tfplan file}`
111-
planJSON, err := os.ReadFile(fileName)
110+
planData, err := os.ReadFile(fileName)
112111
if err != nil {
113112
log.WithContext(ctx).WithError(err).WithFields(lf).Error("Failed to read terraform plan")
114113
return nil, err
115114
}
116115

117-
return MappedItemDiffsFromPlan(ctx, planJSON, fileName, scope, lf)
116+
// Check if this is a JSON plan file
117+
if !isJSONPlanFile(planData) {
118+
return nil, fmt.Errorf("plan file '%s' appears to be in binary format, but Overmind only supports JSON plan files.\n\nTo fix this, convert your binary plan to JSON format:\n 1. Using OpenTofu: tofu show -json %s > plan.json\n 2. Using Terraform: terraform show -json %s > plan.json\n 3. Then run: overmind changes submit-plan plan.json", fileName, fileName, fileName)
119+
}
120+
121+
return MappedItemDiffsFromPlan(ctx, planData, fileName, scope, lf)
118122
}
119123

120124
type TfMapData struct {
@@ -343,6 +347,20 @@ func mapResourceToQuery(itemDiff *sdp.ItemDiff, terraformResource *Resource, map
343347
}
344348
}
345349

350+
// isJSONPlanFile checks if the supplied bytes are valid JSON that could be a plan file.
351+
// This is used to determine if we need to convert a binary plan or if it's already JSON.
352+
func isJSONPlanFile(bytes []byte) bool {
353+
var jsonValue interface{}
354+
355+
err := json.Unmarshal(bytes, &jsonValue)
356+
if err != nil {
357+
return false
358+
}
359+
360+
// If it's valid JSON, we can try to parse it as a plan
361+
return true
362+
}
363+
346364
// Checks if the supplied JSON bytes are a state file. It's a common mistake to
347365
// pass a state file to Overmind rather than a plan file since the commands to
348366
// create them are similar

tfutils/plan_mapper_test.go

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"encoding/json"
66
"fmt"
7+
"os"
78
"strconv"
89
"strings"
910
"testing"
@@ -748,3 +749,120 @@ func terraformDig(srcMapPtr interface{}, path string) interface{} {
748749
return terraformDig(&valueMap, strings.Join(parts[1:], "."))
749750
}
750751
}
752+
753+
func TestIsJSONPlanFile(t *testing.T) {
754+
tests := []struct {
755+
name string
756+
input []byte
757+
expected bool
758+
}{
759+
{
760+
name: "valid JSON object",
761+
input: []byte(`{"format_version": "1.0", "terraform_version": "1.0.0"}`),
762+
expected: true,
763+
},
764+
{
765+
name: "valid JSON array",
766+
input: []byte(`[{"key": "value"}]`),
767+
expected: true,
768+
},
769+
{
770+
name: "valid JSON string",
771+
input: []byte(`"hello world"`),
772+
expected: true,
773+
},
774+
{
775+
name: "valid JSON number",
776+
input: []byte(`42`),
777+
expected: true,
778+
},
779+
{
780+
name: "valid JSON boolean",
781+
input: []byte(`true`),
782+
expected: true,
783+
},
784+
{
785+
name: "valid JSON null",
786+
input: []byte(`null`),
787+
expected: true,
788+
},
789+
{
790+
name: "invalid JSON - binary data",
791+
input: []byte{0x50, 0x4B, 0x03, 0x04}, // ZIP file header
792+
expected: false,
793+
},
794+
{
795+
name: "invalid JSON - incomplete",
796+
input: []byte(`{"incomplete":`),
797+
expected: false,
798+
},
799+
{
800+
name: "empty input",
801+
input: []byte(``),
802+
expected: false,
803+
},
804+
{
805+
name: "non-JSON text",
806+
input: []byte(`this is not json`),
807+
expected: false,
808+
},
809+
}
810+
811+
for _, tt := range tests {
812+
t.Run(tt.name, func(t *testing.T) {
813+
result := isJSONPlanFile(tt.input)
814+
require.Equal(t, tt.expected, result, "isJSONPlanFile(%q) = %v, want %v", string(tt.input), result, tt.expected)
815+
})
816+
}
817+
}
818+
819+
func TestMappedItemDiffsFromPlanFileWithJSON(t *testing.T) {
820+
// Test that existing JSON plan files still work
821+
result, err := MappedItemDiffsFromPlanFile(context.Background(), "testdata/plan.json", "test-scope", log.Fields{})
822+
823+
// This should work if the test data exists and is valid JSON
824+
if err != nil {
825+
// If the test data doesn't exist or is invalid, that's okay for this test
826+
// We're mainly testing that the JSON path is taken
827+
t.Logf("Expected error for test data: %v", err)
828+
} else {
829+
require.NotNil(t, result)
830+
}
831+
}
832+
833+
func TestMappedItemDiffsFromPlanFileWithRealBinaryPlan(t *testing.T) {
834+
// Test with the real binary plan file we created
835+
binaryPlanPath := "testdata/binary-plan.tfplan"
836+
837+
// Check if the test file exists
838+
if _, err := os.Stat(binaryPlanPath); os.IsNotExist(err) {
839+
t.Skip("Skipping test: real binary plan file not found. Run 'make test-binary-plan' to generate it.")
840+
}
841+
842+
// Test that the binary version is detected correctly and returns a clear error
843+
t.Run("Binary_plan_detection", func(t *testing.T) {
844+
// Read the binary plan file
845+
binaryData, err := os.ReadFile(binaryPlanPath)
846+
require.NoError(t, err)
847+
848+
// Verify it's detected as binary (not JSON)
849+
isJSON := isJSONPlanFile(binaryData)
850+
require.False(t, isJSON, "Binary plan should not be detected as JSON")
851+
852+
t.Logf("Binary plan correctly detected as non-JSON (size: %d bytes)", len(binaryData))
853+
})
854+
855+
// Test that the binary plan returns a clear error message
856+
t.Run("Binary_plan_error", func(t *testing.T) {
857+
_, err := MappedItemDiffsFromPlanFile(context.Background(), binaryPlanPath, "test-scope", log.Fields{})
858+
859+
// We expect this to fail with a clear error message
860+
require.Error(t, err)
861+
require.Contains(t, err.Error(), "appears to be in binary format, but Overmind only supports JSON plan files")
862+
require.Contains(t, err.Error(), "tofu show -json")
863+
require.Contains(t, err.Error(), "terraform show -json")
864+
require.Contains(t, err.Error(), "overmind changes submit-plan plan.json")
865+
866+
t.Logf("Binary plan correctly rejected with clear error message")
867+
})
868+
}
2.33 KB
Binary file not shown.

0 commit comments

Comments
 (0)