diff --git a/bin/gen-fakes b/bin/gen-fakes index 6efae229c..ddf07a10c 100755 --- a/bin/gen-fakes +++ b/bin/gen-fakes @@ -31,6 +31,9 @@ counterfeiter director FileReporter counterfeiter director TaskReporter counterfeiter director Event +# FakeInstallation is generated in cmd/cmdfakes because that's where it's used +counterfeiter -o cmd/cmdfakes installation Installation + counterfeiter uaa UAA counterfeiter uaa Token counterfeiter uaa AccessToken diff --git a/cloud/factory.go b/cloud/factory.go index e46bdb527..562352e39 100644 --- a/cloud/factory.go +++ b/cloud/factory.go @@ -31,19 +31,28 @@ func NewFactory( } func (f *factory) NewCloud(installation biinstall.Installation, directorID string, stemcellApiVersion int) (Cloud, error) { - cpiJob := installation.Job() - target := installation.Target() - cpi := CPI{ - JobPath: cpiJob.Path, - JobsDir: target.JobsPath(), - PackagesDir: target.PackagesPath(), + numberCpiBinariesFound := 0 + foundCPI := CPI{} + + for _, cpiJob := range installation.Jobs() { + target := installation.Target() + cpi := CPI{ + JobPath: cpiJob.Path, + JobsDir: target.JobsPath(), + PackagesDir: target.PackagesPath(), + } + + cmdPath := cpi.ExecutablePath() + if f.fs.FileExists(cmdPath) { + numberCpiBinariesFound += 1 + foundCPI = cpi + } } - cmdPath := cpi.ExecutablePath() - if !f.fs.FileExists(cmdPath) { - return nil, bosherr.Errorf("Installed CPI job '%s' does not contain the required executable '%s'", cpiJob.Name, cmdPath) + if numberCpiBinariesFound != 1 { + return nil, bosherr.Errorf("Found %d Jobs with a 'bin/cpi' binary. Expected 1.", numberCpiBinariesFound) } - cpiCmdRunner := NewCPICmdRunner(f.cmdRunner, cpi, f.logger) + cpiCmdRunner := NewCPICmdRunner(f.cmdRunner, foundCPI, f.logger) return NewCloud(cpiCmdRunner, directorID, stemcellApiVersion, f.logger), nil } diff --git a/cmd/cmdfakes/fake_installation.go b/cmd/cmdfakes/fake_installation.go index 46244229a..b614e8176 100644 --- a/cmd/cmdfakes/fake_installation.go +++ b/cmd/cmdfakes/fake_installation.go @@ -1,16 +1,167 @@ +// Code generated by counterfeiter. DO NOT EDIT. package cmdfakes import ( - biinstallation "github.com/cloudfoundry/bosh-cli/v7/installation" + "sync" + + "github.com/cloudfoundry/bosh-cli/v7/installation" ) type FakeInstallation struct { + JobsStub func() []installation.InstalledJob + jobsMutex sync.RWMutex + jobsArgsForCall []struct { + } + jobsReturns struct { + result1 []installation.InstalledJob + } + jobsReturnsOnCall map[int]struct { + result1 []installation.InstalledJob + } + TargetStub func() installation.Target + targetMutex sync.RWMutex + targetArgsForCall []struct { + } + targetReturns struct { + result1 installation.Target + } + targetReturnsOnCall map[int]struct { + result1 installation.Target + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeInstallation) Jobs() []installation.InstalledJob { + fake.jobsMutex.Lock() + ret, specificReturn := fake.jobsReturnsOnCall[len(fake.jobsArgsForCall)] + fake.jobsArgsForCall = append(fake.jobsArgsForCall, struct { + }{}) + stub := fake.JobsStub + fakeReturns := fake.jobsReturns + fake.recordInvocation("Jobs", []interface{}{}) + fake.jobsMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeInstallation) JobsCallCount() int { + fake.jobsMutex.RLock() + defer fake.jobsMutex.RUnlock() + return len(fake.jobsArgsForCall) +} + +func (fake *FakeInstallation) JobsCalls(stub func() []installation.InstalledJob) { + fake.jobsMutex.Lock() + defer fake.jobsMutex.Unlock() + fake.JobsStub = stub +} + +func (fake *FakeInstallation) JobsReturns(result1 []installation.InstalledJob) { + fake.jobsMutex.Lock() + defer fake.jobsMutex.Unlock() + fake.JobsStub = nil + fake.jobsReturns = struct { + result1 []installation.InstalledJob + }{result1} +} + +func (fake *FakeInstallation) JobsReturnsOnCall(i int, result1 []installation.InstalledJob) { + fake.jobsMutex.Lock() + defer fake.jobsMutex.Unlock() + fake.JobsStub = nil + if fake.jobsReturnsOnCall == nil { + fake.jobsReturnsOnCall = make(map[int]struct { + result1 []installation.InstalledJob + }) + } + fake.jobsReturnsOnCall[i] = struct { + result1 []installation.InstalledJob + }{result1} } -func (f *FakeInstallation) Target() biinstallation.Target { - return biinstallation.Target{} +func (fake *FakeInstallation) Target() installation.Target { + fake.targetMutex.Lock() + ret, specificReturn := fake.targetReturnsOnCall[len(fake.targetArgsForCall)] + fake.targetArgsForCall = append(fake.targetArgsForCall, struct { + }{}) + stub := fake.TargetStub + fakeReturns := fake.targetReturns + fake.recordInvocation("Target", []interface{}{}) + fake.targetMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 } -func (f *FakeInstallation) Job() biinstallation.InstalledJob { - return biinstallation.InstalledJob{} +func (fake *FakeInstallation) TargetCallCount() int { + fake.targetMutex.RLock() + defer fake.targetMutex.RUnlock() + return len(fake.targetArgsForCall) } + +func (fake *FakeInstallation) TargetCalls(stub func() installation.Target) { + fake.targetMutex.Lock() + defer fake.targetMutex.Unlock() + fake.TargetStub = stub +} + +func (fake *FakeInstallation) TargetReturns(result1 installation.Target) { + fake.targetMutex.Lock() + defer fake.targetMutex.Unlock() + fake.TargetStub = nil + fake.targetReturns = struct { + result1 installation.Target + }{result1} +} + +func (fake *FakeInstallation) TargetReturnsOnCall(i int, result1 installation.Target) { + fake.targetMutex.Lock() + defer fake.targetMutex.Unlock() + fake.TargetStub = nil + if fake.targetReturnsOnCall == nil { + fake.targetReturnsOnCall = make(map[int]struct { + result1 installation.Target + }) + } + fake.targetReturnsOnCall[i] = struct { + result1 installation.Target + }{result1} +} + +func (fake *FakeInstallation) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.jobsMutex.RLock() + defer fake.jobsMutex.RUnlock() + fake.targetMutex.RLock() + defer fake.targetMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeInstallation) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ installation.Installation = new(FakeInstallation) diff --git a/cmd/create_env_test.go b/cmd/create_env_test.go index 1156a0ca2..0e6afb5f1 100644 --- a/cmd/create_env_test.go +++ b/cmd/create_env_test.go @@ -251,9 +251,8 @@ var _ = Describe("CreateEnvCmd", func() { // parsed CPI deployment manifest installationManifest = biinstallmanifest.Manifest{ - Template: biinstallmanifest.ReleaseJobRef{ - Name: "fake-cpi-release-job-name", - Release: "fake-cpi-release-name", + Templates: []biinstallmanifest.ReleaseJobRef{ + {Name: "fake-cpi-release-job-name", Release: "fake-cpi-release-name"}, }, Mbus: mbusURL, } @@ -322,7 +321,6 @@ var _ = Describe("CreateEnvCmd", func() { cpiInstaller := bicpirel.CpiInstaller{ ReleaseManager: releaseManager, InstallerFactory: mockInstallerFactory, - Validator: bicpirel.NewValidator(), } releaseFetcher := biinstall.NewReleaseFetcher(tarballProvider, releaseReader, releaseManager) stemcellFetcher := bistemcell.Fetcher{ @@ -430,14 +428,15 @@ var _ = Describe("CreateEnvCmd", func() { mockInstallerFactory.EXPECT().NewInstaller(target).Return(mockInstaller).AnyTimes() - installation := biinstall.NewInstallation(target, installedJob, installationManifest) + installation := biinstall.NewInstallation(target, []biinstall.InstalledJob{installedJob}, + installationManifest) expectInstall = mockInstaller.EXPECT().Install(installationManifest, gomock.Any()).Do(func(_ interface{}, stage boshui.Stage) { Expect(fakeStage.SubStages).To(ContainElement(stage)) }).Return(installation, nil).AnyTimes() mockInstaller.EXPECT().Cleanup(installation).AnyTimes() - //mockDeployment := mock_deployment.NewMockDeployment(mockCtrl) + // mockDeployment := mock_deployment.NewMockDeployment(mockCtrl) expectDeploy = mockDeployer.EXPECT().Deploy( mockCloud, @@ -748,7 +747,8 @@ var _ = Describe("CreateEnvCmd", func() { It("returns error", func() { err := command.Run(fakeStage, defaultCreateEnvOpts) Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(Equal("Invalid CPI release 'fake-cpi-release-name': CPI release must contain specified job 'fake-cpi-release-job-name'")) + //nolint:gosimple + Expect(err.Error()).To(Equal(fmt.Sprintf("Found 0 releases containing a template that renders to target 'bin/cpi'. Expected to find 1. Releases inspected: [fake-cpi-release-name]\nrelease 'fake-cpi-release-name' must contain specified job 'fake-cpi-release-job-name'"))) }) }) diff --git a/cmd/deployment_deleter.go b/cmd/deployment_deleter.go index 1503c3274..74a17ec7b 100644 --- a/cmd/deployment_deleter.go +++ b/cmd/deployment_deleter.go @@ -126,15 +126,23 @@ func (c *deploymentDeleter) DeleteDeployment(skipDrain bool, stage biui.Stage) ( return err } - cpiReleaseName := installationManifest.Template.Release - cpiReleaseRef, found := releaseSetManifest.FindByName(cpiReleaseName) - if !found { - return bosherr.Errorf("installation release '%s' must refer to a release in releases", cpiReleaseName) + errs := []error{} + for _, template := range installationManifest.Templates { + + cpiReleaseName := template.Release + cpiReleaseRef, found := releaseSetManifest.FindByName(cpiReleaseName) + if !found { + return bosherr.Errorf("installation release '%s' must refer to a release in releases", cpiReleaseName) + } + + err = c.releaseFetcher.DownloadAndExtract(cpiReleaseRef, stage) + if err != nil { + errs = append(errs, err) + } } - err = c.releaseFetcher.DownloadAndExtract(cpiReleaseRef, stage) - if err != nil { - return err + if len(errs) > 0 { + return bosherr.NewMultiError(errs...) } err = c.cpiInstaller.ValidateCpiRelease(installationManifest, stage) diff --git a/cmd/deployment_deleter_test.go b/cmd/deployment_deleter_test.go index 3eab01878..3416a0945 100644 --- a/cmd/deployment_deleter_test.go +++ b/cmd/deployment_deleter_test.go @@ -182,9 +182,8 @@ cloud_provider: var allowCPIToBeInstalled = func() { installationManifest := biinstallmanifest.Manifest{ Name: "test-release", - Template: biinstallmanifest.ReleaseJobRef{ - Name: "fake-cpi-release-job-name", - Release: "fake-cpi-release-name", + Templates: []biinstallmanifest.ReleaseJobRef{ + {Name: "fake-cpi-release-job-name", Release: "fake-cpi-release-name"}, }, Mbus: mbusURL, Properties: biproperty.Map{}, @@ -216,7 +215,6 @@ cloud_provider: cpiInstaller := bicpirel.CpiInstaller{ ReleaseManager: releaseManager, InstallerFactory: mockInstallerFactory, - Validator: bicpirel.NewValidator(), } releaseFetcher := biinstall.NewReleaseFetcher(tarballProvider, releaseReader, releaseManager) releaseSetAndInstallationManifestParser := cmd.ReleaseSetAndInstallationManifestParser{ @@ -526,9 +524,8 @@ cloud_provider: JustBeforeEach(func() { installationManifest := biinstallmanifest.Manifest{ Name: "test-release", - Template: biinstallmanifest.ReleaseJobRef{ - Name: "fake-cpi-release-job-name", - Release: "fake-cpi-release-name", + Templates: []biinstallmanifest.ReleaseJobRef{ + {Name: "fake-cpi-release-job-name", Release: "fake-cpi-release-name"}, }, Mbus: mbusURL, Properties: biproperty.Map{}, diff --git a/cmd/env_factory.go b/cmd/env_factory.go index c0a50f4c1..b54cb77f0 100644 --- a/cmd/env_factory.go +++ b/cmd/env_factory.go @@ -122,7 +122,6 @@ func NewEnvFactory( f.cpiInstaller = bicpirel.CpiInstaller{ ReleaseManager: f.releaseManager, InstallerFactory: installerFactory, - Validator: bicpirel.NewValidator(), } } diff --git a/cpi/release/installer.go b/cpi/release/installer.go index 10a2b32be..816259e9a 100644 --- a/cpi/release/installer.go +++ b/cpi/release/installer.go @@ -9,25 +9,55 @@ import ( biui "github.com/cloudfoundry/bosh-cli/v7/ui" ) +const ( + ReleaseBinaryName = "bin/cpi" +) + type CpiInstaller struct { ReleaseManager birel.Manager InstallerFactory biinstall.InstallerFactory - Validator Validator } func (i CpiInstaller) ValidateCpiRelease(installationManifest biinstallmanifest.Manifest, stage biui.Stage) error { return stage.Perform("Validating cpi release", func() error { - cpiReleaseName := installationManifest.Template.Release - cpiRelease, found := i.ReleaseManager.Find(cpiReleaseName) - if !found { - return bosherr.Errorf("installation release '%s' must refer to a provided release", cpiReleaseName) + var ( + errs []error + releasePackagingErrs []error + releaseNamesInspected []string + numberCpiBinariesFound = 0 + ) + + for _, template := range installationManifest.Templates { + releaseName := template.Release + releaseJobName := template.Name + release, found := i.ReleaseManager.Find(releaseName) + releaseNamesInspected = append(releaseNamesInspected, releaseName) + + if !found { + releasePackagingErrs = append(releasePackagingErrs, bosherr.Errorf("installation release '%s' must refer to a provided release", releaseName)) + continue + } + + job, ok := release.FindJobByName(releaseJobName) + + if !ok { + releasePackagingErrs = append(releasePackagingErrs, bosherr.Errorf("release '%s' must contain specified job '%s'", releaseName, releaseJobName)) + continue + } + + _, ok = job.FindTemplateByValue(ReleaseBinaryName) + if ok { + numberCpiBinariesFound += 1 + } } - err := i.Validator.Validate(cpiRelease, installationManifest.Template.Name) - if err != nil { - return bosherr.WrapErrorf(err, "Invalid CPI release '%s'", cpiReleaseName) + if numberCpiBinariesFound != 1 { + errs = append(errs, bosherr.Errorf("Found %d releases containing a template that renders to target '%s'. Expected to find 1. Releases inspected: %v", numberCpiBinariesFound, ReleaseBinaryName, releaseNamesInspected)) + errs = append(errs, releasePackagingErrs...) + return bosherr.NewMultiError(errs...) + } else { + return nil } - return nil }) } diff --git a/cpi/release/installer_test.go b/cpi/release/installer_test.go index 7f5ecec94..dc72886af 100644 --- a/cpi/release/installer_test.go +++ b/cpi/release/installer_test.go @@ -52,6 +52,8 @@ var _ = Describe("Installer", func() { expectCleanup = mockInstaller.EXPECT().Cleanup(installation).Return(nil) }) + It("should validate CPI release that include CPI and plugin releases", Pending, func() {}) + It("should install the CPI and call the passed in function with the installation", func() { cpiInstaller := release.CpiInstaller{ InstallerFactory: mockInstallerFactory, diff --git a/cpi/release/validator.go b/cpi/release/validator.go deleted file mode 100644 index d9890bd7e..000000000 --- a/cpi/release/validator.go +++ /dev/null @@ -1,32 +0,0 @@ -package release - -import ( - bosherr "github.com/cloudfoundry/bosh-utils/errors" - - birel "github.com/cloudfoundry/bosh-cli/v7/release" -) - -const ( - ReleaseBinaryName = "bin/cpi" -) - -type Validator struct { -} - -func NewValidator() Validator { - return Validator{} -} - -func (v Validator) Validate(release birel.Release, cpiReleaseJobName string) error { - job, ok := release.FindJobByName(cpiReleaseJobName) - if !ok { - return bosherr.Errorf("CPI release must contain specified job '%s'", cpiReleaseJobName) - } - - _, ok = job.FindTemplateByValue(ReleaseBinaryName) - if !ok { - return bosherr.Errorf("Specified CPI release job '%s' must contain a template that renders to target '%s'", cpiReleaseJobName, ReleaseBinaryName) - } - - return nil -} diff --git a/cpi/release/validator_test.go b/cpi/release/validator_test.go deleted file mode 100644 index 0160e68d7..000000000 --- a/cpi/release/validator_test.go +++ /dev/null @@ -1,100 +0,0 @@ -package release_test - -import ( - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - . "github.com/cloudfoundry/bosh-cli/v7/cpi/release" - boshrel "github.com/cloudfoundry/bosh-cli/v7/release" - boshjob "github.com/cloudfoundry/bosh-cli/v7/release/job" - fakerel "github.com/cloudfoundry/bosh-cli/v7/release/releasefakes" - . "github.com/cloudfoundry/bosh-cli/v7/release/resource" -) - -var _ = Describe("Validator", func() { - var cpiReleaseJobName = "fake-cpi-release-job-name" - - It("validates a valid release without error", func() { - job := boshjob.NewJob(NewResourceWithBuiltArchive( - "fake-cpi-release-job-name", "fake-job-1-fingerprint", "", "fake-job-1-sha")) - - job.Templates = map[string]string{"cpi.erb": "bin/cpi"} - - release := &fakerel.FakeRelease{ - NameStub: func() string { return "fake-release-name" }, - VersionStub: func() string { return "fake-release-version" }, - - FindJobByNameStub: func(name string) (boshjob.Job, bool) { - Expect(name).To(Equal(job.Name())) - return *job, true - }, - } - - validator := NewValidator() - - err := validator.Validate(release, cpiReleaseJobName) - Expect(err).NotTo(HaveOccurred()) - }) - - Context("when the cpi job is not present", func() { - var validator Validator - var release *fakerel.FakeRelease - - BeforeEach(func() { - job := boshjob.NewJob(NewResourceWithBuiltArchive( - "non-cpi-job", "fake-job-1-fingerprint", "", "fake-job-1-sha")) - - job.Templates = map[string]string{"cpi.erb": "bin/cpi"} - - release = &fakerel.FakeRelease{ - NameStub: func() string { return "fake-release-name" }, - VersionStub: func() string { return "fake-release-version" }, - - FindJobByNameStub: func(name string) (boshjob.Job, bool) { - Expect(name).To(Equal(cpiReleaseJobName)) - return boshjob.Job{}, false - }, - } - - validator = NewValidator() - }) - - It("returns an error that the cpi job is not present", func() { - err := validator.Validate(release, cpiReleaseJobName) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring( - "CPI release must contain specified job 'fake-cpi-release-job-name'")) - }) - }) - - Context("when the templates are missing a bin/cpi target", func() { - var validator Validator - var release boshrel.Release - - BeforeEach(func() { - job := boshjob.NewJob(NewResourceWithBuiltArchive( - "fake-cpi-release-job-name", "fake-job-1-fingerprint", "", "fake-job-1-sha")) - - job.Templates = map[string]string{"cpi.erb": "nonsense"} - - release = &fakerel.FakeRelease{ - NameStub: func() string { return "fake-release-name" }, - VersionStub: func() string { return "fake-release-version" }, - - FindJobByNameStub: func(name string) (boshjob.Job, bool) { - Expect(name).To(Equal(job.Name())) - return *job, true - }, - } - - validator = NewValidator() - }) - - It("returns an error that the bin/cpi template target is missing", func() { - err := validator.Validate(release, cpiReleaseJobName) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring( - "Specified CPI release job 'fake-cpi-release-job-name' must contain a template that renders to target 'bin/cpi'")) - }) - }) -}) diff --git a/installation/installation.go b/installation/installation.go index 8b1c564da..b2a34113a 100644 --- a/installation/installation.go +++ b/installation/installation.go @@ -6,23 +6,23 @@ import ( type Installation interface { Target() Target - Job() InstalledJob + Jobs() []InstalledJob } type installation struct { target Target - job InstalledJob + jobs []InstalledJob manifest biinstallmanifest.Manifest } func NewInstallation( target Target, - job InstalledJob, + jobs []InstalledJob, manifest biinstallmanifest.Manifest, ) Installation { return &installation{ target: target, - job: job, + jobs: jobs, manifest: manifest, } } @@ -31,6 +31,6 @@ func (i *installation) Target() Target { return i.target } -func (i *installation) Job() InstalledJob { - return i.job +func (i *installation) Jobs() []InstalledJob { + return i.jobs } diff --git a/installation/installer.go b/installation/installer.go index 28b2f52ad..efb6aa532 100644 --- a/installation/installer.go +++ b/installation/installer.go @@ -81,22 +81,38 @@ func (i *installer) Install(manifest biinstallmanifest.Manifest, stage biui.Stag return nil, bosherr.WrapError(err, "Rendering and uploading Jobs") } - renderedCPIJob := renderedJobRefs[0] - installedJob, err := i.installJob(renderedCPIJob, stage) - if err != nil { - return nil, bosherr.WrapErrorf(err, "Installing job '%s' for CPI release", renderedCPIJob.Name) + installedJobs := []InstalledJob{} + + for _, renderedCPIJob := range renderedJobRefs { + installedJob, err := i.installJob(renderedCPIJob, stage) + if err != nil { + return nil, bosherr.WrapErrorf(err, "Installing job '%s' for CPI release", renderedCPIJob.Name) + } + installedJobs = append(installedJobs, installedJob) + } return NewInstallation( i.target, - installedJob, + installedJobs, manifest, ), nil } func (i *installer) Cleanup(installation Installation) error { - job := installation.Job() - return i.blobExtractor.Cleanup(job.BlobstoreID, job.Path) + errs := []error{} + for _, job := range installation.Jobs() { + err := i.blobExtractor.Cleanup(job.BlobstoreID, job.Path) + if err != nil { + errs = append(errs, err) + } + } + + if len(errs) > 0 { + return bosherr.NewMultiError(errs...) + } else { + return nil + } } func (i *installer) installPackages(compiledPackages []CompiledPackageRef) error { diff --git a/installation/installer_test.go b/installation/installer_test.go index 896c99a17..32a852456 100644 --- a/installation/installer_test.go +++ b/installation/installer_test.go @@ -37,9 +37,9 @@ var _ = Describe("Installer", func() { logger boshlog.Logger - installer Installer - target Target - installedJob InstalledJob + installer Installer + target Target + installedJobs []InstalledJob ) BeforeEach(func() { @@ -56,7 +56,12 @@ var _ = Describe("Installer", func() { Properties: biproperty.Map{}, } renderedCPIJob := NewRenderedJobRef("cpi", "fake-release-job-fingerprint", "fake-rendered-job-blobstore-id", "fake-rendered-job-blobstore-id") - installedJob = NewInstalledJob(renderedCPIJob, "/extracted-release-path/cpi") + renderedCPIPluginJob := NewRenderedJobRef("cpi-plugin", "fake-release-job-fingerprint", "fake-rendered-job-blobstore-id", "fake-rendered-job-blobstore-id") + + installedJobs = make([]InstalledJob, 0) + installedJobs = append(installedJobs, NewInstalledJob(renderedCPIJob, "/extracted-release-path/cpi")) + installedJobs = append(installedJobs, NewInstalledJob(renderedCPIPluginJob, + "/extracted-release-path/cpi-plugin")) }) JustBeforeEach(func() { @@ -92,7 +97,11 @@ var _ = Describe("Installer", func() { compiledPackages := []CompiledPackageRef{ref} releaseJobs = []bireljob.Job{} - renderedJobRefs = []RenderedJobRef{installedJob.RenderedJobRef} + + renderedJobRefs = make([]RenderedJobRef, 0) + for _, installedJob := range installedJobs { + renderedJobRefs = append(renderedJobRefs, installedJob.RenderedJobRef) + } mockJobResolver.EXPECT().From(installationManifest).Return(releaseJobs, nil).AnyTimes() mockPackageCompiler.EXPECT().For(releaseJobs, fakeStage).Return(compiledPackages, nil).AnyTimes() }) @@ -137,7 +146,7 @@ var _ = Describe("Installer", func() { BeforeEach(func() { installation = NewInstallation( target, - installedJob, + installedJobs, installationManifest, ) }) @@ -146,11 +155,13 @@ var _ = Describe("Installer", func() { err := installer.Cleanup(installation) Expect(err).ToNot(HaveOccurred()) - Expect(fakeExtractor.CleanupCallCount()).To(Equal(1)) + Expect(fakeExtractor.CleanupCallCount()).To(Equal(2)) - blobstoreID, extractedBlobPath := fakeExtractor.CleanupArgsForCall(0) - Expect(blobstoreID).To(Equal(installedJob.BlobstoreID)) - Expect(extractedBlobPath).To(Equal(installedJob.Path)) + for i, installedJob := range installedJobs { + blobstoreID, extractedBlobPath := fakeExtractor.CleanupArgsForCall(i) + Expect(blobstoreID).To(Equal(installedJob.BlobstoreID)) + Expect(extractedBlobPath).To(Equal(installedJob.Path)) + } }) It("returns errors when cleaning up installed jobs", func() { diff --git a/installation/job_renderer.go b/installation/job_renderer.go index eab05e9ef..bf1b59792 100644 --- a/installation/job_renderer.go +++ b/installation/job_renderer.go @@ -61,10 +61,6 @@ func (b *jobRenderer) RenderAndUploadFrom(installationManifest biinstallmanifest return nil, bosherr.WrapError(err, "Rendering job templates for installation") } - if len(renderedJobRefs) != 1 { - return nil, bosherr.Error("Too many jobs rendered... oops?") - } - return renderedJobRefs, nil } diff --git a/installation/job_renderer_test.go b/installation/job_renderer_test.go index 4bae6e770..ba169226e 100644 --- a/installation/job_renderer_test.go +++ b/installation/job_renderer_test.go @@ -65,9 +65,8 @@ var _ = Describe("JobRenderer", func() { manifest = biinstallmanifest.Manifest{ Name: "fake-installation-name", - Template: biinstallmanifest.ReleaseJobRef{ - Name: "fake-cpi-job-name", - Release: "fake-cpi-release-name", + Templates: []biinstallmanifest.ReleaseJobRef{ + {Name: "fake-cpi-job-name", Release: "fake-cpi-release-name"}, }, Properties: biproperty.Map{ "fake-installation-property": "fake-installation-property-value", diff --git a/installation/job_resolver.go b/installation/job_resolver.go index 275c6480e..468bcc713 100644 --- a/installation/job_resolver.go +++ b/installation/job_resolver.go @@ -26,7 +26,10 @@ func NewJobResolver( func (b *jobResolver) From(installationManifest biinstallmanifest.Manifest) ([]bireljob.Job, error) { // installation only ever has one job: the cpi job. - jobsReferencesInRelease := []biinstallmanifest.ReleaseJobRef{installationManifest.Template} + jobsReferencesInRelease := []biinstallmanifest.ReleaseJobRef{} + for _, template := range installationManifest.Templates { + jobsReferencesInRelease = append(jobsReferencesInRelease, biinstallmanifest.ReleaseJobRef{Name: template.Name, Release: template.Release}) + } releaseJobs := make([]bireljob.Job, len(jobsReferencesInRelease)) for i, jobRef := range jobsReferencesInRelease { diff --git a/installation/job_resolver_test.go b/installation/job_resolver_test.go index e04e6339b..afd8612b3 100644 --- a/installation/job_resolver_test.go +++ b/installation/job_resolver_test.go @@ -38,9 +38,8 @@ var _ = Describe("JobResolver", func() { manifest = biinstallmanifest.Manifest{ Name: "fake-installation-name", - Template: biinstallmanifest.ReleaseJobRef{ - Name: "fake-cpi-job-name", - Release: "fake-cpi-release-name", + Templates: []biinstallmanifest.ReleaseJobRef{ + {Name: "fake-cpi-job-name", Release: "fake-cpi-release-name"}, }, Properties: biproperty.Map{ "fake-installation-property": "fake-installation-property-value", diff --git a/installation/manifest/manifest.go b/installation/manifest/manifest.go index cb95407b4..1cd724f9e 100644 --- a/installation/manifest/manifest.go +++ b/installation/manifest/manifest.go @@ -5,8 +5,10 @@ import ( ) type Manifest struct { - Name string + Name string + // Deprecated: use Templates instead Template ReleaseJobRef + Templates []ReleaseJobRef Properties biproperty.Map Mbus string Cert Certificate diff --git a/installation/manifest/parser.go b/installation/manifest/parser.go index 04c4d7b12..1cfa4ee65 100644 --- a/installation/manifest/parser.go +++ b/installation/manifest/parser.go @@ -36,6 +36,7 @@ type manifest struct { type installation struct { Template template + Templates []template Properties map[interface{}]interface{} SSHTunnel SSHTunnel `yaml:"ssh_tunnel"` Mbus string @@ -113,14 +114,30 @@ func (p *parser) Parse(path string, vars boshtpl.Variables, op patch.Op, release installationManifest := Manifest{ Name: comboManifest.Name, - Template: ReleaseJobRef{ - Name: comboManifest.CloudProvider.Template.Name, - Release: comboManifest.CloudProvider.Template.Release, - }, Mbus: comboManifest.CloudProvider.Mbus, Cert: comboManifest.CloudProvider.Cert, } + templateList := []ReleaseJobRef{} + if len(comboManifest.CloudProvider.Templates) != 0 { + for _, template := range comboManifest.CloudProvider.Templates { + templateName := template.Name + templateList = append(templateList, ReleaseJobRef{Name: templateName, Release: template.Release}) + } + + installationManifest.Templates = templateList + } else { + templateList = append(templateList, ReleaseJobRef{ + Name: comboManifest.CloudProvider.Template.Name, + Release: comboManifest.CloudProvider.Template.Release, + }) + installationManifest.Templates = templateList + } + + // FIXME: Do we need to deduplicate the templateList? It is illegal to have multiple + // entries with the same Name, but not illegal to have multiple entries with + // different Names but the same Release. + properties, err := biproperty.BuildMap(comboManifest.CloudProvider.Properties) if err != nil { return Manifest{}, bosherr.WrapErrorf(err, "Parsing cloud_provider manifest properties: %#v", comboManifest.CloudProvider.Properties) diff --git a/installation/manifest/parser_test.go b/installation/manifest/parser_test.go index f722e4f95..4e7a0b47c 100644 --- a/installation/manifest/parser_test.go +++ b/installation/manifest/parser_test.go @@ -47,9 +47,9 @@ var _ = Describe("Parser", func() { --- name: fake-deployment-name cloud_provider: - template: - name: fake-cpi-job-name - release: fake-cpi-release-name + templates: + - name: fake-cpi-job-name + release: fake-cpi-release-name mbus: http://fake-mbus-user:fake-mbus-password@0.0.0.0:6868 properties: fake-property-name: @@ -59,9 +59,9 @@ cloud_provider: --- name: fake-deployment-name cloud_provider: - template: - name: fake-cpi-job-name - release: fake-cpi-release-name + templates: + - name: fake-cpi-job-name + release: fake-cpi-release-name ssh_tunnel: host: 54.34.56.8 port: 22 @@ -105,9 +105,8 @@ cloud_provider: Expect(installationManifest).To(Equal(manifest.Manifest{ Name: "fake-deployment-name", - Template: manifest.ReleaseJobRef{ - Name: "fake-cpi-job-name", - Release: "fake-cpi-release-name", + Templates: []manifest.ReleaseJobRef{ + {Name: "fake-cpi-job-name", Release: "fake-cpi-release-name"}, }, Properties: biproperty.Map{ "fake-property-name": biproperty.Map{ @@ -127,9 +126,9 @@ cloud_provider: --- name: fake-deployment-name cloud_provider: - template: - name: fake-cpi-job-name - release: fake-cpi-release-name + templates: + - name: fake-cpi-job-name + release: fake-cpi-release-name ssh_tunnel: host: 54.34.56.8 port: 22 @@ -159,9 +158,8 @@ cloud_provider: Expect(installationManifest).To(Equal(manifest.Manifest{ Name: "fake-deployment-name", - Template: manifest.ReleaseJobRef{ - Name: "fake-cpi-job-name", - Release: "fake-cpi-release-name", + Templates: []manifest.ReleaseJobRef{ + {Name: "fake-cpi-job-name", Release: "fake-cpi-release-name"}, }, Properties: biproperty.Map{}, Mbus: "http://fake-mbus-user:fake-mbus-password@0.0.0.0:6868", @@ -175,9 +173,9 @@ cloud_provider: --- name: fake-deployment-name cloud_provider: - template: - name: fake-cpi-job-name - release: fake-cpi-release-name + templates: + - name: fake-cpi-job-name + release: fake-cpi-release-name ssh_tunnel: host: 54.34.56.8 port: 22 @@ -207,9 +205,9 @@ cloud_provider: --- name: fake-deployment-name cloud_provider: - template: - name: fake-cpi-job-name - release: fake-cpi-release-name + templates: + - name: fake-cpi-job-name + release: fake-cpi-release-name ssh_tunnel: host: 54.34.56.8 port: 22 @@ -242,9 +240,8 @@ cloud_provider: Expect(installationManifest).To(Equal(manifest.Manifest{ Name: "fake-deployment-name", - Template: manifest.ReleaseJobRef{ - Name: "fake-cpi-job-name", - Release: "fake-cpi-release-name", + Templates: []manifest.ReleaseJobRef{ + {Name: "fake-cpi-job-name", Release: "fake-cpi-release-name"}, }, Properties: biproperty.Map{}, Mbus: "http://fake-mbus-user:fake-mbus-password@0.0.0.0:6868", @@ -258,9 +255,9 @@ cloud_provider: --- name: fake-deployment-name cloud_provider: - template: - name: fake-cpi-job-name - release: fake-cpi-release-name + templates: + - name: fake-cpi-job-name + release: fake-cpi-release-name ssh_tunnel: host: 54.34.56.8 port: 22 @@ -290,9 +287,9 @@ cloud_provider: --- name: fake-deployment-name cloud_provider: - template: - name: fake-cpi-job-name - release: fake-cpi-release-name + templates: + - name: fake-cpi-job-name + release: fake-cpi-release-name ssh_tunnel: host: 54.34.56.8 port: 22 @@ -331,9 +328,9 @@ cloud_provider: --- name: fake-deployment-name cloud_provider: - template: - name: fake-cpi-job-name - release: fake-cpi-release-name + templates: + - name: fake-cpi-job-name + release: fake-cpi-release-name ssh_tunnel: host: 54.34.56.8 port: 22 @@ -354,9 +351,9 @@ cloud_provider: err := fakeFs.WriteFileString(comboManifestPath, `--- name: fake-deployment-name cloud_provider: - template: - name: fake-cpi-job-name - release: fake-cpi-release-name + templates: + - name: fake-cpi-job-name + release: fake-cpi-release-name ssh_tunnel: host: 54.34.56.8 port: 22 @@ -376,9 +373,9 @@ cloud_provider: err := fakeFs.WriteFileString(comboManifestPath, `--- name: fake-deployment-name cloud_provider: - template: - name: fake-cpi-job-name - release: fake-cpi-release-name + templates: + - name: fake-cpi-job-name + release: fake-cpi-release-name ssh_tunnel: host: 54.34.56.8 port: 22 @@ -401,9 +398,9 @@ cloud_provider: --- name: fake-deployment-name cloud_provider: - template: - name: fake-cpi-job-name - release: fake-cpi-release-name + templates: + - name: fake-cpi-job-name + release: fake-cpi-release-name ssh_tunnel: host: 54.34.56.8 port: 22 @@ -427,9 +424,9 @@ cloud_provider: err := fakeFs.WriteFileString(comboManifestPath, `--- name: fake-deployment-name cloud_provider: - template: - name: fake-cpi-job-name - release: fake-cpi-release-name + templates: + - name: fake-cpi-job-name + release: fake-cpi-release-name ssh_tunnel: host: 54.34.56.8 port: 22 @@ -470,9 +467,9 @@ cloud_provider: err := fakeFs.WriteFileString(comboManifestPath, `--- name: fake-deployment-name cloud_provider: - template: - name: fake-cpi-job-name - release: fake-cpi-release-name + templates: + - name: fake-cpi-job-name + release: fake-cpi-release-name ssh_tunnel: host: 54.34.56.8 port: 22 @@ -496,9 +493,8 @@ cloud_provider: Expect(installationManifest).To(Equal(manifest.Manifest{ Name: "replaced-name", - Template: manifest.ReleaseJobRef{ - Name: "fake-cpi-job-name", - Release: "fake-cpi-release-name", + Templates: []manifest.ReleaseJobRef{ + {Name: "fake-cpi-job-name", Release: "fake-cpi-release-name"}, }, Properties: biproperty.Map{}, Mbus: "http://fake-mbus-user:fake-mbus-password@0.0.0.0:6868", @@ -520,9 +516,9 @@ cloud_provider: --- name: fake-deployment-name cloud_provider: - template: - name: fake-cpi-job-name - release: fake-cpi-release-name + templates: + - name: fake-cpi-job-name + release: fake-cpi-release-name mbus: http://fake-mbus-user:fake-mbus-password@0.0.0.0:6868 cert: ca: | @@ -581,9 +577,9 @@ nH9ttalAwSLBsobVaK8mmiAdtAdx+CmHWrB4UNxCPYasrt5A6a9A9SiQ2dLd --- name: fake-deployment-name cloud_provider: - template: - name: fake-cpi-job-name - release: fake-cpi-release-name + templates: + - name: fake-cpi-job-name + release: fake-cpi-release-name mbus: http://fake-mbus-user:fake-mbus-password@0.0.0.0:6868 cert: ca: | @@ -617,5 +613,80 @@ cloud_provider: }) }) }) + + Context("when multiple releases are specified in templates", func() { + BeforeEach(func() { + err := fakeFs.WriteFileString(comboManifestPath, ` +--- +name: fake-deployment-name +cloud_provider: + templates: + - name: fake-cpi-job-name + release: fake-cpi-release-name + - name: fake-cpi-plugin-job-name + release: fake-cpi-release-name + mbus: http://fake-mbus-user:fake-mbus-password@0.0.0.0:6868 + properties: + fake-property-name: + nested-property: fake-property-value +`) + Expect(err).ToNot(HaveOccurred()) + }) + + It("parses installation from combo manifest", func() { + installationManifest, err := parser.Parse(comboManifestPath, boshtpl.StaticVariables{}, patch.Ops{}, releaseSetManifest) + Expect(err).ToNot(HaveOccurred()) + + Expect(installationManifest).To(Equal(manifest.Manifest{ + Name: "fake-deployment-name", + Templates: []manifest.ReleaseJobRef{ + {Name: "fake-cpi-job-name", Release: "fake-cpi-release-name"}, + {Name: "fake-cpi-plugin-job-name", Release: "fake-cpi-release-name"}, + }, + Properties: biproperty.Map{ + "fake-property-name": biproperty.Map{ + "nested-property": "fake-property-value", + }, + }, + Mbus: "http://fake-mbus-user:fake-mbus-password@0.0.0.0:6868", + })) + }) + }) + + Context("when the deprecated template key is specified instead of the templates key", func() { + BeforeEach(func() { + err := fakeFs.WriteFileString(comboManifestPath, ` +--- +name: fake-deployment-name +cloud_provider: + template: + name: fake-cpi-job-name + release: fake-cpi-release-name + mbus: http://fake-mbus-user:fake-mbus-password@0.0.0.0:6868 + properties: + fake-property-name: + nested-property: fake-property-value +`) + Expect(err).ToNot(HaveOccurred()) + }) + + It("parses installation from combo manifest", func() { + installationManifest, err := parser.Parse(comboManifestPath, boshtpl.StaticVariables{}, patch.Ops{}, releaseSetManifest) + Expect(err).ToNot(HaveOccurred()) + + Expect(installationManifest).To(Equal(manifest.Manifest{ + Name: "fake-deployment-name", + Templates: []manifest.ReleaseJobRef{ + {Name: "fake-cpi-job-name", Release: "fake-cpi-release-name"}, + }, + Properties: biproperty.Map{ + "fake-property-name": biproperty.Map{ + "nested-property": "fake-property-value", + }, + }, + Mbus: "http://fake-mbus-user:fake-mbus-password@0.0.0.0:6868", + })) + }) + }) }) }) diff --git a/installation/manifest/validator.go b/installation/manifest/validator.go index d58839dfc..b287debc7 100644 --- a/installation/manifest/validator.go +++ b/installation/manifest/validator.go @@ -1,6 +1,7 @@ package manifest import ( + "fmt" "strings" bosherr "github.com/cloudfoundry/bosh-utils/errors" @@ -24,21 +25,16 @@ func NewValidator(logger boshlog.Logger) Validator { } func (v *validator) Validate(manifest Manifest, releaseSetManifest birelsetmanifest.Manifest) error { - errs := []error{} + var errs []error - cpiJobName := manifest.Template.Name - if v.isBlank(cpiJobName) { - errs = append(errs, bosherr.Error("cloud_provider.template.name must be provided")) - } - - cpiReleaseName := manifest.Template.Release - if v.isBlank(cpiReleaseName) { - errs = append(errs, bosherr.Error("cloud_provider.template.release must be provided")) + // When there is nothing in templates, return an error. It should have a CPI release. + if len(manifest.Templates) == 0 { + return fmt.Errorf("manifest.Templates cannot be empty and must contain one release") } - _, found := releaseSetManifest.FindByName(cpiReleaseName) - if !found { - errs = append(errs, bosherr.Errorf("cloud_provider.template.release '%s' must refer to a release in releases", cpiReleaseName)) + for _, template := range manifest.Templates { + errRet := v.validateReleaseJobRef(template, releaseSetManifest) + errs = append(errs, errRet...) } if len(errs) > 0 { @@ -48,6 +44,25 @@ func (v *validator) Validate(manifest Manifest, releaseSetManifest birelsetmanif return nil } +func (v *validator) validateReleaseJobRef(releaseJobRef ReleaseJobRef, releaseSetManifest birelsetmanifest.Manifest) []error { + var errs []error + jobName := releaseJobRef.Name + if v.isBlank(jobName) { + errs = append(errs, bosherr.Error("cloud_provider.template.name must be provided")) + } + + releaseName := releaseJobRef.Release + if v.isBlank(releaseName) { + errs = append(errs, bosherr.Error("cloud_provider.template.release must be provided")) + } + + _, found := releaseSetManifest.FindByName(releaseName) + if !found { + errs = append(errs, bosherr.Errorf("cloud_provider.template.release '%s' must refer to a release in releases", releaseName)) + } + return errs +} + func (v *validator) isBlank(str string) bool { return str == "" || strings.TrimSpace(str) == "" } diff --git a/installation/manifest/validator_test.go b/installation/manifest/validator_test.go index dae917ec5..69757e737 100644 --- a/installation/manifest/validator_test.go +++ b/installation/manifest/validator_test.go @@ -30,9 +30,8 @@ var _ = Describe("Validator", func() { validManifest = Manifest{ Name: "fake-installation-name", - Template: ReleaseJobRef{ - Name: "cpi", - Release: "provided-valid-release-name", + Templates: []ReleaseJobRef{ + {Name: "cpi", Release: "provided-valid-release-name"}, }, Properties: biproperty.Map{ "fake-prop-key": "fake-prop-value", @@ -57,9 +56,21 @@ var _ = Describe("Validator", func() { Expect(err).ToNot(HaveOccurred()) }) - It("validates template must be fully specified", func() { + It("errors when validating an empty manifest", func() { manifest := Manifest{} + err := validator.Validate(manifest, releaseSetManifest) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("manifest.Templates cannot be empty and must contain one release")) + }) + + It("validates template must be fully specified", func() { + manifest := Manifest{ + Templates: []ReleaseJobRef{ + {Name: "", Release: ""}, + }, + } + err := validator.Validate(manifest, releaseSetManifest) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("cloud_provider.template.name must be provided")) @@ -68,8 +79,8 @@ var _ = Describe("Validator", func() { It("validates template.name is not blank", func() { manifest := Manifest{ - Template: ReleaseJobRef{ - Name: " ", + Templates: []ReleaseJobRef{ + {Name: " "}, }, } @@ -80,8 +91,8 @@ var _ = Describe("Validator", func() { It("validates template.release is not blank", func() { manifest := Manifest{ - Template: ReleaseJobRef{ - Release: " ", + Templates: []ReleaseJobRef{ + {Release: " "}, }, } @@ -92,8 +103,8 @@ var _ = Describe("Validator", func() { It("validates the release is available", func() { manifest := Manifest{ - Template: ReleaseJobRef{ - Release: "not-provided-valid-release-name", + Templates: []ReleaseJobRef{ + {Release: "not-provided-valid-release-name"}, }, } @@ -101,5 +112,12 @@ var _ = Describe("Validator", func() { Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("cloud_provider.template.release 'not-provided-valid-release-name' must refer to a release in releases")) }) + + It("validates the release successfully when multiple valid templates are specified", func() { + validManifest.Templates = append(validManifest.Templates, ReleaseJobRef{Name: "plugin", Release: "provided-valid-release-name"}) + + err := validator.Validate(validManifest, releaseSetManifest) + Expect(err).ToNot(HaveOccurred()) + }) }) }) diff --git a/integration/create_env_test.go b/integration/create_env_test.go index aeca93f2d..3433d7f55 100644 --- a/integration/create_env_test.go +++ b/integration/create_env_test.go @@ -291,9 +291,8 @@ cloud_provider: installationManifest := biinstallmanifest.Manifest{ Name: "test-deployment", - Template: biinstallmanifest.ReleaseJobRef{ - Name: "fake-cpi-release-job-name", - Release: "fake-cpi-release-name", + Templates: []biinstallmanifest.ReleaseJobRef{ + {Name: "fake-cpi-release-job-name", Release: "fake-cpi-release-name"}, }, Mbus: mbusURL, Cert: biinstallmanifest.Certificate{ @@ -308,7 +307,8 @@ cloud_provider: installedJob.Name = "fake-cpi-release-job-name" installedJob.Path = filepath.Join(target.JobsPath(), "fake-cpi-release-job-name") - installation := biinstall.NewInstallation(target, installedJob, installationManifest) + installation := biinstall.NewInstallation(target, []biinstall.InstalledJob{installedJob}, + installationManifest) mockInstallerFactory.EXPECT().NewInstaller(target).Return(mockInstaller).AnyTimes() @@ -425,7 +425,6 @@ cloud_provider: cpiInstaller := bicpirel.CpiInstaller{ ReleaseManager: releaseManager, InstallerFactory: mockInstallerFactory, - Validator: bicpirel.NewValidator(), } releaseFetcher := biinstall.NewReleaseFetcher(tarballProvider, releaseReader, releaseManager) stemcellFetcher := bistemcell.Fetcher{