From 75b63a5b780da9cd0bedeb9a32ca04d616dcec3d Mon Sep 17 00:00:00 2001 From: Rahul Bhardwaj Date: Tue, 16 Apr 2024 21:23:30 +0530 Subject: [PATCH] Add support for database events in Doctl (#1524) * Add support for database events in Doctl * Add integration test list database events --------- Co-authored-by: Rahul Bhardwaj --- commands/databases.go | 38 ++++++++++ commands/databases_test.go | 1 + commands/displayers/database.go | 44 ++++++++++++ do/databases.go | 38 ++++++++++ do/mocks/DatabasesService.go | 15 ++++ integration/database_events_list_test.go | 92 ++++++++++++++++++++++++ 6 files changed, 228 insertions(+) create mode 100644 integration/database_events_list_test.go diff --git a/commands/databases.go b/commands/databases.go index b200a22b3..808c69008 100644 --- a/commands/databases.go +++ b/commands/databases.go @@ -155,6 +155,7 @@ For PostgreSQL and MySQL clusters, you can also provide a disk size in MiB to sc cmd.AddCommand(databaseOptions()) cmd.AddCommand(databaseConfiguration()) cmd.AddCommand(databaseTopic()) + cmd.AddCommand(databaseEvents()) return cmd } @@ -2441,3 +2442,40 @@ func RunDatabaseConfigurationUpdate(c *CmdConfig) error { } return nil } + +func databaseEvents() *Command { + listDatabaseEvents := ` + +You can get a list of database events by calling: + + doctl databases events list ` + cmd := &Command{ + Command: &cobra.Command{ + Use: "events", + Short: "Display commands for listing database cluster events", + Long: `The subcommands under ` + "`" + `doctl databases events` + "`" + ` are for listing database cluster events.` + listDatabaseEvents, + }, + } + cmdDatabaseEventsList := CmdBuilder(cmd, RunDatabaseEvents, "list ", "List your database cluster events", `Retrieves a list of database clusters events:`+listDatabaseEvents, Writer, aliasOpt("ls"), displayerType(&displayers.DatabaseEvents{})) + + cmdDatabaseEventsList.Example = `The following example retrieves a list of databases events in a database cluster with the ID ` + "`" + `ca9f591d-f38h-5555-a0ef-1c02d1d1e35` + "`" + `: doctl databases events list ca9f591d-f38h-5555-a0ef-1c02d1d1e35` + + return cmd +} + +// RunDatabaseDBList retrieves a list of databases for specific database cluster +func RunDatabaseEvents(c *CmdConfig) error { + if len(c.Args) == 0 { + return doctl.NewMissingArgsErr(c.NS) + } + + id := c.Args[0] + + dbEvents, err := c.Databases().ListDatabaseEvents(id) + if err != nil { + return err + } + + item := &displayers.DatabaseEvents{DatabaseEvents: dbEvents} + return c.Display(item) +} diff --git a/commands/databases_test.go b/commands/databases_test.go index 50c7e0ac5..6c5ba073d 100644 --- a/commands/databases_test.go +++ b/commands/databases_test.go @@ -249,6 +249,7 @@ func TestDatabasesCommand(t *testing.T) { "connection", "migrate", "resize", + "events", "firewalls", "fork", "backups", diff --git a/commands/displayers/database.go b/commands/displayers/database.go index 0153bdbac..51ccce77e 100644 --- a/commands/displayers/database.go +++ b/commands/displayers/database.go @@ -1647,3 +1647,47 @@ func (dc *RedisConfiguration) KV() []map[string]any { return o } + +type DatabaseEvents struct { + DatabaseEvents do.DatabaseEvents +} + +var _ Displayable = &DatabaseEvents{} + +func (dr *DatabaseEvents) JSON(out io.Writer) error { + return writeJSON(dr.DatabaseEvents, out) +} + +func (dr *DatabaseEvents) Cols() []string { + return []string{ + "ID", + "ServiceName", + "EventType", + "CreateTime", + } +} + +func (dr *DatabaseEvents) ColMap() map[string]string { + + return map[string]string{ + "ID": "ID", + "ServiceName": "Cluster Name", + "EventType": "Type of Event", + "CreateTime": "Create Time", + } +} + +func (dr *DatabaseEvents) KV() []map[string]any { + out := make([]map[string]any, 0, len(dr.DatabaseEvents)) + + for _, r := range dr.DatabaseEvents { + o := map[string]any{ + "ID": r.ID, + "ServiceName": r.ServiceName, + "EventType": r.EventType, + "CreateTime": r.CreateTime, + } + out = append(out, o) + } + return out +} diff --git a/do/databases.go b/do/databases.go index 75c09ccd6..bb865cae3 100644 --- a/do/databases.go +++ b/do/databases.go @@ -125,6 +125,14 @@ type DatabaseTopicPartitions struct { Partitions []*godo.TopicPartition } +// DatabaseEvent is a wrapper for godo.DatabaseEvent +type DatabaseEvent struct { + *godo.DatabaseEvent +} + +// DatabaseEvents is a slice of DatabaseEvent +type DatabaseEvents []DatabaseEvent + // DatabasesService is an interface for interacting with DigitalOcean's Database API type DatabasesService interface { List() (Databases, error) @@ -183,6 +191,8 @@ type DatabasesService interface { CreateTopic(string, *godo.DatabaseCreateTopicRequest) (*DatabaseTopic, error) UpdateTopic(string, string, *godo.DatabaseUpdateTopicRequest) error DeleteTopic(string, string) error + + ListDatabaseEvents(string) (DatabaseEvents, error) } type databasesService struct { @@ -745,3 +755,31 @@ func (ds *databasesService) DeleteTopic(databaseID, topicName string) error { return err } + +func (ds *databasesService) ListDatabaseEvents(databaseID string) (DatabaseEvents, error) { + f := func(opt *godo.ListOptions) ([]any, *godo.Response, error) { + list, resp, err := ds.client.Databases.ListDatabaseEvents(context.TODO(), databaseID, opt) + if err != nil { + return nil, nil, err + } + + si := make([]any, len(list)) + for i := range list { + si[i] = list[i] + } + + return si, resp, err + } + + si, err := PaginateResp(f) + if err != nil { + return nil, err + } + + list := make(DatabaseEvents, len(si)) + for i := range si { + r := si[i].(godo.DatabaseEvent) + list[i] = DatabaseEvent{DatabaseEvent: &r} + } + return list, nil +} diff --git a/do/mocks/DatabasesService.go b/do/mocks/DatabasesService.go index f083415a0..a85ebf4d5 100644 --- a/do/mocks/DatabasesService.go +++ b/do/mocks/DatabasesService.go @@ -469,6 +469,21 @@ func (mr *MockDatabasesServiceMockRecorder) ListDBs(arg0 any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListDBs", reflect.TypeOf((*MockDatabasesService)(nil).ListDBs), arg0) } +// ListDatabaseEvents mocks base method. +func (m *MockDatabasesService) ListDatabaseEvents(arg0 string) (do.DatabaseEvents, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListDatabaseEvents", arg0) + ret0, _ := ret[0].(do.DatabaseEvents) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListDatabaseEvents indicates an expected call of ListDatabaseEvents. +func (mr *MockDatabasesServiceMockRecorder) ListDatabaseEvents(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListDatabaseEvents", reflect.TypeOf((*MockDatabasesService)(nil).ListDatabaseEvents), arg0) +} + // ListOptions mocks base method. func (m *MockDatabasesService) ListOptions() (*do.DatabaseOptions, error) { m.ctrl.T.Helper() diff --git a/integration/database_events_list_test.go b/integration/database_events_list_test.go new file mode 100644 index 000000000..ae5a570d2 --- /dev/null +++ b/integration/database_events_list_test.go @@ -0,0 +1,92 @@ +package integration + +import ( + "fmt" + "net/http" + "net/http/httptest" + "net/http/httputil" + "os/exec" + "strings" + "testing" + + "github.com/sclevine/spec" + "github.com/stretchr/testify/require" +) + +var _ = suite("database/events", func(t *testing.T, when spec.G, it spec.S) { + var ( + expect *require.Assertions + server *httptest.Server + ) + + it.Before(func() { + expect = require.New(t) + + server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + switch req.URL.Path { + case "/v2/databases/some-database-id/events": + auth := req.Header.Get("Authorization") + if auth != "Bearer some-magic-token" { + w.WriteHeader(http.StatusTeapot) + } + + if req.Method != http.MethodGet { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + w.Write([]byte(databaseListEventsResponse)) + default: + dump, err := httputil.DumpRequest(req, true) + if err != nil { + t.Fatal("failed to dump request") + } + + t.Fatalf("received unknown request: %s", dump) + } + })) + }) + + when("all required flags are passed", func() { + it("lists users for the database", func() { + cmd := exec.Command(builtBinaryPath, + "-t", "some-magic-token", + "-u", server.URL, + "database", + "events", + "list", + "some-database-id", + ) + + output, err := cmd.CombinedOutput() + expect.NoError(err, fmt.Sprintf("received error output: %s", output)) + expect.Equal(strings.TrimSpace(databaseListEventsOutput), strings.TrimSpace(string(output))) + }) + }) +}) + +const ( + databaseListEventsOutput = ` +ID Cluster Name Type of Event Create Time +pe8u2huh customer-events cluster_create 2020-10-29T15:57:38Z +pe8ufefuh customer-events cluster_update 2023-10-30T15:57:38Z +` + databaseListEventsResponse = ` +{ + "events": [ + { + "id": "pe8u2huh", + "cluster_name": "customer-events", + "event_type": "cluster_create", + "create_time": "2020-10-29T15:57:38Z" + }, + { + "id": "pe8ufefuh", + "cluster_name": "customer-events", + "event_type": "cluster_update", + "create_time": "2023-10-30T15:57:38Z" + } + ] +} +` +)