1package bosh_test
2
3import (
4	"errors"
5	"fmt"
6	"io"
7	"os"
8	"path/filepath"
9
10	"github.com/cloudfoundry/bosh-bootloader/bosh"
11	"github.com/cloudfoundry/bosh-bootloader/fakes"
12	"github.com/cloudfoundry/bosh-bootloader/fileio"
13	"github.com/cloudfoundry/bosh-bootloader/storage"
14	"github.com/spf13/afero"
15
16	. "github.com/onsi/ginkgo"
17	. "github.com/onsi/gomega"
18)
19
20var _ = Describe("Executor", func() {
21	var (
22		fs                    *afero.Afero
23		cli                   *fakes.BOSHCLI
24		stateDir              string
25		deploymentDir         string
26		varsDir               string
27		relativeDeploymentDir string
28		relativeVarsDir       string
29		relativeStateDir      string
30		dirInput              bosh.DirInput
31
32		executor bosh.Executor
33	)
34
35	BeforeEach(func() {
36		fs = &afero.Afero{afero.NewMemMapFs()}
37		cli = &fakes.BOSHCLI{}
38		cli.RunStub = func(stdout io.Writer, workingDirectory string, args []string) error {
39			stdout.Write([]byte("some-manifest"))
40			return nil
41		}
42		cli.GetBOSHPathCall.Returns.Path = "bosh-path"
43
44		var err error
45		stateDir, err = fs.TempDir("", "")
46		Expect(err).NotTo(HaveOccurred())
47
48		deploymentDir = filepath.Join(stateDir, "deployment")
49		err = fs.Mkdir(deploymentDir, os.ModePerm)
50		Expect(err).NotTo(HaveOccurred())
51
52		varsDir = filepath.Join(stateDir, "vars")
53		err = fs.Mkdir(varsDir, os.ModePerm)
54		Expect(err).NotTo(HaveOccurred())
55
56		relativeDeploymentDir = "${BBL_STATE_DIR}/deployment"
57		relativeVarsDir = "${BBL_STATE_DIR}/vars"
58		relativeStateDir = "${BBL_STATE_DIR}"
59
60		dirInput = bosh.DirInput{
61			VarsDir:  varsDir,
62			StateDir: stateDir,
63		}
64
65		executor = bosh.NewExecutor(cli, fs)
66	})
67
68	Describe("PlanJumpbox", func() {
69		It("writes bosh-deployment assets to the deployment dir", func() {
70			err := executor.PlanJumpbox(dirInput, deploymentDir, "aws")
71			Expect(err).NotTo(HaveOccurred())
72
73			By("writing bosh-deployment assets to the deployment dir", func() {
74				simplePath := filepath.Join(deploymentDir, "no-external-ip.yml")
75				expectedContents := bosh.MustAsset("vendor/github.com/cloudfoundry/jumpbox-deployment/no-external-ip.yml")
76
77				contents, err := fs.ReadFile(simplePath)
78				Expect(err).NotTo(HaveOccurred())
79				Expect(contents).To(Equal(expectedContents))
80
81				nestedPath := filepath.Join(deploymentDir, "vsphere", "cpi.yml")
82				expectedContents = bosh.MustAsset("vendor/github.com/cloudfoundry/jumpbox-deployment/vsphere/cpi.yml")
83
84				contents, err = fs.ReadFile(nestedPath)
85				Expect(err).NotTo(HaveOccurred())
86				Expect(contents).To(Equal(expectedContents))
87			})
88
89			By("writing create-env and delete-env scripts", func() {
90				expectedArgs := []string{
91					fmt.Sprintf("%s/jumpbox.yml", relativeDeploymentDir),
92					"--state", fmt.Sprintf("%s/jumpbox-state.json", relativeVarsDir),
93					"--vars-store", fmt.Sprintf("%s/jumpbox-vars-store.yml", relativeVarsDir),
94					"--vars-file", fmt.Sprintf("%s/jumpbox-vars-file.yml", relativeVarsDir),
95					"-o", fmt.Sprintf("%s/aws/cpi.yml", relativeDeploymentDir),
96					"-v", `access_key_id="${BBL_AWS_ACCESS_KEY_ID}"`,
97					"-v", `secret_access_key="${BBL_AWS_SECRET_ACCESS_KEY}"`,
98				}
99
100				expectedScript := formatScript("create-env", stateDir, expectedArgs)
101				scriptPath := fmt.Sprintf("%s/create-jumpbox.sh", stateDir)
102				shellScript, err := fs.ReadFile(scriptPath)
103				Expect(err).NotTo(HaveOccurred())
104
105				fileinfo, err := fs.Stat(scriptPath)
106				Expect(err).NotTo(HaveOccurred())
107				Expect(fileinfo.Mode().String()).To(Equal("-rwxr-x---"))
108				Expect(string(shellScript)).To(Equal(expectedScript))
109
110				expectedScript = formatScript("delete-env", stateDir, expectedArgs)
111				scriptPath = fmt.Sprintf("%s/delete-jumpbox.sh", stateDir)
112				shellScript, err = fs.ReadFile(scriptPath)
113				Expect(err).NotTo(HaveOccurred())
114
115				fileinfo, err = fs.Stat(scriptPath)
116				Expect(err).NotTo(HaveOccurred())
117				Expect(fileinfo.Mode().String()).To(Equal("-rwxr-x---"))
118				Expect(err).NotTo(HaveOccurred())
119				Expect(string(shellScript)).To(Equal(expectedScript))
120			})
121		})
122
123		Context("on azure", func() {
124			It("generates create-env args for jumpbox", func() {
125				err := executor.PlanJumpbox(dirInput, deploymentDir, "azure")
126				Expect(err).NotTo(HaveOccurred())
127
128				expectedArgs := []string{
129					fmt.Sprintf("%s/jumpbox.yml", relativeDeploymentDir),
130					"--state", fmt.Sprintf("%s/jumpbox-state.json", relativeVarsDir),
131					"--vars-store", fmt.Sprintf("%s/jumpbox-vars-store.yml", relativeVarsDir),
132					"--vars-file", fmt.Sprintf("%s/jumpbox-vars-file.yml", relativeVarsDir),
133					"-o", fmt.Sprintf("%s/azure/cpi.yml", relativeDeploymentDir),
134					"-v", `subscription_id="${BBL_AZURE_SUBSCRIPTION_ID}"`,
135					"-v", `client_id="${BBL_AZURE_CLIENT_ID}"`,
136					"-v", `client_secret="${BBL_AZURE_CLIENT_SECRET}"`,
137					"-v", `tenant_id="${BBL_AZURE_TENANT_ID}"`,
138				}
139
140				By("writing the create-env args to a shell script", func() {
141					expectedScript := formatScript("create-env", stateDir, expectedArgs)
142					shellScript, err := fs.ReadFile(fmt.Sprintf("%s/create-jumpbox.sh", stateDir))
143					Expect(err).NotTo(HaveOccurred())
144
145					Expect(string(shellScript)).To(Equal(expectedScript))
146				})
147
148				By("writing the delete-env args to a shell script", func() {
149					expectedScript := formatScript("delete-env", stateDir, expectedArgs)
150					shellScript, err := fs.ReadFile(fmt.Sprintf("%s/delete-jumpbox.sh", stateDir))
151					Expect(err).NotTo(HaveOccurred())
152
153					Expect(string(shellScript)).To(Equal(expectedScript))
154				})
155			})
156		})
157
158		Context("on gcp", func() {
159			It("generates create-env args for jumpbox", func() {
160				err := executor.PlanJumpbox(dirInput, deploymentDir, "gcp")
161				Expect(err).NotTo(HaveOccurred())
162
163				expectedArgs := []string{
164					fmt.Sprintf("%s/jumpbox.yml", relativeDeploymentDir),
165					"--state", fmt.Sprintf("%s/jumpbox-state.json", relativeVarsDir),
166					"--vars-store", fmt.Sprintf("%s/jumpbox-vars-store.yml", relativeVarsDir),
167					"--vars-file", fmt.Sprintf("%s/jumpbox-vars-file.yml", relativeVarsDir),
168					"-o", fmt.Sprintf("%s/gcp/cpi.yml", relativeDeploymentDir),
169					"--var-file", `gcp_credentials_json="${BBL_GCP_SERVICE_ACCOUNT_KEY_PATH}"`,
170					"-v", `project_id="${BBL_GCP_PROJECT_ID}"`,
171					"-v", `zone="${BBL_GCP_ZONE}"`,
172				}
173
174				By("writing the create-env args to a shell script", func() {
175					expectedScript := formatScript("create-env", stateDir, expectedArgs)
176					shellScript, err := fs.ReadFile(fmt.Sprintf("%s/create-jumpbox.sh", stateDir))
177					Expect(err).NotTo(HaveOccurred())
178
179					Expect(string(shellScript)).To(Equal(expectedScript))
180				})
181
182				By("writing the delete-env args to a shell script", func() {
183					expectedScript := formatScript("delete-env", stateDir, expectedArgs)
184					shellScript, err := fs.ReadFile(fmt.Sprintf("%s/delete-jumpbox.sh", stateDir))
185					Expect(err).NotTo(HaveOccurred())
186
187					Expect(string(shellScript)).To(Equal(expectedScript))
188				})
189			})
190		})
191
192		Context("when the iaas is vsphere", func() {
193			It("generates create-env args for jumpbox", func() {
194				err := executor.PlanJumpbox(dirInput, deploymentDir, "vsphere")
195				Expect(err).NotTo(HaveOccurred())
196
197				expectedArgs := []string{
198					fmt.Sprintf("%s/jumpbox.yml", relativeDeploymentDir),
199					"--state", fmt.Sprintf("%s/jumpbox-state.json", relativeVarsDir),
200					"--vars-store", fmt.Sprintf("%s/jumpbox-vars-store.yml", relativeVarsDir),
201					"--vars-file", fmt.Sprintf("%s/jumpbox-vars-file.yml", relativeVarsDir),
202					"-o", fmt.Sprintf("%s/vsphere/cpi.yml", relativeDeploymentDir),
203					"-o", fmt.Sprintf("%s/vsphere/resource-pool.yml", relativeDeploymentDir),
204					"-o", fmt.Sprintf("%s/vsphere-jumpbox-network.yml", relativeDeploymentDir),
205					"-v", `vcenter_user="${BBL_VSPHERE_VCENTER_USER}"`,
206					"-v", `vcenter_password="${BBL_VSPHERE_VCENTER_PASSWORD}"`,
207				}
208
209				By("writing the jumpbox-network ops-file", func() {
210					opsfile, err := fs.ReadFile(fmt.Sprintf("%s/vsphere-jumpbox-network.yml", deploymentDir))
211					Expect(err).NotTo(HaveOccurred())
212
213					Expect(string(opsfile)).To(ContainSubstring("instance_groups/name=jumpbox/networks/name=public"))
214				})
215
216				By("writing the create-env args to a shell script", func() {
217					expectedScript := formatScript("create-env", stateDir, expectedArgs)
218					shellScript, err := fs.ReadFile(fmt.Sprintf("%s/create-jumpbox.sh", stateDir))
219					Expect(err).NotTo(HaveOccurred())
220
221					Expect(string(shellScript)).To(Equal(expectedScript))
222				})
223
224				By("writing the delete-env args to a shell script", func() {
225					expectedScript := formatScript("delete-env", stateDir, expectedArgs)
226					shellScript, err := fs.ReadFile(fmt.Sprintf("%s/delete-jumpbox.sh", stateDir))
227					Expect(err).NotTo(HaveOccurred())
228
229					Expect(string(shellScript)).To(Equal(expectedScript))
230				})
231			})
232		})
233
234		Context("openstack", func() {
235			It("generates create-env args for jumpbox", func() {
236				err := executor.PlanJumpbox(dirInput, deploymentDir, "openstack")
237				Expect(err).NotTo(HaveOccurred())
238
239				expectedArgs := []string{
240					fmt.Sprintf("%s/jumpbox.yml", relativeDeploymentDir),
241					"--state", fmt.Sprintf("%s/jumpbox-state.json", relativeVarsDir),
242					"--vars-store", fmt.Sprintf("%s/jumpbox-vars-store.yml", relativeVarsDir),
243					"--vars-file", fmt.Sprintf("%s/jumpbox-vars-file.yml", relativeVarsDir),
244					"-o", fmt.Sprintf("%s/openstack/cpi.yml", relativeDeploymentDir),
245					"-v", `openstack_username="${BBL_OPENSTACK_USERNAME}"`,
246					"-v", `openstack_password="${BBL_OPENSTACK_PASSWORD}"`,
247				}
248
249				By("writing the create-env args to a shell script", func() {
250					expectedScript := formatScript("create-env", stateDir, expectedArgs)
251					shellScript, err := fs.ReadFile(fmt.Sprintf("%s/create-jumpbox.sh", stateDir))
252					Expect(err).NotTo(HaveOccurred())
253
254					Expect(string(shellScript)).To(Equal(expectedScript))
255				})
256
257				By("writing the delete-env args to a shell script", func() {
258					expectedScript := formatScript("delete-env", stateDir, expectedArgs)
259					shellScript, err := fs.ReadFile(fmt.Sprintf("%s/delete-jumpbox.sh", stateDir))
260					Expect(err).NotTo(HaveOccurred())
261
262					Expect(string(shellScript)).To(Equal(expectedScript))
263				})
264			})
265		})
266	})
267
268	Describe("PlanDirector", func() {
269		It("writes bosh-deployment assets to the deployment dir", func() {
270			err := executor.PlanDirector(dirInput, deploymentDir, "warden")
271			Expect(err).NotTo(HaveOccurred())
272
273			simplePath := filepath.Join(deploymentDir, "LICENSE")
274			expectedContents := bosh.MustAsset("vendor/github.com/cloudfoundry/bosh-deployment/LICENSE")
275
276			contents, err := fs.ReadFile(simplePath)
277			Expect(err).NotTo(HaveOccurred())
278			Expect(contents).To(Equal(expectedContents))
279
280			nestedPath := filepath.Join(deploymentDir, "vsphere", "cpi.yml")
281			expectedContents = bosh.MustAsset("vendor/github.com/cloudfoundry/bosh-deployment/vsphere/cpi.yml")
282
283			contents, err = fs.ReadFile(nestedPath)
284			Expect(err).NotTo(HaveOccurred())
285			Expect(contents).To(Equal(expectedContents))
286		})
287
288		Context("aws", func() {
289			It("writes create-director.sh and delete-director.sh", func() {
290				expectedArgs := []string{
291					filepath.Join(relativeDeploymentDir, "bosh.yml"),
292					"--state", filepath.Join(relativeVarsDir, "bosh-state.json"),
293					"--vars-store", filepath.Join(relativeVarsDir, "director-vars-store.yml"),
294					"--vars-file", filepath.Join(relativeVarsDir, "director-vars-file.yml"),
295					"-o", filepath.Join(relativeDeploymentDir, "aws", "cpi.yml"),
296					"-o", filepath.Join(relativeDeploymentDir, "jumpbox-user.yml"),
297					"-o", filepath.Join(relativeDeploymentDir, "uaa.yml"),
298					"-o", filepath.Join(relativeDeploymentDir, "credhub.yml"),
299					"-o", filepath.Join(relativeStateDir, "bbl-ops-files", "aws", "bosh-director-ephemeral-ip-ops.yml"),
300					"-o", filepath.Join(relativeDeploymentDir, "aws", "iam-instance-profile.yml"),
301					"-o", filepath.Join(relativeDeploymentDir, "aws", "encrypted-disk.yml"),
302					"-v", `access_key_id="${BBL_AWS_ACCESS_KEY_ID}"`,
303					"-v", `secret_access_key="${BBL_AWS_SECRET_ACCESS_KEY}"`,
304				}
305
306				behavesLikePlan(expectedArgs, cli, fs, executor, dirInput, deploymentDir, "aws", stateDir)
307			})
308
309			It("writes aws-specific ops files", func() {
310				err := executor.PlanDirector(dirInput, deploymentDir, "aws")
311				Expect(err).NotTo(HaveOccurred())
312
313				ipOpsFile := filepath.Join(stateDir, "bbl-ops-files", "aws", "bosh-director-ephemeral-ip-ops.yml")
314
315				ipOpsFileContents, err := fs.ReadFile(ipOpsFile)
316				Expect(err).NotTo(HaveOccurred())
317				Expect(string(ipOpsFileContents)).To(Equal(`
318- type: replace
319  path: /resource_pools/name=vms/cloud_properties/auto_assign_public_ip?
320  value: true
321`))
322			})
323		})
324
325		Context("gcp", func() {
326			It("writes create-director.sh and delete-director.sh", func() {
327				expectedArgs := []string{
328					filepath.Join(relativeDeploymentDir, "bosh.yml"),
329					"--state", filepath.Join(relativeVarsDir, "bosh-state.json"),
330					"--vars-store", filepath.Join(relativeVarsDir, "director-vars-store.yml"),
331					"--vars-file", filepath.Join(relativeVarsDir, "director-vars-file.yml"),
332					"-o", filepath.Join(relativeDeploymentDir, "gcp", "cpi.yml"),
333					"-o", filepath.Join(relativeDeploymentDir, "jumpbox-user.yml"),
334					"-o", filepath.Join(relativeDeploymentDir, "uaa.yml"),
335					"-o", filepath.Join(relativeDeploymentDir, "credhub.yml"),
336					"-o", filepath.Join(relativeStateDir, "bbl-ops-files", "gcp", "bosh-director-ephemeral-ip-ops.yml"),
337					"--var-file", `gcp_credentials_json="${BBL_GCP_SERVICE_ACCOUNT_KEY_PATH}"`,
338					"-v", `project_id="${BBL_GCP_PROJECT_ID}"`,
339					"-v", `zone="${BBL_GCP_ZONE}"`,
340				}
341
342				behavesLikePlan(expectedArgs, cli, fs, executor, dirInput, deploymentDir, "gcp", stateDir)
343			})
344
345			It("writes gcp-specific ops files", func() {
346				err := executor.PlanDirector(dirInput, deploymentDir, "gcp")
347				Expect(err).NotTo(HaveOccurred())
348
349				ipOpsFile := filepath.Join(stateDir, "bbl-ops-files", "gcp", "bosh-director-ephemeral-ip-ops.yml")
350
351				ipOpsFileContents, err := fs.ReadFile(ipOpsFile)
352				Expect(err).NotTo(HaveOccurred())
353				Expect(string(ipOpsFileContents)).To(Equal(`
354- type: replace
355  path: /networks/name=default/subnets/0/cloud_properties/ephemeral_external_ip?
356  value: true
357`))
358			})
359		})
360
361		Context("azure", func() {
362			It("writes create-director.sh and delete-director.sh", func() {
363				expectedArgs := []string{
364					filepath.Join(relativeDeploymentDir, "bosh.yml"),
365					"--state", filepath.Join(relativeVarsDir, "bosh-state.json"),
366					"--vars-store", filepath.Join(relativeVarsDir, "director-vars-store.yml"),
367					"--vars-file", filepath.Join(relativeVarsDir, "director-vars-file.yml"),
368					"-o", filepath.Join(relativeDeploymentDir, "azure", "cpi.yml"),
369					"-o", filepath.Join(relativeDeploymentDir, "jumpbox-user.yml"),
370					"-o", filepath.Join(relativeDeploymentDir, "uaa.yml"),
371					"-o", filepath.Join(relativeDeploymentDir, "credhub.yml"),
372					"-v", `subscription_id="${BBL_AZURE_SUBSCRIPTION_ID}"`,
373					"-v", `client_id="${BBL_AZURE_CLIENT_ID}"`,
374					"-v", `client_secret="${BBL_AZURE_CLIENT_SECRET}"`,
375					"-v", `tenant_id="${BBL_AZURE_TENANT_ID}"`,
376				}
377
378				behavesLikePlan(expectedArgs, cli, fs, executor, dirInput, deploymentDir, "azure", stateDir)
379			})
380		})
381
382		Context("vsphere", func() {
383			It("writes create-director.sh and delete-director.sh", func() {
384				expectedArgs := []string{
385					filepath.Join(relativeDeploymentDir, "bosh.yml"),
386					"--state", filepath.Join(relativeVarsDir, "bosh-state.json"),
387					"--vars-store", filepath.Join(relativeVarsDir, "director-vars-store.yml"),
388					"--vars-file", filepath.Join(relativeVarsDir, "director-vars-file.yml"),
389					"-o", filepath.Join(relativeDeploymentDir, "vsphere", "cpi.yml"),
390					"-o", filepath.Join(relativeDeploymentDir, "jumpbox-user.yml"),
391					"-o", filepath.Join(relativeDeploymentDir, "uaa.yml"),
392					"-o", filepath.Join(relativeDeploymentDir, "credhub.yml"),
393					"-o", filepath.Join(relativeDeploymentDir, "vsphere", "resource-pool.yml"),
394					"-v", `vcenter_user="${BBL_VSPHERE_VCENTER_USER}"`,
395					"-v", `vcenter_password="${BBL_VSPHERE_VCENTER_PASSWORD}"`,
396				}
397
398				behavesLikePlan(expectedArgs, cli, fs, executor, dirInput, deploymentDir, "vsphere", stateDir)
399			})
400		})
401
402		Context("openstack", func() {
403			It("writes create-director.sh and delete-director.sh", func() {
404				expectedArgs := []string{
405					filepath.Join(relativeDeploymentDir, "bosh.yml"),
406					"--state", filepath.Join(relativeVarsDir, "bosh-state.json"),
407					"--vars-store", filepath.Join(relativeVarsDir, "director-vars-store.yml"),
408					"--vars-file", filepath.Join(relativeVarsDir, "director-vars-file.yml"),
409					"-o", filepath.Join(relativeDeploymentDir, "openstack", "cpi.yml"),
410					"-o", filepath.Join(relativeDeploymentDir, "jumpbox-user.yml"),
411					"-o", filepath.Join(relativeDeploymentDir, "uaa.yml"),
412					"-o", filepath.Join(relativeDeploymentDir, "credhub.yml"),
413					"-v", `openstack_username="${BBL_OPENSTACK_USERNAME}"`,
414					"-v", `openstack_password="${BBL_OPENSTACK_PASSWORD}"`,
415				}
416
417				behavesLikePlan(expectedArgs, cli, fs, executor, dirInput, deploymentDir, "openstack", stateDir)
418			})
419		})
420	})
421
422	Describe("WriteDeploymentVars", func() {
423		BeforeEach(func() {
424			dirInput.Deployment = "some-deployment"
425		})
426
427		It("writes the deployment vars yml file", func() {
428			err := executor.WriteDeploymentVars(dirInput, "some-deployment-vars")
429			Expect(err).NotTo(HaveOccurred())
430
431			deploymentVars, err := fs.ReadFile(filepath.Join(varsDir, "some-deployment-vars-file.yml"))
432			Expect(err).NotTo(HaveOccurred())
433
434			Expect(string(deploymentVars)).To(Equal("some-deployment-vars"))
435		})
436	})
437
438	Describe("CreateEnv", func() {
439		var (
440			cli      *fakes.BOSHCLI
441			executor bosh.Executor
442
443			createEnvPath string
444			varsDir       string
445			stateDir      string
446
447			dirInput bosh.DirInput
448			state    storage.State
449		)
450
451		BeforeEach(func() {
452			fs = &afero.Afero{afero.NewOsFs()} // real os fs so we can exec scripts...
453			cli = &fakes.BOSHCLI{}
454
455			var err error
456			varsDir, err = fs.TempDir("", "")
457			Expect(err).NotTo(HaveOccurred())
458			stateDir, err = fs.TempDir("", "")
459			Expect(err).NotTo(HaveOccurred())
460
461			executor = bosh.NewExecutor(cli, fs)
462
463			dirInput = bosh.DirInput{
464				Deployment: "some-deployment",
465				StateDir:   stateDir,
466				VarsDir:    varsDir,
467			}
468
469			createEnvPath = filepath.Join(stateDir, "create-some-deployment.sh")
470			createEnvContents := fmt.Sprintf("#!/bin/bash\necho 'some-vars-store-contents' > %s/some-deployment-vars-store.yml\n", varsDir)
471
472			fs.WriteFile(createEnvPath, []byte(createEnvContents), storage.ScriptMode)
473		})
474
475		AfterEach(func() {
476			fs.Remove(filepath.Join(varsDir, "some-deployment-vars-store.yml"))
477			fs.Remove(createEnvPath)
478			fs.Remove(filepath.Join(stateDir, "create-some-deployment-override.sh"))
479			os.Unsetenv("BBL_STATE_DIR")
480		})
481
482		Context("when the user provides a create-env override", func() {
483			BeforeEach(func() {
484				overridePath := filepath.Join(stateDir, "create-some-deployment-override.sh")
485				overrideContents := fmt.Sprintf("#!/bin/bash\necho 'override-vars-store-contents' > %s/some-deployment-vars-store.yml\n", varsDir)
486
487				fs.WriteFile(overridePath, []byte(overrideContents), storage.ScriptMode)
488			})
489
490			It("runs the create-env-override.sh script", func() {
491				vars, err := executor.CreateEnv(dirInput, state)
492				Expect(err).NotTo(HaveOccurred())
493
494				Expect(cli.RunCallCount()).To(Equal(0))
495				Expect(vars).To(ContainSubstring("override-vars-store-contents"))
496			})
497		})
498
499		Context("when the user provides a create-env override with flag-provided environment variable", func() {
500			BeforeEach(func() {
501				err := os.Unsetenv("BBL_IAAS")
502				Expect(err).NotTo(HaveOccurred())
503
504				state.IAAS = "some-fictional-iaas"
505				overrideContents := fmt.Sprintf("#!/bin/bash\n [ \"${BBL_IAAS}\" = \"some-fictional-iaas\" ]")
506
507				overridePath := filepath.Join(stateDir, "create-some-deployment-override.sh")
508				fs.WriteFile(overridePath, []byte(overrideContents), storage.ScriptMode)
509			})
510
511			It("runs the create-env-override.sh script", func() {
512				_, err := executor.CreateEnv(dirInput, state)
513				Expect(err).NotTo(HaveOccurred())
514
515				Expect(cli.RunCallCount()).To(Equal(0))
516			})
517		})
518
519		It("runs the create-env script and returns the resulting vars-store contents", func() {
520			vars, err := executor.CreateEnv(dirInput, state)
521			Expect(err).NotTo(HaveOccurred())
522
523			Expect(cli.RunCallCount()).To(Equal(0))
524			Expect(vars).To(ContainSubstring("some-vars-store-contents"))
525
526			By("setting BBL_STATE_DIR environment variable", func() {
527				bblStateDirEnv := os.Getenv("BBL_STATE_DIR")
528				Expect(bblStateDirEnv).To(Equal(stateDir))
529			})
530		})
531
532		Context("when iaas credentials are provided", func() {
533			Context("on aws", func() {
534				BeforeEach(func() {
535					state.IAAS = "aws"
536					state.AWS = storage.AWS{
537						AccessKeyID:     "some-access-key-id",
538						SecretAccessKey: "some-secret-access-key",
539					}
540				})
541
542				It("sets credentials in environment variables", func() {
543					_, err := executor.CreateEnv(dirInput, state)
544					Expect(err).NotTo(HaveOccurred())
545
546					Expect(os.Getenv("BBL_AWS_ACCESS_KEY_ID")).To(Equal("some-access-key-id"))
547					Expect(os.Getenv("BBL_AWS_SECRET_ACCESS_KEY")).To(Equal("some-secret-access-key"))
548				})
549			})
550
551			Context("on azure", func() {
552				BeforeEach(func() {
553					state.IAAS = "azure"
554					state.Azure = storage.Azure{
555						ClientID:       "some-client-id",
556						ClientSecret:   "some-client-secret",
557						SubscriptionID: "some-subscription-id",
558						TenantID:       "some-tenant-id",
559					}
560				})
561
562				It("sets credentials in environment variables", func() {
563					_, err := executor.CreateEnv(dirInput, state)
564					Expect(err).NotTo(HaveOccurred())
565
566					Expect(os.Getenv("BBL_AZURE_CLIENT_ID")).To(Equal("some-client-id"))
567					Expect(os.Getenv("BBL_AZURE_CLIENT_SECRET")).To(Equal("some-client-secret"))
568					Expect(os.Getenv("BBL_AZURE_SUBSCRIPTION_ID")).To(Equal("some-subscription-id"))
569					Expect(os.Getenv("BBL_AZURE_TENANT_ID")).To(Equal("some-tenant-id"))
570				})
571			})
572
573			Context("on gcp", func() {
574				BeforeEach(func() {
575					state.IAAS = "gcp"
576					state.GCP = storage.GCP{
577						ServiceAccountKeyPath: "some-service-account-key-path",
578						Zone:      "some-zone",
579						ProjectID: "some-project-id",
580					}
581				})
582
583				It("sets credentials in environment variables", func() {
584					_, err := executor.CreateEnv(dirInput, state)
585					Expect(err).NotTo(HaveOccurred())
586
587					Expect(os.Getenv("BBL_GCP_SERVICE_ACCOUNT_KEY_PATH")).To(Equal("some-service-account-key-path"))
588					Expect(os.Getenv("BBL_GCP_ZONE")).To(Equal("some-zone"))
589					Expect(os.Getenv("BBL_GCP_PROJECT_ID")).To(Equal("some-project-id"))
590				})
591			})
592
593			Context("on vsphere", func() {
594				It("sets credentials in environment variables", func() {
595					_, err := executor.CreateEnv(dirInput, storage.State{
596						IAAS: "vsphere",
597						VSphere: storage.VSphere{
598							VCenterUser:     "some-user",
599							VCenterPassword: "some-password",
600						},
601					})
602					Expect(err).NotTo(HaveOccurred())
603
604					Expect(os.Getenv("BBL_VSPHERE_VCENTER_USER")).To(Equal("some-user"))
605					Expect(os.Getenv("BBL_VSPHERE_VCENTER_PASSWORD")).To(Equal("some-password"))
606				})
607			})
608
609			Context("on openstack", func() {
610				It("sets credentials in environment variables", func() {
611					_, err := executor.CreateEnv(dirInput, storage.State{
612						IAAS: "openstack",
613						OpenStack: storage.OpenStack{
614							Username: "some-user",
615							Password: "some-password",
616						},
617					})
618					Expect(err).NotTo(HaveOccurred())
619
620					Expect(os.Getenv("BBL_OPENSTACK_USERNAME")).To(Equal("some-user"))
621					Expect(os.Getenv("BBL_OPENSTACK_PASSWORD")).To(Equal("some-password"))
622				})
623			})
624		})
625
626		Context("when the create-env script returns an error", func() {
627			BeforeEach(func() {
628				createEnvContents := "#!/bin/bash\nexit 1\n"
629				fs.WriteFile(createEnvPath, []byte(createEnvContents), storage.ScriptMode)
630			})
631
632			It("returns an error", func() {
633				vars, err := executor.CreateEnv(dirInput, state)
634				Expect(err).To(MatchError(fmt.Sprintf("Running %s: exit status 1", createEnvPath)))
635				Expect(vars).To(Equal(""))
636			})
637		})
638	})
639
640	Describe("DeleteEnv", func() {
641		var (
642			executor bosh.Executor
643
644			deleteEnvPath string
645			varsDir       string
646			stateDir      string
647
648			dirInput bosh.DirInput
649			state    storage.State
650		)
651
652		BeforeEach(func() {
653			fs = &afero.Afero{afero.NewOsFs()} // real os fs so we can exec scripts...
654
655			var err error
656			varsDir, err = fs.TempDir("", "")
657			Expect(err).NotTo(HaveOccurred())
658			stateDir, err = fs.TempDir("", "")
659			Expect(err).NotTo(HaveOccurred())
660
661			executor = bosh.NewExecutor(cli, fs)
662
663			dirInput = bosh.DirInput{
664				Deployment: "director",
665				VarsDir:    varsDir,
666				StateDir:   stateDir,
667			}
668
669			state = storage.State{
670				IAAS: "some-iaas",
671			}
672
673			deleteEnvPath = filepath.Join(stateDir, "delete-director.sh")
674			deleteEnvContents := "#!/bin/bash\necho delete-env > /dev/null\n"
675			fs.WriteFile(deleteEnvPath, []byte(deleteEnvContents), storage.ScriptMode)
676
677			deploymentStateJson := filepath.Join(varsDir, "bosh-state.json")
678			fs.WriteFile(deploymentStateJson, []byte("some: deployment"), storage.StateMode)
679		})
680
681		AfterEach(func() {
682			os.Unsetenv("BBL_STATE_DIR")
683			fs.Remove(filepath.Join(stateDir, "delete-director.sh"))
684		})
685
686		Context("when the user provides a delete-env override", func() {
687			BeforeEach(func() {
688				overridePath := filepath.Join(stateDir, "delete-director-override.sh")
689				overrideContents := fmt.Sprintf("#!/bin/bash\necho 'override' > %s/delete-env-output\n", varsDir)
690
691				fs.WriteFile(overridePath, []byte(overrideContents), storage.ScriptMode)
692			})
693
694			AfterEach(func() {
695				fs.Remove(filepath.Join(varsDir, "delete-env-output"))
696				fs.Remove(filepath.Join(stateDir, "delete-director-override.sh"))
697			})
698
699			It("runs the delete-env-override.sh script", func() {
700				err := executor.DeleteEnv(dirInput, state)
701				Expect(err).NotTo(HaveOccurred())
702
703				Expect(cli.RunCallCount()).To(Equal(0))
704
705				overrideOut, err := fs.ReadFile(filepath.Join(varsDir, "delete-env-output"))
706				Expect(err).NotTo(HaveOccurred())
707				Expect(overrideOut).To(ContainSubstring("override"))
708			})
709		})
710
711		Context("when the user tries to delete a jumpbox", func() {
712			BeforeEach(func() {
713				dirInput.Deployment = "jumpbox"
714				deleteEnvPath = filepath.Join(stateDir, "delete-jumpbox.sh")
715				deleteEnvContents := "#!/bin/bash\necho delete-env > /dev/null\n"
716				fs.WriteFile(deleteEnvPath, []byte(deleteEnvContents), storage.ScriptMode)
717
718				deploymentStateJson := filepath.Join(varsDir, "jumpbox-state.json")
719				fs.WriteFile(deploymentStateJson, []byte("some: deployment"), storage.StateMode)
720			})
721
722			AfterEach(func() {
723				fs.Remove(filepath.Join(stateDir, "delete-jumpbox.sh"))
724				fs.Remove(filepath.Join(stateDir, "jumpbox-state.json"))
725			})
726
727			It("deletes a bosh environment with the delete-env script", func() {
728				err := executor.DeleteEnv(dirInput, state)
729				Expect(err).NotTo(HaveOccurred())
730
731				Expect(cli.RunCallCount()).To(Equal(0))
732
733				By("setting BBL_STATE_DIR environment variable", func() {
734					bblStateDirEnv := os.Getenv("BBL_STATE_DIR")
735					Expect(bblStateDirEnv).To(Equal(stateDir))
736				})
737			})
738		})
739
740		Context("when the user tries to delete an unfamiliar deployment-type-thing", func() {
741			BeforeEach(func() {
742				dirInput.Deployment = "garbaggio-deployment"
743			})
744
745			It("errors reasonably", func() {
746				err := executor.DeleteEnv(dirInput, state)
747				Expect(err).To(HaveOccurred())
748			})
749		})
750
751		It("deletes a bosh environment with the delete-env script", func() {
752			err := executor.DeleteEnv(dirInput, state)
753			Expect(err).NotTo(HaveOccurred())
754
755			Expect(cli.RunCallCount()).To(Equal(0))
756
757			By("setting BBL_STATE_DIR environment variable", func() {
758				bblStateDirEnv := os.Getenv("BBL_STATE_DIR")
759				Expect(bblStateDirEnv).To(Equal(stateDir))
760			})
761		})
762
763		Context("when iaas credentials are provided", func() {
764			Context("on aws", func() {
765				BeforeEach(func() {
766					state.IAAS = "aws"
767					state.AWS = storage.AWS{
768						AccessKeyID:     "some-access-key-id",
769						SecretAccessKey: "some-secret-access-key",
770					}
771				})
772
773				It("sets credentials in environment variables", func() {
774					err := executor.DeleteEnv(dirInput, state)
775					Expect(err).NotTo(HaveOccurred())
776
777					Expect(os.Getenv("BBL_AWS_ACCESS_KEY_ID")).To(Equal("some-access-key-id"))
778					Expect(os.Getenv("BBL_AWS_SECRET_ACCESS_KEY")).To(Equal("some-secret-access-key"))
779				})
780			})
781
782			Context("on azure", func() {
783				BeforeEach(func() {
784					state.IAAS = "azure"
785					state.Azure = storage.Azure{
786						ClientID:       "some-client-id",
787						ClientSecret:   "some-client-secret",
788						SubscriptionID: "some-subscription-id",
789						TenantID:       "some-tenant-id",
790					}
791				})
792
793				It("sets credentials in environment variables", func() {
794					err := executor.DeleteEnv(dirInput, state)
795					Expect(err).NotTo(HaveOccurred())
796
797					Expect(os.Getenv("BBL_AZURE_CLIENT_ID")).To(Equal("some-client-id"))
798					Expect(os.Getenv("BBL_AZURE_CLIENT_SECRET")).To(Equal("some-client-secret"))
799					Expect(os.Getenv("BBL_AZURE_SUBSCRIPTION_ID")).To(Equal("some-subscription-id"))
800					Expect(os.Getenv("BBL_AZURE_TENANT_ID")).To(Equal("some-tenant-id"))
801				})
802			})
803
804			Context("on gcp", func() {
805				BeforeEach(func() {
806					state.IAAS = "gcp"
807					state.GCP = storage.GCP{
808						ServiceAccountKeyPath: "some-service-account-key-path",
809						Zone:      "some-zone",
810						ProjectID: "some-project-id",
811					}
812				})
813
814				It("sets credentials in environment variables", func() {
815					err := executor.DeleteEnv(dirInput, state)
816					Expect(err).NotTo(HaveOccurred())
817
818					Expect(os.Getenv("BBL_GCP_SERVICE_ACCOUNT_KEY_PATH")).To(Equal("some-service-account-key-path"))
819					Expect(os.Getenv("BBL_GCP_ZONE")).To(Equal("some-zone"))
820					Expect(os.Getenv("BBL_GCP_PROJECT_ID")).To(Equal("some-project-id"))
821				})
822			})
823
824			Context("on vsphere", func() {
825				BeforeEach(func() {
826					state.IAAS = "vsphere"
827					state.VSphere = storage.VSphere{
828						VCenterUser:     "some-user",
829						VCenterPassword: "some-password",
830					}
831				})
832
833				It("sets credentials in environment variables", func() {
834					err := executor.DeleteEnv(dirInput, state)
835					Expect(err).NotTo(HaveOccurred())
836
837					Expect(os.Getenv("BBL_VSPHERE_VCENTER_USER")).To(Equal("some-user"))
838					Expect(os.Getenv("BBL_VSPHERE_VCENTER_PASSWORD")).To(Equal("some-password"))
839				})
840			})
841		})
842
843		Context("when the create-env script returns an error", func() {
844			BeforeEach(func() {
845				deleteEnvContents := "#!/bin/bash\nexit 1\n"
846				fs.WriteFile(deleteEnvPath, []byte(deleteEnvContents), storage.ScriptMode)
847			})
848
849			It("returns an error", func() {
850				err := executor.DeleteEnv(dirInput, state)
851				Expect(err).To(MatchError("Run bosh delete-env director: exit status 1"))
852			})
853		})
854	})
855
856	Describe("Version", func() {
857		var (
858			cli      *fakes.BOSHCLI
859			executor bosh.Executor
860		)
861		BeforeEach(func() {
862			cli = &fakes.BOSHCLI{}
863			cli.RunStub = func(stdout io.Writer, workingDirectory string, args []string) error {
864				stdout.Write([]byte("some-text version 1.1.1 some-other-text"))
865				return nil
866			}
867
868			executor = bosh.NewExecutor(cli, fs)
869		})
870
871		It("returns the correctly trimmed version", func() {
872			version, err := executor.Version()
873			Expect(err).NotTo(HaveOccurred())
874
875			_, _, args := cli.RunArgsForCall(0)
876			Expect(args).To(Equal([]string{"-v"}))
877
878			Expect(version).To(Equal("1.1.1"))
879		})
880
881		Context("when the run cli fails", func() {
882			BeforeEach(func() {
883				cli.RunStub = nil
884				cli.RunCall.Returns.Error = errors.New("banana")
885			})
886
887			It("returns an error", func() {
888				_, err := executor.Version()
889				Expect(err).To(MatchError("banana"))
890			})
891		})
892
893		Context("when the version cannot be parsed", func() {
894			BeforeEach(func() {
895				cli.RunStub = func(stdout io.Writer, workingDirectory string, args []string) error {
896					stdout.Write([]byte(""))
897					return nil
898				}
899			})
900
901			It("returns a bosh version error", func() {
902				_, err := executor.Version()
903				Expect(err).To(MatchError("BOSH version could not be parsed"))
904			})
905		})
906	})
907})
908
909func formatScript(command string, stateDir string, args []string) string {
910	script := fmt.Sprintf("#!/bin/sh\nbosh-path %s \\\n", command)
911	for _, arg := range args {
912		if arg[0] == '-' {
913			script = fmt.Sprintf("%s  %s", script, arg)
914		} else {
915			script = fmt.Sprintf("%s  %s \\\n", script, arg)
916		}
917	}
918
919	return fmt.Sprintf("%s\n", script[:len(script)-2])
920}
921
922type behavesLikePlanFs interface {
923	fileio.FileReader
924	fileio.Stater
925}
926
927func behavesLikePlan(expectedArgs []string, cli *fakes.BOSHCLI, fs behavesLikePlanFs, executor bosh.Executor, input bosh.DirInput, deploymentDir, iaas, stateDir string) {
928	cli.RunStub = func(stdout io.Writer, workingDirectory string, args []string) error {
929		stdout.Write([]byte("some-manifest"))
930		return nil
931	}
932
933	err := executor.PlanDirector(input, deploymentDir, iaas)
934	Expect(err).NotTo(HaveOccurred())
935	Expect(cli.RunCallCount()).To(Equal(0))
936
937	By("writing the create-env args to a shell script", func() {
938		expectedScript := formatScript("create-env", stateDir, expectedArgs)
939		scriptPath := fmt.Sprintf("%s/create-director.sh", stateDir)
940		shellScript, err := fs.ReadFile(scriptPath)
941		Expect(err).NotTo(HaveOccurred())
942
943		fileinfo, err := fs.Stat(scriptPath)
944		Expect(err).NotTo(HaveOccurred())
945		Expect(fileinfo.Mode().String()).To(Equal("-rwxr-x---"))
946		Expect(err).NotTo(HaveOccurred())
947
948		Expect(string(shellScript)).To(Equal(expectedScript))
949	})
950
951	By("writing the delete-env args to a shell script", func() {
952		expectedScript := formatScript("delete-env", stateDir, expectedArgs)
953		scriptPath := fmt.Sprintf("%s/delete-director.sh", stateDir)
954		shellScript, err := fs.ReadFile(scriptPath)
955		Expect(err).NotTo(HaveOccurred())
956
957		fileinfo, err := fs.Stat(scriptPath)
958		Expect(err).NotTo(HaveOccurred())
959		Expect(fileinfo.Mode().String()).To(Equal("-rwxr-x---"))
960		Expect(err).NotTo(HaveOccurred())
961		Expect(err).NotTo(HaveOccurred())
962
963		Expect(string(shellScript)).To(Equal(expectedScript))
964	})
965}
966