|
| 1 | +//go:build linux || darwin |
| 2 | + |
| 3 | +package terraform_test |
| 4 | + |
| 5 | +import ( |
| 6 | +"encoding/json" |
| 7 | +"fmt" |
| 8 | +"os" |
| 9 | +"path/filepath" |
| 10 | +"slices" |
| 11 | +"strings" |
| 12 | +"testing" |
| 13 | + |
| 14 | +tfjson"github.com/hashicorp/terraform-json" |
| 15 | +"github.com/stretchr/testify/require" |
| 16 | + |
| 17 | +"cdr.dev/slog/sloggers/slogtest" |
| 18 | +"github.com/coder/coder/v2/provisioner/terraform" |
| 19 | +"github.com/coder/coder/v2/testutil" |
| 20 | +) |
| 21 | + |
| 22 | +// TestConvertStateGolden compares the output of ConvertState to a golden |
| 23 | +// file to prevent regressions. If the logic changes, update the golden files |
| 24 | +// accordingly. |
| 25 | +// |
| 26 | +// This was created to aid in refactoring `ConvertState`. |
| 27 | +funcTestConvertStateGolden(t*testing.T) { |
| 28 | +t.Parallel() |
| 29 | + |
| 30 | +testResourceDirectories:=filepath.Join("testdata","resources") |
| 31 | +entries,err:=os.ReadDir(testResourceDirectories) |
| 32 | +require.NoError(t,err) |
| 33 | + |
| 34 | +for_,testDirectory:=rangeentries { |
| 35 | +if!testDirectory.IsDir() { |
| 36 | +continue |
| 37 | +} |
| 38 | + |
| 39 | +testFiles,err:=os.ReadDir(filepath.Join(testResourceDirectories,testDirectory.Name())) |
| 40 | +require.NoError(t,err) |
| 41 | + |
| 42 | +// ConvertState works on both a plan file and a state file. |
| 43 | +// The test should create a golden file for both. |
| 44 | +for_,step:=range []string{"plan","state"} { |
| 45 | +srcIdc:=slices.IndexFunc(testFiles,func(entry os.DirEntry)bool { |
| 46 | +returnstrings.HasSuffix(entry.Name(),fmt.Sprintf(".tf%s.json",step)) |
| 47 | +}) |
| 48 | +dotIdx:=slices.IndexFunc(testFiles,func(entry os.DirEntry)bool { |
| 49 | +returnstrings.HasSuffix(entry.Name(),fmt.Sprintf(".tf%s.dot",step)) |
| 50 | +}) |
| 51 | + |
| 52 | +// If the directory is missing these files, we cannot run ConvertState |
| 53 | +// on it. So it's skipped. |
| 54 | +ifsrcIdc==-1||dotIdx==-1 { |
| 55 | +continue |
| 56 | +} |
| 57 | + |
| 58 | +t.Run(step+"_"+testDirectory.Name(),func(t*testing.T) { |
| 59 | +t.Parallel() |
| 60 | +testDirectoryPath:=filepath.Join(testResourceDirectories,testDirectory.Name()) |
| 61 | +planFile:=filepath.Join(testDirectoryPath,testFiles[srcIdc].Name()) |
| 62 | +dotFile:=filepath.Join(testDirectoryPath,testFiles[dotIdx].Name()) |
| 63 | + |
| 64 | +ctx:=testutil.Context(t,testutil.WaitMedium) |
| 65 | +logger:=slogtest.Make(t,nil) |
| 66 | + |
| 67 | +// Gather plan |
| 68 | +tfStepRaw,err:=os.ReadFile(planFile) |
| 69 | +require.NoError(t,err) |
| 70 | + |
| 71 | +varmodules []*tfjson.StateModule |
| 72 | +switchstep { |
| 73 | +case"plan": |
| 74 | +vartfPlan tfjson.Plan |
| 75 | +err=json.Unmarshal(tfStepRaw,&tfPlan) |
| 76 | +require.NoError(t,err) |
| 77 | + |
| 78 | +modules= []*tfjson.StateModule{tfPlan.PlannedValues.RootModule} |
| 79 | +iftfPlan.PriorState!=nil { |
| 80 | +modules=append(modules,tfPlan.PriorState.Values.RootModule) |
| 81 | +} |
| 82 | +case"state": |
| 83 | +vartfState tfjson.State |
| 84 | +err=json.Unmarshal(tfStepRaw,&tfState) |
| 85 | +require.NoError(t,err) |
| 86 | +modules= []*tfjson.StateModule{tfState.Values.RootModule} |
| 87 | +default: |
| 88 | +t.Fatalf("unknown step: %s",step) |
| 89 | +} |
| 90 | + |
| 91 | +// Gather graph |
| 92 | +dotFileRaw,err:=os.ReadFile(dotFile) |
| 93 | +require.NoError(t,err) |
| 94 | + |
| 95 | +// expectedOutput is `any` to support errors too. If `ConvertState` returns an |
| 96 | +// error, that error is the golden file output. |
| 97 | +varexpectedOutputany |
| 98 | +state,err:=terraform.ConvertState(ctx,modules,string(dotFileRaw),logger) |
| 99 | +iferr==nil { |
| 100 | +sortResources(state.Resources) |
| 101 | +sortExternalAuthProviders(state.ExternalAuthProviders) |
| 102 | +deterministicAppIDs(state.Resources) |
| 103 | +expectedOutput=state |
| 104 | +}else { |
| 105 | +// Write the error to the file then. Track errors as much as valid paths. |
| 106 | +expectedOutput=err.Error() |
| 107 | +} |
| 108 | + |
| 109 | +expPath:=filepath.Join(testDirectoryPath,fmt.Sprintf("converted_state.%s.golden",step)) |
| 110 | +if*updateGoldenFiles { |
| 111 | +gotBytes,err:=json.MarshalIndent(expectedOutput,""," ") |
| 112 | +require.NoError(t,err,"marshaling converted state to JSON") |
| 113 | +// Newline at end of file for git purposes |
| 114 | +err=os.WriteFile(expPath,append(gotBytes,'\n'),0o600) |
| 115 | +require.NoError(t,err) |
| 116 | +return |
| 117 | +} |
| 118 | + |
| 119 | +gotBytes,err:=json.Marshal(expectedOutput) |
| 120 | +require.NoError(t,err,"marshaling converted state to JSON") |
| 121 | + |
| 122 | +expBytes,err:=os.ReadFile(expPath) |
| 123 | +require.NoError(t,err) |
| 124 | + |
| 125 | +require.JSONEq(t,string(expBytes),string(gotBytes),"converted state") |
| 126 | +}) |
| 127 | +} |
| 128 | +} |
| 129 | +} |