From 9201b52c2de040141c560f2d96c9225cfe765363 Mon Sep 17 00:00:00 2001 From: Artem Gavrilov Date: Tue, 26 Sep 2023 18:35:41 +0200 Subject: [PATCH] PMM-12460 Add dump model, implement ListDumps method --- managed/models/dump.go | 76 +++++++++++ managed/models/dump_helpers.go | 84 ++++++++++++ managed/models/dump_reform.go | 161 +++++++++++++++++++++++ managed/services/management/dump/dump.go | 92 +++++++++++++ 4 files changed, 413 insertions(+) create mode 100644 managed/models/dump.go create mode 100644 managed/models/dump_helpers.go create mode 100644 managed/models/dump_reform.go create mode 100644 managed/services/management/dump/dump.go diff --git a/managed/models/dump.go b/managed/models/dump.go new file mode 100644 index 0000000000..10d3892f29 --- /dev/null +++ b/managed/models/dump.go @@ -0,0 +1,76 @@ +package models + +import ( + "time" + + "gopkg.in/reform.v1" +) + +//go:generate ../../bin/reform + +type DumpStatus string + +const ( + DumpStatusInProgress = DumpStatus("in_progress") + DumpStatusSuccess = DumpStatus("success") + DumpStatusError = DumpStatus("error") +) + +// Validate validates Dumps status. +func (ds DumpStatus) Validate() error { + switch ds { + case DumpStatusInProgress: + case DumpStatusSuccess: + case DumpStatusError: + default: + return NewInvalidArgumentError("invalid dump status '%s'", ds) + } + + return nil +} + +// Pointer returns a pointer to status value. +func (ds DumpStatus) Pointer() *DumpStatus { + return &ds +} + +// Dump represents pmm dump artifact. +// +//reform:dumps +type Dump struct { + ID string `reform:"id,pk"` + Status DumpStatus `reform:"status"` + NodeIDs []string `reform:"node_ids"` + StartTime time.Time `reform:"start_time"` + EndTime time.Time `reform:"end_time"` + CreatedAt time.Time `reform:"created_at"` + UpdatedAt time.Time `reform:"updated_at"` +} + +// BeforeInsert implements reform.BeforeInserter interface. +func (d *Dump) BeforeInsert() error { + now := Now() + d.CreatedAt = now + d.UpdatedAt = now + return nil +} + +// BeforeUpdate implements reform.BeforeUpdater interface. +func (d *Dump) BeforeUpdate() error { + d.UpdatedAt = Now() + return nil +} + +// AfterFind implements reform.AfterFinder interface. +func (d *Dump) AfterFind() error { + d.CreatedAt = d.CreatedAt.UTC() + d.UpdatedAt = d.UpdatedAt.UTC() + return nil +} + +// check interfaces. +var ( + _ reform.BeforeInserter = (*Dump)(nil) + _ reform.BeforeUpdater = (*Dump)(nil) + _ reform.AfterFinder = (*Dump)(nil) +) diff --git a/managed/models/dump_helpers.go b/managed/models/dump_helpers.go new file mode 100644 index 0000000000..f35cbf3429 --- /dev/null +++ b/managed/models/dump_helpers.go @@ -0,0 +1,84 @@ +package models + +import ( + "fmt" + "strings" + + "github.com/pkg/errors" + "gopkg.in/reform.v1" +) + +// DumpFilters represents filters for artifacts list. +type DumpFilters struct { + // Return only artifacts by specified status. + Status BackupStatus +} + +// FindDumps returns dumps list sorted by creation time in DESCENDING order. +func FindDumps(q *reform.Querier, filters DumpFilters) ([]*Dump, error) { + var conditions []string + var args []interface{} + var idx int + + if filters.Status != "" { + idx++ + conditions = append(conditions, fmt.Sprintf("status = %s", q.Placeholder(idx))) + args = append(args, filters.Status) + } + + var whereClause string + if len(conditions) != 0 { + whereClause = fmt.Sprintf("WHERE %s", strings.Join(conditions, " AND ")) + } + rows, err := q.SelectAllFrom(DumpTable, fmt.Sprintf("%s ORDER BY created_at DESC", whereClause), args...) + if err != nil { + return nil, errors.Wrap(err, "failed to select dumps") + } + + dumps := make([]*Dump, 0, len(rows)) + for _, r := range rows { + dumps = append(dumps, r.(*Dump)) //nolint:forcetypeassert + } + + return dumps, nil +} + +// FindDumpByID returns dump by given ID if found, ErrNotFound if not. +func FindDumpByID(q *reform.Querier, id string) (*Dump, error) { + if id == "" { + return nil, errors.New("provided dump id is empty") + } + + dump := &Dump{ID: id} + err := q.Reload(dump) + if err != nil { + if errors.Is(err, reform.ErrNoRows) { + return nil, errors.Wrapf(ErrNotFound, "dump by id '%s'", id) + } + return nil, errors.WithStack(err) + } + + return dump, nil +} + +// DeleteDump removes dump by ID. +func DeleteDump(q *reform.Querier, id string) error { + if _, err := FindDumpByID(q, id); err != nil { + return err + } + + if err := q.Delete(&Dump{ID: id}); err != nil { + return errors.Wrapf(err, "failed to delete dump by id '%s'", id) + } + return nil +} + +// IsDumpFinalStatus checks if dump status is one of the final ones. +func IsDumpFinalStatus(dumpStatus DumpStatus) bool { + switch dumpStatus { + case DumpStatusSuccess, DumpStatusError: + return true + default: + return false + } +} diff --git a/managed/models/dump_reform.go b/managed/models/dump_reform.go new file mode 100644 index 0000000000..f178613bd9 --- /dev/null +++ b/managed/models/dump_reform.go @@ -0,0 +1,161 @@ +// Code generated by gopkg.in/reform.v1. DO NOT EDIT. + +package models + +import ( + "fmt" + "strings" + + "gopkg.in/reform.v1" + "gopkg.in/reform.v1/parse" +) + +type dumpTableType struct { + s parse.StructInfo + z []interface{} +} + +// Schema returns a schema name in SQL database (""). +func (v *dumpTableType) Schema() string { + return v.s.SQLSchema +} + +// Name returns a view or table name in SQL database ("dumps"). +func (v *dumpTableType) Name() string { + return v.s.SQLName +} + +// Columns returns a new slice of column names for that view or table in SQL database. +func (v *dumpTableType) Columns() []string { + return []string{ + "id", + "status", + "node_ids", + "start_time", + "end_time", + "created_at", + "updated_at", + } +} + +// NewStruct makes a new struct for that view or table. +func (v *dumpTableType) NewStruct() reform.Struct { + return new(Dump) +} + +// NewRecord makes a new record for that table. +func (v *dumpTableType) NewRecord() reform.Record { + return new(Dump) +} + +// PKColumnIndex returns an index of primary key column for that table in SQL database. +func (v *dumpTableType) PKColumnIndex() uint { + return uint(v.s.PKFieldIndex) +} + +// DumpTable represents dumps view or table in SQL database. +var DumpTable = &dumpTableType{ + s: parse.StructInfo{ + Type: "Dump", + SQLName: "dumps", + Fields: []parse.FieldInfo{ + {Name: "ID", Type: "string", Column: "id"}, + {Name: "Status", Type: "DumpStatus", Column: "status"}, + {Name: "NodeIDs", Type: "[]string", Column: "node_ids"}, + {Name: "StartTime", Type: "time.Time", Column: "start_time"}, + {Name: "EndTime", Type: "time.Time", Column: "end_time"}, + {Name: "CreatedAt", Type: "time.Time", Column: "created_at"}, + {Name: "UpdatedAt", Type: "time.Time", Column: "updated_at"}, + }, + PKFieldIndex: 0, + }, + z: new(Dump).Values(), +} + +// String returns a string representation of this struct or record. +func (s Dump) String() string { + res := make([]string, 7) + res[0] = "ID: " + reform.Inspect(s.ID, true) + res[1] = "Status: " + reform.Inspect(s.Status, true) + res[2] = "NodeIDs: " + reform.Inspect(s.NodeIDs, true) + res[3] = "StartTime: " + reform.Inspect(s.StartTime, true) + res[4] = "EndTime: " + reform.Inspect(s.EndTime, true) + res[5] = "CreatedAt: " + reform.Inspect(s.CreatedAt, true) + res[6] = "UpdatedAt: " + reform.Inspect(s.UpdatedAt, true) + return strings.Join(res, ", ") +} + +// Values returns a slice of struct or record field values. +// Returned interface{} values are never untyped nils. +func (s *Dump) Values() []interface{} { + return []interface{}{ + s.ID, + s.Status, + s.NodeIDs, + s.StartTime, + s.EndTime, + s.CreatedAt, + s.UpdatedAt, + } +} + +// Pointers returns a slice of pointers to struct or record fields. +// Returned interface{} values are never untyped nils. +func (s *Dump) Pointers() []interface{} { + return []interface{}{ + &s.ID, + &s.Status, + &s.NodeIDs, + &s.StartTime, + &s.EndTime, + &s.CreatedAt, + &s.UpdatedAt, + } +} + +// View returns View object for that struct. +func (s *Dump) View() reform.View { + return DumpTable +} + +// Table returns Table object for that record. +func (s *Dump) Table() reform.Table { + return DumpTable +} + +// PKValue returns a value of primary key for that record. +// Returned interface{} value is never untyped nil. +func (s *Dump) PKValue() interface{} { + return s.ID +} + +// PKPointer returns a pointer to primary key field for that record. +// Returned interface{} value is never untyped nil. +func (s *Dump) PKPointer() interface{} { + return &s.ID +} + +// HasPK returns true if record has non-zero primary key set, false otherwise. +func (s *Dump) HasPK() bool { + return s.ID != DumpTable.z[DumpTable.s.PKFieldIndex] +} + +// SetPK sets record primary key, if possible. +// +// Deprecated: prefer direct field assignment where possible: s.ID = pk. +func (s *Dump) SetPK(pk interface{}) { + reform.SetPK(s, pk) +} + +// check interfaces +var ( + _ reform.View = DumpTable + _ reform.Struct = (*Dump)(nil) + _ reform.Table = DumpTable + _ reform.Record = (*Dump)(nil) + _ fmt.Stringer = (*Dump)(nil) +) + +func init() { + parse.AssertUpToDate(&DumpTable.s, new(Dump)) +} diff --git a/managed/services/management/dump/dump.go b/managed/services/management/dump/dump.go new file mode 100644 index 0000000000..73314bbd2d --- /dev/null +++ b/managed/services/management/dump/dump.go @@ -0,0 +1,92 @@ +package dump + +import ( + "context" + + "github.com/percona/pmm/managed/models" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "google.golang.org/protobuf/types/known/timestamppb" + "gopkg.in/reform.v1" + + dumpv1beta1 "github.com/percona/pmm/api/managementpb/dump" +) + +type Service struct { + db *reform.DB + l *logrus.Entry + + dumpv1beta1.UnimplementedDumpsServer +} + +func New(db *reform.DB) *Service { + return &Service{ + db: db, + l: logrus.WithField("component", "management/dump"), + } +} + +func (s *Service) StartDump(ctx context.Context, req *dumpv1beta1.StartDumpRequest) (*dumpv1beta1.StartDumpResponse, error) { + // TODO implement me + panic("implement me") +} + +func (s *Service) ListDumps(ctx context.Context, req *dumpv1beta1.ListDumpsRequest) (*dumpv1beta1.ListDumpsResponse, error) { + dumps, err := models.FindDumps(s.db.Querier, models.DumpFilters{}) + if err != nil { + return nil, err + } + + dumpsResponse := make([]*dumpv1beta1.Dump, 0, len(dumps)) + for _, dump := range dumps { + d, err := convertDump(dump) + if err != nil { + return nil, err + } + + dumpsResponse = append(dumpsResponse, d) + } + + return &dumpv1beta1.ListDumpsResponse{ + Dumps: dumpsResponse, + }, nil +} + +func (s *Service) DeleteDump(ctx context.Context, req *dumpv1beta1.DeleteDumpRequest) (*dumpv1beta1.DeleteDumpResponse, error) { + // TODO implement me + panic("implement me") +} + +func (s *Service) GetDumpLogs(ctx context.Context, req *dumpv1beta1.GetDumpLogsRequest) (*dumpv1beta1.GetDumpLogsResponse, error) { + // TODO implement me + panic("implement me") +} + +func convertDump(dump *models.Dump) (*dumpv1beta1.Dump, error) { + status, err := convertDumpStatus(dump.Status) + if err != nil { + return nil, errors.Wrap(err, "failed to convert dump status") + } + + return &dumpv1beta1.Dump{ + DumpId: dump.ID, + Status: status, + NodeIds: dump.NodeIDs, + StartTime: timestamppb.New(dump.StartTime), + EndTime: timestamppb.New(dump.EndTime), + CreatedAt: timestamppb.New(dump.CreatedAt), + }, nil +} + +func convertDumpStatus(status models.DumpStatus) (dumpv1beta1.DumpStatus, error) { + switch status { + case models.DumpStatusSuccess: + return dumpv1beta1.DumpStatus_BACKUP_STATUS_SUCCESS, nil + case models.DumpStatusError: + return dumpv1beta1.DumpStatus_BACKUP_STATUS_ERROR, nil + case models.DumpStatusInProgress: + return dumpv1beta1.DumpStatus_BACKUP_STATUS_IN_PROGRESS, nil + default: + return dumpv1beta1.DumpStatus_BACKUP_STATUS_INVALID, errors.Errorf("invalid status '%s'", status) + } +}