From 49a28f864769faaad253990778824a25c5a50401 Mon Sep 17 00:00:00 2001 From: Danny Berger Date: Fri, 11 Nov 2016 11:14:28 -0800 Subject: [PATCH] Implement `cpi-config` and `update-cpi-config` commands [#132264755](https://www.pivotaltracker.com/story/show/132264755) Signed-off-by: Ming Xiao --- cmd/cmd.go | 6 ++ cmd/cpi_config.go | 26 ++++++ cmd/cpi_config_test.go | 52 +++++++++++ cmd/deployments_table.go | 4 +- cmd/opts.go | 19 +++++ cmd/opts_test.go | 16 ++++ cmd/update_cpi_config.go | 34 ++++++++ cmd/update_cpi_config_test.go | 103 ++++++++++++++++++++++ director/cpi_configs.go | 54 ++++++++++++ director/cpi_configs_test.go | 109 ++++++++++++++++++++++++ director/directorfakes/fake_director.go | 83 ++++++++++++++++++ director/interfaces.go | 3 + 12 files changed, 507 insertions(+), 2 deletions(-) create mode 100644 cmd/cpi_config.go create mode 100644 cmd/cpi_config_test.go create mode 100644 cmd/update_cpi_config.go create mode 100644 cmd/update_cpi_config_test.go create mode 100644 director/cpi_configs.go create mode 100644 director/cpi_configs_test.go diff --git a/cmd/cmd.go b/cmd/cmd.go index 47995d91b..919e31cd9 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -207,6 +207,12 @@ func (c Cmd) Execute() (cmdErr error) { case *UpdateCloudConfigOpts: return NewUpdateCloudConfigCmd(deps.UI, c.director()).Run(*opts) + case *CPIConfigOpts: + return NewCPIConfigCmd(deps.UI, c.director()).Run() + + case *UpdateCPIConfigOpts: + return NewUpdateCPIConfigCmd(deps.UI, c.director()).Run(*opts) + case *RuntimeConfigOpts: return NewRuntimeConfigCmd(deps.UI, c.director()).Run() diff --git a/cmd/cpi_config.go b/cmd/cpi_config.go new file mode 100644 index 000000000..2b906cfdf --- /dev/null +++ b/cmd/cpi_config.go @@ -0,0 +1,26 @@ +package cmd + +import ( + boshdir "github.com/cloudfoundry/bosh-cli/director" + boshui "github.com/cloudfoundry/bosh-cli/ui" +) + +type CPIConfigCmd struct { + ui boshui.UI + director boshdir.Director +} + +func NewCPIConfigCmd(ui boshui.UI, director boshdir.Director) CPIConfigCmd { + return CPIConfigCmd{ui: ui, director: director} +} + +func (c CPIConfigCmd) Run() error { + cpiConfig, err := c.director.LatestCPIConfig() + if err != nil { + return err + } + + c.ui.PrintBlock(cpiConfig.Properties) + + return nil +} diff --git a/cmd/cpi_config_test.go b/cmd/cpi_config_test.go new file mode 100644 index 000000000..beea2e239 --- /dev/null +++ b/cmd/cpi_config_test.go @@ -0,0 +1,52 @@ +package cmd_test + +import ( + "errors" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + . "github.com/cloudfoundry/bosh-cli/cmd" + boshdir "github.com/cloudfoundry/bosh-cli/director" + fakedir "github.com/cloudfoundry/bosh-cli/director/directorfakes" + fakeui "github.com/cloudfoundry/bosh-cli/ui/fakes" +) + +var _ = Describe("CPIConfigCmd", func() { + var ( + ui *fakeui.FakeUI + director *fakedir.FakeDirector + command CPIConfigCmd + ) + + BeforeEach(func() { + ui = &fakeui.FakeUI{} + director = &fakedir.FakeDirector{} + command = NewCPIConfigCmd(ui, director) + }) + + Describe("Run", func() { + act := func() error { return command.Run() } + + It("shows cpi config", func() { + cpiConfig := boshdir.CPIConfig{ + Properties: "some-properties", + } + + director.LatestCPIConfigReturns(cpiConfig, nil) + + err := act() + Expect(err).ToNot(HaveOccurred()) + + Expect(ui.Blocks).To(Equal([]string{"some-properties"})) + }) + + It("returns error if cpi config cannot be retrieved", func() { + director.LatestCPIConfigReturns(boshdir.CPIConfig{}, errors.New("fake-err")) + + err := act() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("fake-err")) + }) + }) +}) diff --git a/cmd/deployments_table.go b/cmd/deployments_table.go index d9fee289e..7449d259b 100644 --- a/cmd/deployments_table.go +++ b/cmd/deployments_table.go @@ -33,7 +33,7 @@ func (t DeploymentsTable) Print() error { return err } - config, err := d.CloudConfig() + cloud_config, err := d.CloudConfig() if err != nil { return err } @@ -42,7 +42,7 @@ func (t DeploymentsTable) Print() error { boshtbl.NewValueString(d.Name()), boshtbl.NewValueStrings(t.takeReleases(releases)), boshtbl.NewValueStrings(t.takeStemcells(stemcells)), - boshtbl.NewValueString(config), + boshtbl.NewValueString(cloud_config), }) } diff --git a/cmd/opts.go b/cmd/opts.go index 908d22e71..16ea92d3a 100644 --- a/cmd/opts.go +++ b/cmd/opts.go @@ -63,6 +63,10 @@ type BoshOpts struct { CloudConfig CloudConfigOpts `command:"cloud-config" alias:"cc" description:"Show current cloud config"` UpdateCloudConfig UpdateCloudConfigOpts `command:"update-cloud-config" alias:"ucc" description:"Update current cloud config"` + // CPI Config + CPIConfig CPIConfigOpts `command:"cpi-config" description:"Show current CPI config"` + UpdateCPIConfig UpdateCPIConfigOpts `command:"update-cpi-config" description:"Update current CPI config"` + // Runtime config RuntimeConfig RuntimeConfigOpts `command:"runtime-config" alias:"rc" description:"Show current runtime config"` UpdateRuntimeConfig UpdateRuntimeConfigOpts `command:"update-runtime-config" alias:"urc" description:"Update current runtime config"` @@ -294,6 +298,21 @@ type UpdateCloudConfigArgs struct { CloudConfig FileBytesArg `positional-arg-name:"PATH" description:"Path to a cloud config file"` } +type CPIConfigOpts struct { + cmd +} + +type UpdateCPIConfigOpts struct { + Args UpdateCPIConfigArgs `positional-args:"true" required:"true"` + VarFlags + OpsFlags + cmd +} + +type UpdateCPIConfigArgs struct { + CPIConfig FileBytesArg `positional-arg-name:"PATH" description:"Path to a CPI config file"` +} + // Runtime config type RuntimeConfigOpts struct { cmd diff --git a/cmd/opts_test.go b/cmd/opts_test.go index 48a6307e0..bf655cb64 100644 --- a/cmd/opts_test.go +++ b/cmd/opts_test.go @@ -312,6 +312,22 @@ var _ = Describe("Opts", func() { }) }) + Describe("CPIConfig", func() { + It("contains desired values", func() { + Expect(getStructTagForName("CPIConfig", opts)).To(Equal( + `command:"cpi-config" description:"Show current CPI config"`, + )) + }) + }) + + Describe("UpdateCPIConfig", func() { + It("contains desired values", func() { + Expect(getStructTagForName("UpdateCPIConfig", opts)).To(Equal( + `command:"update-cpi-config" description:"Update current CPI config"`, + )) + }) + }) + Describe("RuntimeConfig", func() { It("contains desired values", func() { Expect(getStructTagForName("RuntimeConfig", opts)).To(Equal( diff --git a/cmd/update_cpi_config.go b/cmd/update_cpi_config.go new file mode 100644 index 000000000..9d841f810 --- /dev/null +++ b/cmd/update_cpi_config.go @@ -0,0 +1,34 @@ +package cmd + +import ( + bosherr "github.com/cloudfoundry/bosh-utils/errors" + + boshdir "github.com/cloudfoundry/bosh-cli/director" + boshtpl "github.com/cloudfoundry/bosh-cli/director/template" + boshui "github.com/cloudfoundry/bosh-cli/ui" +) + +type UpdateCPIConfigCmd struct { + ui boshui.UI + director boshdir.Director +} + +func NewUpdateCPIConfigCmd(ui boshui.UI, director boshdir.Director) UpdateCPIConfigCmd { + return UpdateCPIConfigCmd{ui: ui, director: director} +} + +func (c UpdateCPIConfigCmd) Run(opts UpdateCPIConfigOpts) error { + tpl := boshtpl.NewTemplate(opts.Args.CPIConfig.Bytes) + + bytes, err := tpl.Evaluate(opts.VarFlags.AsVariables(), opts.OpsFlags.AsOps(), boshtpl.EvaluateOpts{}) + if err != nil { + return bosherr.WrapErrorf(err, "Evaluating cpi config") + } + + err = c.ui.AskForConfirmation() + if err != nil { + return err + } + + return c.director.UpdateCPIConfig(bytes) +} diff --git a/cmd/update_cpi_config_test.go b/cmd/update_cpi_config_test.go new file mode 100644 index 000000000..845b69c4c --- /dev/null +++ b/cmd/update_cpi_config_test.go @@ -0,0 +1,103 @@ +package cmd_test + +import ( + "errors" + + "github.com/cppforlife/go-patch/patch" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + . "github.com/cloudfoundry/bosh-cli/cmd" + fakedir "github.com/cloudfoundry/bosh-cli/director/directorfakes" + boshtpl "github.com/cloudfoundry/bosh-cli/director/template" + fakeui "github.com/cloudfoundry/bosh-cli/ui/fakes" +) + +var _ = Describe("UpdateCPIConfigCmd", func() { + var ( + ui *fakeui.FakeUI + director *fakedir.FakeDirector + command UpdateCPIConfigCmd + ) + + BeforeEach(func() { + ui = &fakeui.FakeUI{} + director = &fakedir.FakeDirector{} + command = NewUpdateCPIConfigCmd(ui, director) + }) + + Describe("Run", func() { + var ( + opts UpdateCPIConfigOpts + ) + + BeforeEach(func() { + opts = UpdateCPIConfigOpts{ + Args: UpdateCPIConfigArgs{ + CPIConfig: FileBytesArg{Bytes: []byte("cpi-config")}, + }, + } + }) + + act := func() error { return command.Run(opts) } + + It("updates cpi config", func() { + err := act() + Expect(err).ToNot(HaveOccurred()) + + Expect(director.UpdateCPIConfigCallCount()).To(Equal(1)) + + bytes := director.UpdateCPIConfigArgsForCall(0) + Expect(bytes).To(Equal([]byte("cpi-config\n"))) + }) + + It("updates templated cpi config", func() { + opts.Args.CPIConfig = FileBytesArg{ + Bytes: []byte("name: ((name))\ntype: ((type))"), + } + + opts.VarKVs = []boshtpl.VarKV{ + {Name: "name", Value: "val1-from-kv"}, + } + + opts.VarsFiles = []boshtpl.VarsFileArg{ + {Vars: boshtpl.Variables(map[string]interface{}{"name": "val1-from-file"})}, + {Vars: boshtpl.Variables(map[string]interface{}{"type": "val2-from-file"})}, + } + + opts.OpsFiles = []OpsFileArg{ + { + Ops: patch.Ops([]patch.Op{ + patch.ReplaceOp{Path: patch.MustNewPointerFromString("/xyz?"), Value: "val"}, + }), + }, + } + + err := act() + Expect(err).ToNot(HaveOccurred()) + + Expect(director.UpdateCPIConfigCallCount()).To(Equal(1)) + + bytes := director.UpdateCPIConfigArgsForCall(0) + Expect(bytes).To(Equal([]byte("name: val1-from-kv\ntype: val2-from-file\nxyz: val\n"))) + }) + + It("does not stop if confirmation is rejected", func() { + ui.AskedConfirmationErr = errors.New("stop") + + err := act() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("stop")) + + Expect(director.UpdateCPIConfigCallCount()).To(Equal(0)) + }) + + It("returns error if updating failed", func() { + director.UpdateCPIConfigReturns(errors.New("fake-err")) + + err := act() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("fake-err")) + }) + }) +}) diff --git a/director/cpi_configs.go b/director/cpi_configs.go new file mode 100644 index 000000000..afbbcef9f --- /dev/null +++ b/director/cpi_configs.go @@ -0,0 +1,54 @@ +package director + +import ( + "net/http" + + bosherr "github.com/cloudfoundry/bosh-utils/errors" +) + +type CPIConfig struct { + Properties string +} + +func (d DirectorImpl) LatestCPIConfig() (CPIConfig, error) { + resps, err := d.client.CPIConfigs() + if err != nil { + return CPIConfig{}, err + } + + if len(resps) == 0 { + return CPIConfig{}, bosherr.Error("No CPI config") + } + + return resps[0], nil +} + +func (d DirectorImpl) UpdateCPIConfig(manifest []byte) error { + return d.client.UpdateCPIConfig(manifest) +} + +func (c Client) CPIConfigs() ([]CPIConfig, error) { + var resps []CPIConfig + + err := c.clientRequest.Get("/cpi_configs?limit=1", &resps) + if err != nil { + return resps, bosherr.WrapErrorf(err, "Finding CPI configs") + } + + return resps, nil +} + +func (c Client) UpdateCPIConfig(manifest []byte) error { + path := "/cpi_configs" + + setHeaders := func(req *http.Request) { + req.Header.Add("Content-Type", "text/yaml") + } + + _, _, err := c.clientRequest.RawPost(path, manifest, setHeaders) + if err != nil { + return bosherr.WrapErrorf(err, "Updating CPI config") + } + + return nil +} diff --git a/director/cpi_configs_test.go b/director/cpi_configs_test.go new file mode 100644 index 000000000..a577ecc8e --- /dev/null +++ b/director/cpi_configs_test.go @@ -0,0 +1,109 @@ +package director_test + +import ( + "net/http" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/ghttp" + + . "github.com/cloudfoundry/bosh-cli/director" +) + +var _ = Describe("Director", func() { + var ( + director Director + server *ghttp.Server + ) + + BeforeEach(func() { + director, server = BuildServer() + }) + + AfterEach(func() { + server.Close() + }) + + Describe("LatestCPIConfig", func() { + It("returns latest cpi config if there is at least one", func() { + server.AppendHandlers( + ghttp.CombineHandlers( + ghttp.VerifyRequest("GET", "/cpi_configs", "limit=1"), + ghttp.VerifyBasicAuth("username", "password"), + ghttp.RespondWith(http.StatusOK, `[ + {"properties": "first"}, + {"properties": "second"} +]`), + ), + ) + + cc, err := director.LatestCPIConfig() + Expect(err).ToNot(HaveOccurred()) + Expect(cc).To(Equal(CPIConfig{Properties: "first"})) + }) + + It("returns error if there is no cpi config", func() { + server.AppendHandlers( + ghttp.CombineHandlers( + ghttp.VerifyRequest("GET", "/cpi_configs", "limit=1"), + ghttp.VerifyBasicAuth("username", "password"), + ghttp.RespondWith(http.StatusOK, `[]`), + ), + ) + + _, err := director.LatestCPIConfig() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("No CPI config")) + }) + + It("returns error if info response in non-200", func() { + AppendBadRequest(ghttp.VerifyRequest("GET", "/cpi_configs"), server) + + _, err := director.LatestCPIConfig() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring( + "Finding CPI configs: Director responded with non-successful status code")) + }) + + It("returns error if info cannot be unmarshalled", func() { + server.AppendHandlers( + ghttp.CombineHandlers( + ghttp.VerifyRequest("GET", "/cpi_configs"), + ghttp.RespondWith(http.StatusOK, ``), + ), + ) + + _, err := director.LatestCPIConfig() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring( + "Finding CPI configs: Unmarshaling Director response")) + }) + }) + + Describe("UpdateCPIConfig", func() { + It("updates cpi config", func() { + server.AppendHandlers( + ghttp.CombineHandlers( + ghttp.VerifyRequest("POST", "/cpi_configs"), + ghttp.VerifyBasicAuth("username", "password"), + ghttp.VerifyHeader(http.Header{ + "Content-Type": []string{"text/yaml"}, + }), + ghttp.RespondWith(http.StatusOK, `{}`), + ), + ) + + err := director.UpdateCPIConfig([]byte("config")) + Expect(err).ToNot(HaveOccurred()) + }) + + It("returns error if info response in non-200", func() { + AppendBadRequest(ghttp.VerifyRequest("POST", "/cpi_configs"), server) + + err := director.UpdateCPIConfig(nil) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring( + "Updating CPI config: Director responded with non-successful status code")) + }) + }) +}) diff --git a/director/directorfakes/fake_director.go b/director/directorfakes/fake_director.go index cd544697b..7f7f27a09 100644 --- a/director/directorfakes/fake_director.go +++ b/director/directorfakes/fake_director.go @@ -209,6 +209,21 @@ type FakeDirector struct { updateCloudConfigReturns struct { result1 error } + LatestCPIConfigStub func() (director.CPIConfig, error) + latestCPIConfigMutex sync.RWMutex + latestCPIConfigArgsForCall []struct{} + latestCPIConfigReturns struct { + result1 director.CPIConfig + result2 error + } + UpdateCPIConfigStub func([]byte) error + updateCPIConfigMutex sync.RWMutex + updateCPIConfigArgsForCall []struct { + arg1 []byte + } + updateCPIConfigReturns struct { + result1 error + } LatestRuntimeConfigStub func() (director.RuntimeConfig, error) latestRuntimeConfigMutex sync.RWMutex latestRuntimeConfigArgsForCall []struct{} @@ -1007,6 +1022,70 @@ func (fake *FakeDirector) UpdateCloudConfigReturns(result1 error) { }{result1} } +func (fake *FakeDirector) LatestCPIConfig() (director.CPIConfig, error) { + fake.latestCPIConfigMutex.Lock() + fake.latestCPIConfigArgsForCall = append(fake.latestCPIConfigArgsForCall, struct{}{}) + fake.recordInvocation("LatestCPIConfig", []interface{}{}) + fake.latestCPIConfigMutex.Unlock() + if fake.LatestCPIConfigStub != nil { + return fake.LatestCPIConfigStub() + } else { + return fake.latestCPIConfigReturns.result1, fake.latestCPIConfigReturns.result2 + } +} + +func (fake *FakeDirector) LatestCPIConfigCallCount() int { + fake.latestCPIConfigMutex.RLock() + defer fake.latestCPIConfigMutex.RUnlock() + return len(fake.latestCPIConfigArgsForCall) +} + +func (fake *FakeDirector) LatestCPIConfigReturns(result1 director.CPIConfig, result2 error) { + fake.LatestCPIConfigStub = nil + fake.latestCPIConfigReturns = struct { + result1 director.CPIConfig + result2 error + }{result1, result2} +} + +func (fake *FakeDirector) UpdateCPIConfig(arg1 []byte) error { + var arg1Copy []byte + if arg1 != nil { + arg1Copy = make([]byte, len(arg1)) + copy(arg1Copy, arg1) + } + fake.updateCPIConfigMutex.Lock() + fake.updateCPIConfigArgsForCall = append(fake.updateCPIConfigArgsForCall, struct { + arg1 []byte + }{arg1Copy}) + fake.recordInvocation("UpdateCPIConfig", []interface{}{arg1Copy}) + fake.updateCPIConfigMutex.Unlock() + if fake.UpdateCPIConfigStub != nil { + return fake.UpdateCPIConfigStub(arg1) + } else { + return fake.updateCPIConfigReturns.result1 + } +} + +func (fake *FakeDirector) UpdateCPIConfigCallCount() int { + fake.updateCPIConfigMutex.RLock() + defer fake.updateCPIConfigMutex.RUnlock() + return len(fake.updateCPIConfigArgsForCall) +} + +func (fake *FakeDirector) UpdateCPIConfigArgsForCall(i int) []byte { + fake.updateCPIConfigMutex.RLock() + defer fake.updateCPIConfigMutex.RUnlock() + return fake.updateCPIConfigArgsForCall[i].arg1 +} + +func (fake *FakeDirector) UpdateCPIConfigReturns(result1 error) { + fake.UpdateCPIConfigStub = nil + fake.updateCPIConfigReturns = struct { + result1 error + }{result1} +} + func (fake *FakeDirector) LatestRuntimeConfig() (director.RuntimeConfig, error) { fake.latestRuntimeConfigMutex.Lock() fake.latestRuntimeConfigArgsForCall = append(fake.latestRuntimeConfigArgsForCall, struct{}{}) @@ -1280,6 +1359,10 @@ func (fake *FakeDirector) Invocations() map[string][][]interface{} { defer fake.latestCloudConfigMutex.RUnlock() fake.updateCloudConfigMutex.RLock() defer fake.updateCloudConfigMutex.RUnlock() + fake.latestCPIConfigMutex.RLock() + defer fake.latestCPIConfigMutex.RUnlock() + fake.updateCPIConfigMutex.RLock() + defer fake.updateCPIConfigMutex.RUnlock() fake.latestRuntimeConfigMutex.RLock() defer fake.latestRuntimeConfigMutex.RUnlock() fake.updateRuntimeConfigMutex.RLock() diff --git a/director/interfaces.go b/director/interfaces.go index 4d7a0b9af..274e5d2b6 100644 --- a/director/interfaces.go +++ b/director/interfaces.go @@ -42,6 +42,9 @@ type Director interface { LatestCloudConfig() (CloudConfig, error) UpdateCloudConfig([]byte) error + LatestCPIConfig() (CPIConfig, error) + UpdateCPIConfig([]byte) error + LatestRuntimeConfig() (RuntimeConfig, error) UpdateRuntimeConfig([]byte) error