github.com/replicatedhq/ship@v0.55.0/pkg/lifecycle/terraform/terraformer_test.go (about) 1 package terraform 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "log" 8 "os" 9 "os/exec" 10 "reflect" 11 "testing" 12 "time" 13 14 log2 "github.com/go-kit/kit/log" 15 "github.com/golang/mock/gomock" 16 "github.com/replicatedhq/ship/pkg/api" 17 "github.com/replicatedhq/ship/pkg/lifecycle/daemon/daemontypes" 18 "github.com/replicatedhq/ship/pkg/state" 19 "github.com/replicatedhq/ship/pkg/test-mocks/daemon" 20 mocktf "github.com/replicatedhq/ship/pkg/test-mocks/tfplan" 21 "github.com/replicatedhq/ship/pkg/testing/logger" 22 "github.com/spf13/afero" 23 "github.com/stretchr/testify/require" 24 ) 25 26 type subproc struct { 27 Fail bool 28 Stdout string 29 Stderr string 30 ExpectArgv []string 31 } 32 33 func TestTerraformer(t *testing.T) { 34 tests := []struct { 35 name string 36 init subproc 37 plan subproc 38 apply subproc 39 expectConfirmPlan bool 40 expectPlan string 41 expectApply bool 42 expectApplyOutput string 43 expectError bool 44 }{ 45 { 46 name: "init plan apply success", 47 init: subproc{ 48 ExpectArgv: []string{"init", "-input=false"}, 49 }, 50 plan: subproc{ 51 ExpectArgv: []string{"plan", "-input=false", "-out=plan.tfplan"}, 52 Stdout: fmt.Sprintf("state%sCreating 1 cluster%show to apply", tfSep, tfSep), 53 }, 54 apply: subproc{ 55 ExpectArgv: []string{"apply", "-input=false", "-auto-approve=true", "plan.tfplan"}, 56 Stdout: "Applied", 57 }, 58 expectConfirmPlan: true, 59 expectPlan: `<div class="term-container">Creating 1 cluster</div>`, 60 expectApply: true, 61 expectApplyOutput: `<div class="term-container">Applied</div>`, 62 }, 63 { 64 name: "init fail", 65 init: subproc{ 66 Fail: true, 67 }, 68 expectError: true, 69 }, 70 { 71 name: "plan no changes", 72 plan: subproc{ 73 Stdout: fmt.Sprintf("\n\n%s\n\n", tfNoChanges), 74 }, 75 }, 76 { 77 name: "apply fail", 78 plan: subproc{ 79 Stdout: fmt.Sprintf("state%sCreating 1 cluster%show to apply", tfSep, tfSep), 80 }, 81 expectConfirmPlan: true, 82 expectPlan: `<div class="term-container">Creating 1 cluster</div>`, 83 apply: subproc{ 84 Fail: true, 85 }, 86 }, 87 } 88 for _, test := range tests { 89 t.Run(test.name, func(t *testing.T) { 90 req := require.New(t) 91 mc := gomock.NewController(t) 92 mockDaemon := daemon.NewMockDaemon(mc) 93 mockPlanner := mocktf.NewMockPlanConfirmer(mc) 94 mockFS := afero.Afero{Fs: afero.NewMemMapFs()} 95 tf := &ForkTerraformer{ 96 Logger: &logger.TestLogger{T: t}, 97 Daemon: mockDaemon, 98 PlanConfirmer: mockPlanner, 99 Terraform: func(string) *exec.Cmd { 100 cmd := exec.Command(os.Args[0], "-test.run=TestMockTerraform") 101 102 init, err := json.Marshal(test.init) 103 if err != nil { 104 log.Fatal(err) 105 } 106 107 plan, err := json.Marshal(test.plan) 108 if err != nil { 109 log.Fatal(err) 110 } 111 112 apply, err := json.Marshal(test.apply) 113 if err != nil { 114 log.Fatal(err) 115 } 116 117 cmd.Env = append(os.Environ(), 118 "GOTEST_SUBPROCESS_MOCK=1", 119 "TERRAFORM_INIT="+string(init), 120 "TERRAFORM_PLAN="+string(plan), 121 "TERRAFORM_APPLY="+string(apply), 122 ) 123 return cmd 124 }, 125 FS: mockFS, 126 // skip this for now, will be tested separately 127 StateSaver: func(debug log2.Logger, fs afero.Afero, statemanager state.Manager, dir string) error { 128 return nil 129 }, 130 StateRestorer: func(debug log2.Logger, fs afero.Afero, statemanager state.Manager, dir string) error { 131 return nil 132 }, 133 } 134 135 if test.expectConfirmPlan { 136 mockPlanner. 137 EXPECT(). 138 ConfirmPlan(gomock.Any(), test.expectPlan, gomock.Any(), gomock.Any()). 139 Return(test.expectApply, nil) 140 } 141 142 if test.expectApply { 143 mockDaemon. 144 EXPECT(). 145 PushStreamStep(gomock.Any(), gomock.Any()) 146 147 msg := daemontypes.Message{ 148 Contents: test.expectApplyOutput, 149 TrustedHTML: true, 150 } 151 if test.apply.Fail { 152 msg.Level = "error" 153 } 154 155 mockDaemon. 156 EXPECT(). 157 PushMessageStep(gomock.Any(), msg, gomock.Any()). 158 Return() 159 160 if test.apply.Fail { 161 ch := make(chan bool, 1) 162 ch <- false 163 mockDaemon. 164 EXPECT(). 165 TerraformConfirmedChan(). 166 Return(ch) 167 } else { 168 ch := make(chan string, 1) 169 ch <- "" 170 mockDaemon. 171 EXPECT(). 172 MessageConfirmedChan(). 173 Return(ch) 174 } 175 } 176 177 err := tf.Execute( 178 context.Background(), 179 api.Release{}, 180 api.Terraform{}, 181 make(chan bool), 182 ) 183 184 if test.expectError { 185 req.Error(err) 186 } else { 187 req.NoError(err) 188 } 189 190 mc.Finish() 191 }) 192 } 193 } 194 195 func TestMockTerraform(t *testing.T) { 196 if os.Getenv("GOTEST_SUBPROCESS_MOCK") == "" { 197 return 198 } 199 200 var env string 201 switch os.Args[2] { 202 case "init": 203 env = "TERRAFORM_INIT" 204 case "plan": 205 env = "TERRAFORM_PLAN" 206 case "apply": 207 env = "TERRAFORM_APPLY" 208 } 209 210 var config subproc 211 err := json.Unmarshal([]byte(os.Getenv(env)), &config) 212 if err != nil { 213 t.Fatal(err) 214 } 215 216 if len(config.ExpectArgv) > 0 { 217 receivedArgs := os.Args[2:] 218 if !reflect.DeepEqual(receivedArgs, config.ExpectArgv) { 219 fmt.Fprintf(os.Stderr, "; FAIL expected args %+v got %+v", config.ExpectArgv, receivedArgs) 220 os.Exit(2) 221 } 222 } 223 224 fmt.Fprintf(os.Stdout, config.Stdout) 225 fmt.Fprintf(os.Stderr, config.Stderr) 226 227 if config.Fail { 228 os.Exit(1) 229 } 230 231 os.Exit(0) 232 } 233 234 func TestForkTerraformerApply(t *testing.T) { 235 req := require.New(t) 236 mc := gomock.NewController(t) 237 mockDaemon := daemon.NewMockDaemon(mc) 238 mockFS := afero.Afero{Fs: afero.NewMemMapFs()} 239 ft := ForkTerraformer{ 240 Daemon: mockDaemon, 241 Logger: &logger.TestLogger{T: t}, 242 Terraform: func(string) *exec.Cmd { 243 cmd := exec.Command(os.Args[0], "-test.run=TestMockTerraformApply") 244 cmd.Env = append(os.Environ(), 245 "GOTEST_SUBPROCESS_MOCK=1", 246 ) 247 return cmd 248 }, 249 FS: mockFS, 250 } 251 252 msgs := make(chan daemontypes.Message, 10) 253 output, err := ft.apply("fakedir", msgs) 254 req.NoError(err) 255 req.Equal(output, `<div class="term-container">stdout1stderr1stdout2</div>`) 256 257 req.EqualValues(daemontypes.Message{ 258 Contents: `<div class="term-container">terraform apply</div>`, 259 TrustedHTML: true, 260 }, <-msgs) 261 262 req.EqualValues(daemontypes.Message{ 263 Contents: `<div class="term-container">stdout1</div>`, 264 TrustedHTML: true, 265 }, <-msgs) 266 267 req.EqualValues(daemontypes.Message{ 268 Contents: `<div class="term-container">stdout1stderr1</div>`, 269 TrustedHTML: true, 270 }, <-msgs) 271 272 req.EqualValues(daemontypes.Message{ 273 Contents: `<div class="term-container">stdout1stderr1stdout2</div>`, 274 TrustedHTML: true, 275 }, <-msgs) 276 } 277 278 func TestMockTerraformApply(t *testing.T) { 279 if os.Getenv("GOTEST_SUBPROCESS_MOCK") == "" { 280 return 281 } 282 283 fmt.Fprintf(os.Stdout, "stdout1") 284 time.Sleep(time.Millisecond) 285 fmt.Fprintf(os.Stderr, "stderr1") 286 time.Sleep(time.Millisecond) 287 fmt.Fprintf(os.Stdout, "stdout2") 288 289 os.Exit(0) 290 }