From 8bf862243b02d5c2529768e5383bdfb094256755 Mon Sep 17 00:00:00 2001 From: Sara Bee <855595+doeg@users.noreply.github.com> Date: Tue, 2 Feb 2021 17:19:09 -0500 Subject: [PATCH] Add /api/schemas endpoint to vtadmin-api Signed-off-by: Sara Bee <855595+doeg@users.noreply.github.com> --- go/vt/proto/vtadmin/vtadmin.pb.go | 287 +++++++++++--- go/vt/vtadmin/api.go | 149 ++++++++ go/vt/vtadmin/api_test.go | 480 ++++++++++++++++++++++++ go/vt/vtadmin/http/schemas.go | 32 ++ go/vt/vtadmin/testutil/proto_compare.go | 52 +++ proto/vtadmin.proto | 18 + 6 files changed, 965 insertions(+), 53 deletions(-) create mode 100644 go/vt/vtadmin/http/schemas.go create mode 100644 go/vt/vtadmin/testutil/proto_compare.go diff --git a/go/vt/proto/vtadmin/vtadmin.pb.go b/go/vt/proto/vtadmin/vtadmin.pb.go index 03ff0f561f3..7332ce1373b 100644 --- a/go/vt/proto/vtadmin/vtadmin.pb.go +++ b/go/vt/proto/vtadmin/vtadmin.pb.go @@ -12,6 +12,7 @@ import ( grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" status "google.golang.org/grpc/status" + tabletmanagerdata "vitess.io/vitess/go/vt/proto/tabletmanagerdata" topodata "vitess.io/vitess/go/vt/proto/topodata" vtctldata "vitess.io/vitess/go/vt/proto/vtctldata" ) @@ -52,7 +53,7 @@ func (x Tablet_ServingState) String() string { } func (Tablet_ServingState) EnumDescriptor() ([]byte, []int) { - return fileDescriptor_609739e22a0a50b3, []int{2, 0} + return fileDescriptor_609739e22a0a50b3, []int{3, 0} } // Cluster represents information about a Vitess cluster. @@ -152,6 +153,61 @@ func (m *Keyspace) GetKeyspace() *vtctldata.Keyspace { return nil } +type Schema struct { + Cluster *Cluster `protobuf:"bytes,1,opt,name=cluster,proto3" json:"cluster,omitempty"` + Keyspace string `protobuf:"bytes,2,opt,name=keyspace,proto3" json:"keyspace,omitempty"` + TableDefinitions []*tabletmanagerdata.TableDefinition `protobuf:"bytes,3,rep,name=table_definitions,json=tableDefinitions,proto3" json:"table_definitions,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Schema) Reset() { *m = Schema{} } +func (m *Schema) String() string { return proto.CompactTextString(m) } +func (*Schema) ProtoMessage() {} +func (*Schema) Descriptor() ([]byte, []int) { + return fileDescriptor_609739e22a0a50b3, []int{2} +} + +func (m *Schema) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Schema.Unmarshal(m, b) +} +func (m *Schema) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Schema.Marshal(b, m, deterministic) +} +func (m *Schema) XXX_Merge(src proto.Message) { + xxx_messageInfo_Schema.Merge(m, src) +} +func (m *Schema) XXX_Size() int { + return xxx_messageInfo_Schema.Size(m) +} +func (m *Schema) XXX_DiscardUnknown() { + xxx_messageInfo_Schema.DiscardUnknown(m) +} + +var xxx_messageInfo_Schema proto.InternalMessageInfo + +func (m *Schema) GetCluster() *Cluster { + if m != nil { + return m.Cluster + } + return nil +} + +func (m *Schema) GetKeyspace() string { + if m != nil { + return m.Keyspace + } + return "" +} + +func (m *Schema) GetTableDefinitions() []*tabletmanagerdata.TableDefinition { + if m != nil { + return m.TableDefinitions + } + return nil +} + // Tablet groups the topo information of a tablet together with the Vitess // cluster it belongs to. type Tablet struct { @@ -167,7 +223,7 @@ func (m *Tablet) Reset() { *m = Tablet{} } func (m *Tablet) String() string { return proto.CompactTextString(m) } func (*Tablet) ProtoMessage() {} func (*Tablet) Descriptor() ([]byte, []int) { - return fileDescriptor_609739e22a0a50b3, []int{2} + return fileDescriptor_609739e22a0a50b3, []int{3} } func (m *Tablet) XXX_Unmarshal(b []byte) error { @@ -222,7 +278,7 @@ func (m *Vtctld) Reset() { *m = Vtctld{} } func (m *Vtctld) String() string { return proto.CompactTextString(m) } func (*Vtctld) ProtoMessage() {} func (*Vtctld) Descriptor() ([]byte, []int) { - return fileDescriptor_609739e22a0a50b3, []int{3} + return fileDescriptor_609739e22a0a50b3, []int{4} } func (m *Vtctld) XXX_Unmarshal(b []byte) error { @@ -280,7 +336,7 @@ func (m *VTGate) Reset() { *m = VTGate{} } func (m *VTGate) String() string { return proto.CompactTextString(m) } func (*VTGate) ProtoMessage() {} func (*VTGate) Descriptor() ([]byte, []int) { - return fileDescriptor_609739e22a0a50b3, []int{4} + return fileDescriptor_609739e22a0a50b3, []int{5} } func (m *VTGate) XXX_Unmarshal(b []byte) error { @@ -347,7 +403,7 @@ func (m *GetClustersRequest) Reset() { *m = GetClustersRequest{} } func (m *GetClustersRequest) String() string { return proto.CompactTextString(m) } func (*GetClustersRequest) ProtoMessage() {} func (*GetClustersRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_609739e22a0a50b3, []int{5} + return fileDescriptor_609739e22a0a50b3, []int{6} } func (m *GetClustersRequest) XXX_Unmarshal(b []byte) error { @@ -379,7 +435,7 @@ func (m *GetClustersResponse) Reset() { *m = GetClustersResponse{} } func (m *GetClustersResponse) String() string { return proto.CompactTextString(m) } func (*GetClustersResponse) ProtoMessage() {} func (*GetClustersResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_609739e22a0a50b3, []int{6} + return fileDescriptor_609739e22a0a50b3, []int{7} } func (m *GetClustersResponse) XXX_Unmarshal(b []byte) error { @@ -418,7 +474,7 @@ func (m *GetGatesRequest) Reset() { *m = GetGatesRequest{} } func (m *GetGatesRequest) String() string { return proto.CompactTextString(m) } func (*GetGatesRequest) ProtoMessage() {} func (*GetGatesRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_609739e22a0a50b3, []int{7} + return fileDescriptor_609739e22a0a50b3, []int{8} } func (m *GetGatesRequest) XXX_Unmarshal(b []byte) error { @@ -457,7 +513,7 @@ func (m *GetGatesResponse) Reset() { *m = GetGatesResponse{} } func (m *GetGatesResponse) String() string { return proto.CompactTextString(m) } func (*GetGatesResponse) ProtoMessage() {} func (*GetGatesResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_609739e22a0a50b3, []int{8} + return fileDescriptor_609739e22a0a50b3, []int{9} } func (m *GetGatesResponse) XXX_Unmarshal(b []byte) error { @@ -496,7 +552,7 @@ func (m *GetKeyspacesRequest) Reset() { *m = GetKeyspacesRequest{} } func (m *GetKeyspacesRequest) String() string { return proto.CompactTextString(m) } func (*GetKeyspacesRequest) ProtoMessage() {} func (*GetKeyspacesRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_609739e22a0a50b3, []int{9} + return fileDescriptor_609739e22a0a50b3, []int{10} } func (m *GetKeyspacesRequest) XXX_Unmarshal(b []byte) error { @@ -535,7 +591,7 @@ func (m *GetKeyspacesResponse) Reset() { *m = GetKeyspacesResponse{} } func (m *GetKeyspacesResponse) String() string { return proto.CompactTextString(m) } func (*GetKeyspacesResponse) ProtoMessage() {} func (*GetKeyspacesResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_609739e22a0a50b3, []int{10} + return fileDescriptor_609739e22a0a50b3, []int{11} } func (m *GetKeyspacesResponse) XXX_Unmarshal(b []byte) error { @@ -563,6 +619,84 @@ func (m *GetKeyspacesResponse) GetKeyspaces() []*Keyspace { return nil } +type GetSchemasRequest struct { + ClusterIds []string `protobuf:"bytes,1,rep,name=cluster_ids,json=clusterIds,proto3" json:"cluster_ids,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GetSchemasRequest) Reset() { *m = GetSchemasRequest{} } +func (m *GetSchemasRequest) String() string { return proto.CompactTextString(m) } +func (*GetSchemasRequest) ProtoMessage() {} +func (*GetSchemasRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_609739e22a0a50b3, []int{12} +} + +func (m *GetSchemasRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GetSchemasRequest.Unmarshal(m, b) +} +func (m *GetSchemasRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GetSchemasRequest.Marshal(b, m, deterministic) +} +func (m *GetSchemasRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetSchemasRequest.Merge(m, src) +} +func (m *GetSchemasRequest) XXX_Size() int { + return xxx_messageInfo_GetSchemasRequest.Size(m) +} +func (m *GetSchemasRequest) XXX_DiscardUnknown() { + xxx_messageInfo_GetSchemasRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_GetSchemasRequest proto.InternalMessageInfo + +func (m *GetSchemasRequest) GetClusterIds() []string { + if m != nil { + return m.ClusterIds + } + return nil +} + +type GetSchemasResponse struct { + Schemas []*Schema `protobuf:"bytes,1,rep,name=schemas,proto3" json:"schemas,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GetSchemasResponse) Reset() { *m = GetSchemasResponse{} } +func (m *GetSchemasResponse) String() string { return proto.CompactTextString(m) } +func (*GetSchemasResponse) ProtoMessage() {} +func (*GetSchemasResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_609739e22a0a50b3, []int{13} +} + +func (m *GetSchemasResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GetSchemasResponse.Unmarshal(m, b) +} +func (m *GetSchemasResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GetSchemasResponse.Marshal(b, m, deterministic) +} +func (m *GetSchemasResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetSchemasResponse.Merge(m, src) +} +func (m *GetSchemasResponse) XXX_Size() int { + return xxx_messageInfo_GetSchemasResponse.Size(m) +} +func (m *GetSchemasResponse) XXX_DiscardUnknown() { + xxx_messageInfo_GetSchemasResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_GetSchemasResponse proto.InternalMessageInfo + +func (m *GetSchemasResponse) GetSchemas() []*Schema { + if m != nil { + return m.Schemas + } + return nil +} + type GetTabletRequest struct { Hostname string `protobuf:"bytes,1,opt,name=hostname,proto3" json:"hostname,omitempty"` // ClusterIDs is an optional parameter to narrow the scope of the search, if @@ -578,7 +712,7 @@ func (m *GetTabletRequest) Reset() { *m = GetTabletRequest{} } func (m *GetTabletRequest) String() string { return proto.CompactTextString(m) } func (*GetTabletRequest) ProtoMessage() {} func (*GetTabletRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_609739e22a0a50b3, []int{11} + return fileDescriptor_609739e22a0a50b3, []int{14} } func (m *GetTabletRequest) XXX_Unmarshal(b []byte) error { @@ -624,7 +758,7 @@ func (m *GetTabletsRequest) Reset() { *m = GetTabletsRequest{} } func (m *GetTabletsRequest) String() string { return proto.CompactTextString(m) } func (*GetTabletsRequest) ProtoMessage() {} func (*GetTabletsRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_609739e22a0a50b3, []int{12} + return fileDescriptor_609739e22a0a50b3, []int{15} } func (m *GetTabletsRequest) XXX_Unmarshal(b []byte) error { @@ -663,7 +797,7 @@ func (m *GetTabletsResponse) Reset() { *m = GetTabletsResponse{} } func (m *GetTabletsResponse) String() string { return proto.CompactTextString(m) } func (*GetTabletsResponse) ProtoMessage() {} func (*GetTabletsResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_609739e22a0a50b3, []int{13} + return fileDescriptor_609739e22a0a50b3, []int{16} } func (m *GetTabletsResponse) XXX_Unmarshal(b []byte) error { @@ -695,6 +829,7 @@ func init() { proto.RegisterEnum("vtadmin.Tablet_ServingState", Tablet_ServingState_name, Tablet_ServingState_value) proto.RegisterType((*Cluster)(nil), "vtadmin.Cluster") proto.RegisterType((*Keyspace)(nil), "vtadmin.Keyspace") + proto.RegisterType((*Schema)(nil), "vtadmin.Schema") proto.RegisterType((*Tablet)(nil), "vtadmin.Tablet") proto.RegisterType((*Vtctld)(nil), "vtadmin.Vtctld") proto.RegisterType((*VTGate)(nil), "vtadmin.VTGate") @@ -704,6 +839,8 @@ func init() { proto.RegisterType((*GetGatesResponse)(nil), "vtadmin.GetGatesResponse") proto.RegisterType((*GetKeyspacesRequest)(nil), "vtadmin.GetKeyspacesRequest") proto.RegisterType((*GetKeyspacesResponse)(nil), "vtadmin.GetKeyspacesResponse") + proto.RegisterType((*GetSchemasRequest)(nil), "vtadmin.GetSchemasRequest") + proto.RegisterType((*GetSchemasResponse)(nil), "vtadmin.GetSchemasResponse") proto.RegisterType((*GetTabletRequest)(nil), "vtadmin.GetTabletRequest") proto.RegisterType((*GetTabletsRequest)(nil), "vtadmin.GetTabletsRequest") proto.RegisterType((*GetTabletsResponse)(nil), "vtadmin.GetTabletsResponse") @@ -712,46 +849,52 @@ func init() { func init() { proto.RegisterFile("vtadmin.proto", fileDescriptor_609739e22a0a50b3) } var fileDescriptor_609739e22a0a50b3 = []byte{ - // 611 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x54, 0xdf, 0x6e, 0xd3, 0x3e, - 0x14, 0x5e, 0xb2, 0xb6, 0x69, 0x4e, 0xf6, 0x6b, 0x3b, 0x6f, 0x17, 0xf9, 0x65, 0x45, 0x54, 0x16, - 0xa0, 0x82, 0x20, 0x91, 0x02, 0x42, 0xea, 0x15, 0x1a, 0x13, 0x8a, 0xc6, 0x44, 0x8b, 0xd2, 0x52, - 0x24, 0x6e, 0xa6, 0xac, 0xb1, 0x4a, 0x44, 0xd6, 0x94, 0xda, 0xab, 0xc4, 0x8b, 0xf0, 0x56, 0x3c, - 0x09, 0x2f, 0x81, 0x62, 0x3b, 0x6e, 0xfa, 0x87, 0x6d, 0xdc, 0xd9, 0xe7, 0xcf, 0xf7, 0x7d, 0xe7, - 0x3b, 0x4e, 0xe0, 0xbf, 0x25, 0x8b, 0xe2, 0xeb, 0x64, 0xe6, 0xce, 0x17, 0x19, 0xcb, 0x90, 0x21, - 0xaf, 0x4e, 0x83, 0x65, 0xf3, 0x2c, 0x8e, 0x58, 0x24, 0x12, 0x4e, 0x73, 0xc9, 0x26, 0x2c, 0x5d, - 0x05, 0xf0, 0x0b, 0x30, 0xce, 0xd2, 0x1b, 0xca, 0xc8, 0x02, 0x35, 0x40, 0x4f, 0x62, 0x5b, 0xeb, - 0x68, 0x5d, 0x33, 0xd4, 0x93, 0x18, 0x21, 0xa8, 0xcc, 0xa2, 0x6b, 0x62, 0xeb, 0x3c, 0xc2, 0xcf, - 0x78, 0x0a, 0xf5, 0x0b, 0xf2, 0x83, 0xce, 0xa3, 0x09, 0x41, 0xcf, 0xc0, 0x98, 0x88, 0x56, 0xde, - 0x64, 0xf9, 0x2d, 0xb7, 0x50, 0x21, 0x21, 0xc3, 0xa2, 0x00, 0x79, 0x50, 0xff, 0x26, 0xfb, 0x38, - 0x9e, 0xe5, 0x1f, 0xb9, 0x2b, 0x29, 0x05, 0x64, 0xa8, 0x8a, 0xf0, 0x2f, 0x0d, 0x6a, 0xa3, 0xe8, - 0x2a, 0x25, 0xec, 0x9f, 0x78, 0xba, 0x50, 0x63, 0xbc, 0x4b, 0xb2, 0xb4, 0x5c, 0x65, 0x80, 0x40, - 0x0b, 0x65, 0x1e, 0xf9, 0x50, 0xa5, 0x2c, 0x62, 0xc4, 0xde, 0xef, 0x68, 0xdd, 0x86, 0xdf, 0x56, - 0x98, 0xa2, 0xce, 0x1d, 0x92, 0xc5, 0x32, 0x99, 0x4d, 0x87, 0x79, 0x4d, 0x28, 0x4a, 0x71, 0x0f, - 0x0e, 0xca, 0x61, 0x64, 0x81, 0xf1, 0xa9, 0x7f, 0xd1, 0x1f, 0x7c, 0xee, 0xb7, 0xf6, 0xf2, 0xcb, - 0xf0, 0x5d, 0x38, 0x3e, 0xef, 0x07, 0x2d, 0x0d, 0x35, 0xc1, 0xea, 0x0f, 0x46, 0x97, 0x45, 0x40, - 0xc7, 0x1f, 0xa1, 0x36, 0xe6, 0xf3, 0x22, 0x07, 0xea, 0x5f, 0x33, 0xca, 0xb8, 0xb5, 0xc2, 0x6c, - 0x75, 0x2f, 0x8f, 0xaa, 0xdf, 0x31, 0x2a, 0xfe, 0xa9, 0x41, 0x6d, 0x3c, 0x0a, 0x72, 0x1d, 0xb7, - 0x41, 0x22, 0xa8, 0xcc, 0xb3, 0x2c, 0x2d, 0xb6, 0x98, 0x9f, 0xf3, 0xd8, 0x84, 0xa4, 0x29, 0x1f, - 0xdd, 0x0c, 0xf9, 0xb9, 0x4c, 0x5d, 0xb9, 0xcb, 0xe5, 0x36, 0x98, 0xc5, 0xa2, 0xa8, 0x5d, 0xed, - 0xec, 0x77, 0xcd, 0x70, 0x15, 0xc0, 0xc7, 0x80, 0x02, 0xc2, 0x64, 0x13, 0x0d, 0xc9, 0xf7, 0x1b, - 0x42, 0x19, 0x3e, 0x83, 0xa3, 0xb5, 0x28, 0x9d, 0x67, 0x33, 0x4a, 0xd0, 0x73, 0xa8, 0x4b, 0x54, - 0x6a, 0x6b, 0x9d, 0xfd, 0x9d, 0xbc, 0xaa, 0x02, 0xfb, 0xd0, 0x0c, 0x08, 0xcb, 0x67, 0x2e, 0x70, - 0xd1, 0x43, 0xb0, 0x64, 0xfa, 0x32, 0x89, 0x05, 0x86, 0x19, 0x82, 0x0c, 0x9d, 0xc7, 0x14, 0xf7, - 0xa0, 0xb5, 0xea, 0x91, 0xac, 0x8f, 0xa1, 0x3a, 0xcd, 0x03, 0x92, 0xb2, 0xa9, 0x28, 0x85, 0xa1, - 0xa1, 0xc8, 0xe2, 0xd7, 0x5c, 0x73, 0xf1, 0x3a, 0xef, 0x4f, 0x19, 0xc0, 0xf1, 0x7a, 0x9f, 0xa4, - 0xf5, 0xca, 0xbe, 0x09, 0xea, 0x43, 0x45, 0xad, 0x3e, 0x82, 0x92, 0x95, 0x03, 0xae, 0x5d, 0xbe, - 0x5c, 0xc9, 0x7e, 0xdb, 0xb2, 0x37, 0x94, 0xe9, 0x5b, 0xca, 0x5e, 0xc1, 0xa1, 0x02, 0xbc, 0xff, - 0x3c, 0x6f, 0xf8, 0x46, 0x55, 0x97, 0x9c, 0xe6, 0x29, 0x18, 0xe2, 0x5b, 0xda, 0xb6, 0x51, 0x2a, - 0x2e, 0xf2, 0xfe, 0x6f, 0x1d, 0x8c, 0xf1, 0xe8, 0x34, 0xcf, 0xa1, 0xf7, 0x60, 0x95, 0x1e, 0x02, - 0x3a, 0x51, 0x4d, 0xdb, 0x8f, 0xc6, 0x69, 0xef, 0x4e, 0x0a, 0x01, 0x78, 0x0f, 0x9d, 0x42, 0xbd, - 0xd8, 0x2d, 0xb2, 0xcb, 0xb5, 0xe5, 0x27, 0xe2, 0xfc, 0xbf, 0x23, 0xa3, 0x20, 0x3e, 0xc0, 0x41, - 0x79, 0x57, 0x68, 0x8d, 0x72, 0x73, 0xf5, 0xce, 0x83, 0xbf, 0x64, 0x15, 0x5c, 0x0f, 0x4c, 0x65, - 0x15, 0x5a, 0x23, 0x5e, 0xdb, 0xa2, 0xb3, 0xe9, 0x15, 0xde, 0x43, 0x01, 0xc0, 0xca, 0x65, 0xe4, - 0x6c, 0xf7, 0x2a, 0x15, 0x27, 0x3b, 0x73, 0x85, 0x86, 0xb7, 0x4f, 0xbe, 0x3c, 0x5a, 0x26, 0x8c, - 0x50, 0xea, 0x26, 0x99, 0x27, 0x4e, 0xde, 0x34, 0xf3, 0x96, 0xcc, 0xe3, 0xff, 0x7c, 0x4f, 0x36, - 0x5f, 0xd5, 0xf8, 0xf5, 0xe5, 0x9f, 0x00, 0x00, 0x00, 0xff, 0xff, 0x2e, 0x56, 0xa5, 0x48, 0x3d, - 0x06, 0x00, 0x00, + // 709 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x55, 0x5d, 0x6e, 0xd3, 0x4c, + 0x14, 0xad, 0x93, 0x36, 0x89, 0xaf, 0xfb, 0x35, 0xe9, 0xb4, 0xd2, 0x67, 0xdc, 0x22, 0xa2, 0x11, + 0xa0, 0x80, 0xc0, 0x96, 0x0c, 0x42, 0xea, 0x13, 0x2a, 0x05, 0x59, 0xa5, 0x22, 0x41, 0x4e, 0x08, + 0x12, 0x2f, 0x95, 0x1b, 0x0f, 0xa9, 0x45, 0x62, 0x87, 0xcc, 0x34, 0x12, 0x1b, 0xe1, 0x9d, 0x3d, + 0xb0, 0x0d, 0xf6, 0x84, 0x3c, 0x33, 0x1e, 0xdb, 0x49, 0x69, 0x9b, 0x37, 0xcf, 0xfd, 0x39, 0xe7, + 0xfe, 0x9c, 0x2b, 0xc3, 0x7f, 0x0b, 0x16, 0x84, 0xd3, 0x28, 0xb6, 0x67, 0xf3, 0x84, 0x25, 0xa8, + 0x2e, 0x9f, 0xd6, 0xff, 0x2c, 0xb8, 0x98, 0x10, 0x36, 0x0d, 0xe2, 0x60, 0x4c, 0xe6, 0x61, 0xc0, + 0x02, 0x11, 0x61, 0xed, 0xb0, 0x64, 0x96, 0x14, 0xde, 0xcd, 0x05, 0x1b, 0xb1, 0x49, 0x6e, 0xc0, + 0xcf, 0xa1, 0x7e, 0x32, 0xb9, 0xa2, 0x8c, 0xcc, 0xd1, 0x0e, 0x54, 0xa2, 0xd0, 0xd4, 0xda, 0x5a, + 0x47, 0xf7, 0x2b, 0x51, 0x88, 0x10, 0x6c, 0xc6, 0xc1, 0x94, 0x98, 0x15, 0x6e, 0xe1, 0xdf, 0x78, + 0x0c, 0x8d, 0x33, 0xf2, 0x83, 0xce, 0x82, 0x11, 0x41, 0x4f, 0xa1, 0x3e, 0x12, 0xa9, 0x3c, 0xc9, + 0x70, 0x5b, 0x76, 0x56, 0x9e, 0x84, 0xf4, 0xb3, 0x00, 0xe4, 0x40, 0xe3, 0x9b, 0xcc, 0xe3, 0x78, + 0x86, 0xbb, 0x67, 0xe7, 0xa5, 0x64, 0x90, 0xbe, 0x0a, 0xc2, 0xbf, 0x34, 0xa8, 0xf5, 0x47, 0x97, + 0x64, 0x1a, 0xac, 0xc5, 0x63, 0x2d, 0xf1, 0xe8, 0x39, 0x24, 0xea, 0xc1, 0x2e, 0x1f, 0xd3, 0x79, + 0x48, 0xbe, 0x46, 0x71, 0xc4, 0xa2, 0x24, 0xa6, 0x66, 0xb5, 0x5d, 0xed, 0x18, 0x2e, 0xb6, 0x57, + 0x07, 0x38, 0x48, 0x2d, 0x6f, 0x55, 0xa8, 0xdf, 0x62, 0x65, 0x03, 0xc5, 0x7f, 0x34, 0xa8, 0xf1, + 0x28, 0xb6, 0x56, 0x8d, 0x1d, 0xa8, 0x09, 0x36, 0x39, 0x89, 0x96, 0xad, 0x96, 0x24, 0xd0, 0x7c, + 0xe9, 0x47, 0x2e, 0x6c, 0x51, 0x16, 0x30, 0x62, 0x56, 0xdb, 0x5a, 0x67, 0xc7, 0x3d, 0x54, 0x98, + 0x22, 0xce, 0xee, 0x93, 0xf9, 0x22, 0x8a, 0xc7, 0xfd, 0x34, 0xc6, 0x17, 0xa1, 0xf8, 0x08, 0xb6, + 0x8b, 0x66, 0x64, 0x40, 0xfd, 0x53, 0xf7, 0xac, 0xdb, 0xfb, 0xdc, 0x6d, 0x6d, 0xa4, 0x8f, 0xfe, + 0x3b, 0x7f, 0x78, 0xda, 0xf5, 0x5a, 0x1a, 0x6a, 0x82, 0xd1, 0xed, 0x0d, 0xce, 0x33, 0x43, 0x05, + 0x7f, 0x84, 0xda, 0x90, 0xef, 0x24, 0x1d, 0xe3, 0x65, 0x42, 0x19, 0x5f, 0xbf, 0x10, 0x84, 0x7a, + 0x17, 0x5b, 0xad, 0xdc, 0xd2, 0x2a, 0xfe, 0xa9, 0x41, 0x6d, 0x38, 0xf0, 0xd2, 0x3a, 0x6e, 0x82, + 0x44, 0xb0, 0x39, 0x4b, 0x92, 0x49, 0xa6, 0xb4, 0xf4, 0x3b, 0xb5, 0x8d, 0xc8, 0x64, 0xc2, 0x5b, + 0xd7, 0x7d, 0xfe, 0x5d, 0xa4, 0xde, 0xbc, 0x6d, 0xca, 0x87, 0xa0, 0x67, 0x9b, 0xa7, 0xe6, 0x56, + 0xbb, 0xda, 0xd1, 0xfd, 0xdc, 0x80, 0xf7, 0x01, 0x79, 0x84, 0xc9, 0x24, 0xea, 0x93, 0xef, 0x57, + 0x84, 0x32, 0x7c, 0x02, 0x7b, 0x25, 0x2b, 0x9d, 0x25, 0x31, 0x25, 0xe8, 0x19, 0x34, 0x24, 0x2a, + 0x35, 0x35, 0xae, 0x97, 0x55, 0x5e, 0x15, 0x81, 0x5d, 0x68, 0x7a, 0x84, 0xa5, 0x3d, 0x67, 0xb8, + 0xe8, 0x01, 0x18, 0xd2, 0x7d, 0x1e, 0x85, 0x02, 0x43, 0xf7, 0x41, 0x9a, 0x4e, 0x43, 0x8a, 0x8f, + 0xa0, 0x95, 0xe7, 0x48, 0xd6, 0x47, 0xb0, 0x35, 0x4e, 0x0d, 0x92, 0xb2, 0xa9, 0x28, 0xc5, 0x40, + 0x7d, 0xe1, 0xc5, 0xaf, 0x78, 0xcd, 0xd9, 0x05, 0xdd, 0x9d, 0xd2, 0x83, 0xfd, 0x72, 0x9e, 0xa4, + 0x75, 0x8a, 0x73, 0x13, 0xd4, 0xbb, 0x8a, 0x5a, 0x1d, 0x6a, 0x61, 0x94, 0x2f, 0x61, 0xd7, 0x23, + 0x4c, 0xdc, 0xea, 0xdd, 0xe9, 0x5f, 0xf3, 0x05, 0xa8, 0x2c, 0x49, 0xfe, 0x04, 0xea, 0x54, 0x98, + 0x56, 0xba, 0x16, 0xa1, 0x7e, 0xe6, 0xc7, 0x3d, 0x3e, 0x32, 0x79, 0x30, 0x92, 0xf5, 0x26, 0x8d, + 0x2d, 0x55, 0x54, 0x59, 0xa9, 0x48, 0xf4, 0x21, 0x00, 0xd7, 0xed, 0x43, 0x65, 0xe5, 0x7d, 0x88, + 0x13, 0x5e, 0xed, 0x43, 0x56, 0x9c, 0xf9, 0xdd, 0xdf, 0x55, 0xa8, 0x0f, 0x07, 0xc7, 0xa9, 0x0f, + 0xbd, 0x07, 0xa3, 0xa0, 0x3f, 0x74, 0xa0, 0x92, 0x56, 0xb5, 0x6a, 0x1d, 0x5e, 0xef, 0x14, 0x05, + 0xe0, 0x0d, 0x74, 0x0c, 0x8d, 0x4c, 0x52, 0xc8, 0x2c, 0xc6, 0x16, 0x95, 0x69, 0xdd, 0xbb, 0xc6, + 0xa3, 0x20, 0x3e, 0xc0, 0x76, 0x51, 0x22, 0xa8, 0x44, 0xb9, 0xac, 0x38, 0xeb, 0xfe, 0x3f, 0xbc, + 0x0a, 0xce, 0x03, 0xc8, 0x57, 0x8e, 0xac, 0x62, 0x78, 0x59, 0x3d, 0xd6, 0xc1, 0xb5, 0x3e, 0x05, + 0x74, 0x04, 0xba, 0x9a, 0x39, 0x2a, 0x75, 0x50, 0x92, 0x83, 0xb5, 0x3c, 0x74, 0x55, 0x83, 0x5c, + 0x57, 0xb9, 0x86, 0xf2, 0xe6, 0xcb, 0x35, 0x2c, 0xed, 0x17, 0x6f, 0xbc, 0x79, 0xfc, 0xe5, 0xe1, + 0x22, 0x62, 0x84, 0x52, 0x3b, 0x4a, 0x1c, 0xf1, 0xe5, 0x8c, 0x13, 0x67, 0xc1, 0x1c, 0xfe, 0x5f, + 0x75, 0x64, 0xf2, 0x45, 0x8d, 0x3f, 0x5f, 0xfc, 0x0d, 0x00, 0x00, 0xff, 0xff, 0x4c, 0x35, 0x35, + 0x72, 0xba, 0x07, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. @@ -772,6 +915,8 @@ type VTAdminClient interface { GetGates(ctx context.Context, in *GetGatesRequest, opts ...grpc.CallOption) (*GetGatesResponse, error) // GetKeyspaces returns all keyspaces across the specified clusters. GetKeyspaces(ctx context.Context, in *GetKeyspacesRequest, opts ...grpc.CallOption) (*GetKeyspacesResponse, error) + // GetSchemas returns all schemas across the specified clusters. + GetSchemas(ctx context.Context, in *GetSchemasRequest, opts ...grpc.CallOption) (*GetSchemasResponse, error) // GetTablet looks up a tablet by hostname across all clusters and returns // the result. GetTablet(ctx context.Context, in *GetTabletRequest, opts ...grpc.CallOption) (*Tablet, error) @@ -814,6 +959,15 @@ func (c *vTAdminClient) GetKeyspaces(ctx context.Context, in *GetKeyspacesReques return out, nil } +func (c *vTAdminClient) GetSchemas(ctx context.Context, in *GetSchemasRequest, opts ...grpc.CallOption) (*GetSchemasResponse, error) { + out := new(GetSchemasResponse) + err := c.cc.Invoke(ctx, "/vtadmin.VTAdmin/GetSchemas", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *vTAdminClient) GetTablet(ctx context.Context, in *GetTabletRequest, opts ...grpc.CallOption) (*Tablet, error) { out := new(Tablet) err := c.cc.Invoke(ctx, "/vtadmin.VTAdmin/GetTablet", in, out, opts...) @@ -840,6 +994,8 @@ type VTAdminServer interface { GetGates(context.Context, *GetGatesRequest) (*GetGatesResponse, error) // GetKeyspaces returns all keyspaces across the specified clusters. GetKeyspaces(context.Context, *GetKeyspacesRequest) (*GetKeyspacesResponse, error) + // GetSchemas returns all schemas across the specified clusters. + GetSchemas(context.Context, *GetSchemasRequest) (*GetSchemasResponse, error) // GetTablet looks up a tablet by hostname across all clusters and returns // the result. GetTablet(context.Context, *GetTabletRequest) (*Tablet, error) @@ -860,6 +1016,9 @@ func (*UnimplementedVTAdminServer) GetGates(ctx context.Context, req *GetGatesRe func (*UnimplementedVTAdminServer) GetKeyspaces(ctx context.Context, req *GetKeyspacesRequest) (*GetKeyspacesResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method GetKeyspaces not implemented") } +func (*UnimplementedVTAdminServer) GetSchemas(ctx context.Context, req *GetSchemasRequest) (*GetSchemasResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetSchemas not implemented") +} func (*UnimplementedVTAdminServer) GetTablet(ctx context.Context, req *GetTabletRequest) (*Tablet, error) { return nil, status.Errorf(codes.Unimplemented, "method GetTablet not implemented") } @@ -925,6 +1084,24 @@ func _VTAdmin_GetKeyspaces_Handler(srv interface{}, ctx context.Context, dec fun return interceptor(ctx, in, info, handler) } +func _VTAdmin_GetSchemas_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetSchemasRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(VTAdminServer).GetSchemas(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/vtadmin.VTAdmin/GetSchemas", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(VTAdminServer).GetSchemas(ctx, req.(*GetSchemasRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _VTAdmin_GetTablet_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(GetTabletRequest) if err := dec(in); err != nil { @@ -977,6 +1154,10 @@ var _VTAdmin_serviceDesc = grpc.ServiceDesc{ MethodName: "GetKeyspaces", Handler: _VTAdmin_GetKeyspaces_Handler, }, + { + MethodName: "GetSchemas", + Handler: _VTAdmin_GetSchemas_Handler, + }, { MethodName: "GetTablet", Handler: _VTAdmin_GetTablet_Handler, diff --git a/go/vt/vtadmin/api.go b/go/vt/vtadmin/api.go index fb9cae5c5d9..4a848caa7a8 100644 --- a/go/vt/vtadmin/api.go +++ b/go/vt/vtadmin/api.go @@ -80,6 +80,7 @@ func NewAPI(clusters []*cluster.Cluster, opts grpcserver.Options, httpOpts vtadm router.HandleFunc("/clusters", httpAPI.Adapt(vtadminhttp.GetClusters)).Name("API.GetClusters") router.HandleFunc("/gates", httpAPI.Adapt(vtadminhttp.GetGates)).Name("API.GetGates") router.HandleFunc("/keyspaces", httpAPI.Adapt(vtadminhttp.GetKeyspaces)).Name("API.GetKeyspaces") + router.HandleFunc("/schemas", httpAPI.Adapt(vtadminhttp.GetSchemas)).Name("API.GetSchemas") router.HandleFunc("/tablets", httpAPI.Adapt(vtadminhttp.GetTablets)).Name("API.GetTablets") router.HandleFunc("/tablet/{tablet}", httpAPI.Adapt(vtadminhttp.GetTablet)).Name("API.GetTablet") @@ -242,6 +243,154 @@ func (api *API) GetKeyspaces(ctx context.Context, req *vtadminpb.GetKeyspacesReq }, nil } +// GetSchemas is part of the vtadminpb.VTAdminServer interface. +func (api *API) GetSchemas(ctx context.Context, req *vtadminpb.GetSchemasRequest) (*vtadminpb.GetSchemasResponse, error) { + span, ctx := trace.NewSpan(ctx, "API.GetSchemas") + defer span.Finish() + + clusters, _ := api.getClustersForRequest(req.ClusterIds) + + var ( + schemas []*vtadminpb.Schema + wg sync.WaitGroup + er concurrency.AllErrorRecorder + m sync.Mutex + ) + + for _, c := range clusters { + wg.Add(1) + + // Get schemas for the cluster + go func(c *cluster.Cluster) { + defer wg.Done() + + // Since tablets are per-cluster, we can fetch them once + // and use them throughout the other waitgroups. + tablets, err := api.getTablets(ctx, c) + if err != nil { + er.RecordError(err) + return + } + + ss, err := api.getSchemas(ctx, c, tablets) + if err != nil { + er.RecordError(err) + return + } + + m.Lock() + schemas = append(schemas, ss...) + m.Unlock() + }(c) + } + + wg.Wait() + + if er.HasErrors() { + return nil, er.Error() + } + + return &vtadminpb.GetSchemasResponse{ + Schemas: schemas, + }, nil +} + +// getSchemas returns all of the schemas across all keyspaces in the given cluster. +func (api *API) getSchemas(ctx context.Context, c *cluster.Cluster, tablets []*vtadminpb.Tablet) ([]*vtadminpb.Schema, error) { + if err := c.Vtctld.Dial(ctx); err != nil { + return nil, err + } + + resp, err := c.Vtctld.GetKeyspaces(ctx, &vtctldatapb.GetKeyspacesRequest{}) + if err != nil { + return nil, err + } + + var ( + schemas []*vtadminpb.Schema + wg sync.WaitGroup + er concurrency.AllErrorRecorder + m sync.Mutex + ) + + for _, ks := range resp.Keyspaces { + wg.Add(1) + + // Get schemas for the cluster/keyspace + go func(c *cluster.Cluster, ks *vtctldatapb.Keyspace) { + defer wg.Done() + + ss, err := api.getSchemasForKeyspace(ctx, c, ks, tablets) + if err != nil { + er.RecordError(err) + return + } + + // Ignore keyspaces without schemas + if ss == nil { + return + } + + m.Lock() + schemas = append(schemas, ss) + m.Unlock() + }(c, ks) + } + + wg.Wait() + + if er.HasErrors() { + return nil, er.Error() + } + + return schemas, nil +} + +func (api *API) getSchemasForKeyspace(ctx context.Context, c *cluster.Cluster, ks *vtctldatapb.Keyspace, tablets []*vtadminpb.Tablet) (*vtadminpb.Schema, error) { + // Choose the first serving tablet. + var kt *vtadminpb.Tablet + for _, t := range tablets { + if t.Tablet.Keyspace != ks.Name || t.State != vtadminpb.Tablet_SERVING { + continue + } + + kt = t + } + + // Skip schema lookup on this keyspace if there are no serving tablets. + if kt == nil { + return nil, nil + } + + if err := c.Vtctld.Dial(ctx); err != nil { + return nil, err + } + + s, err := c.Vtctld.GetSchema(ctx, &vtctldatapb.GetSchemaRequest{ + TabletAlias: kt.Tablet.Alias, + }) + + if err != nil { + return nil, err + } + + // Ignore any schemas without table definitions; otherwise we return + // a vtadminpb.Schema object with only Cluster and Keyspace defined, + // which is not particularly useful. + if s == nil || s.Schema == nil || len(s.Schema.TableDefinitions) == 0 { + return nil, nil + } + + return &vtadminpb.Schema{ + Cluster: &vtadminpb.Cluster{ + Id: c.ID, + Name: c.Name, + }, + Keyspace: ks.Name, + TableDefinitions: s.Schema.TableDefinitions, + }, nil +} + // GetTablet is part of the vtadminpb.VTAdminServer interface. func (api *API) GetTablet(ctx context.Context, req *vtadminpb.GetTabletRequest) (*vtadminpb.Tablet, error) { span, ctx := trace.NewSpan(ctx, "API.GetTablet") diff --git a/go/vt/vtadmin/api_test.go b/go/vt/vtadmin/api_test.go index 8a234010c65..a104e2f43bc 100644 --- a/go/vt/vtadmin/api_test.go +++ b/go/vt/vtadmin/api_test.go @@ -27,7 +27,9 @@ import ( "google.golang.org/grpc" "vitess.io/vitess/go/vt/grpcclient" + "vitess.io/vitess/go/vt/topo" "vitess.io/vitess/go/vt/topo/memorytopo" + "vitess.io/vitess/go/vt/topo/topoproto" "vitess.io/vitess/go/vt/vitessdriver" "vitess.io/vitess/go/vt/vtadmin/cluster" "vitess.io/vitess/go/vt/vtadmin/cluster/discovery/fakediscovery" @@ -41,10 +43,13 @@ import ( "vitess.io/vitess/go/vt/vtctl/vtctldclient" "vitess.io/vitess/go/vt/vttablet/tmclient" + querypb "vitess.io/vitess/go/vt/proto/query" + "vitess.io/vitess/go/vt/proto/tabletmanagerdata" topodatapb "vitess.io/vitess/go/vt/proto/topodata" vtadminpb "vitess.io/vitess/go/vt/proto/vtadmin" vtctldatapb "vitess.io/vitess/go/vt/proto/vtctldata" "vitess.io/vitess/go/vt/proto/vttime" + vtadmintestutil "vitess.io/vitess/go/vt/vtadmin/testutil" ) func init() { @@ -271,6 +276,481 @@ func TestGetKeyspaces(t *testing.T) { }) } +func TestGetSchemas(t *testing.T) { + tests := []struct { + name string + clusterTablets [][]*vtadminpb.Tablet + // Indexed by tablet alias + tabletSchemas map[string]*tabletmanagerdata.SchemaDefinition + req *vtadminpb.GetSchemasRequest + expected *vtadminpb.GetSchemasResponse + }{ + { + name: "one schema in one cluster", + clusterTablets: [][]*vtadminpb.Tablet{ + // cluster0 + { + { + State: vtadminpb.Tablet_SERVING, + Tablet: &topodatapb.Tablet{ + Alias: &topodatapb.TabletAlias{ + Cell: "c0_cell1", + Uid: 100, + }, + Keyspace: "commerce", + }, + }, + }, + // cluster1 + { + { + State: vtadminpb.Tablet_SERVING, + Tablet: &topodatapb.Tablet{ + Alias: &topodatapb.TabletAlias{ + Cell: "c1_cell1", + Uid: 100, + }, + Keyspace: "commerce", + }, + }, + }, + }, + tabletSchemas: map[string]*tabletmanagerdata.SchemaDefinition{ + "c0_cell1-0000000100": { + DatabaseSchema: "CREATE DATABASE vt_testkeyspace", + TableDefinitions: []*tabletmanagerdata.TableDefinition{ + { + Name: "t1", + Schema: `CREATE TABLE t1 (id int(11) not null,PRIMARY KEY (id));`, + Type: "BASE", + Columns: []string{"id"}, + DataLength: 100, + RowCount: 50, + Fields: []*querypb.Field{ + { + Name: "id", + Type: querypb.Type_INT32, + }, + }, + }, + }, + }, + }, + req: &vtadminpb.GetSchemasRequest{}, + expected: &vtadminpb.GetSchemasResponse{ + Schemas: []*vtadminpb.Schema{ + { + Cluster: &vtadminpb.Cluster{ + Id: "c0", + Name: "cluster0", + }, + Keyspace: "commerce", + TableDefinitions: []*tabletmanagerdata.TableDefinition{ + { + Name: "t1", + Schema: `CREATE TABLE t1 (id int(11) not null,PRIMARY KEY (id));`, + Type: "BASE", + Columns: []string{"id"}, + DataLength: 100, + RowCount: 50, + Fields: []*querypb.Field{ + { + Name: "id", + Type: querypb.Type_INT32, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "one schema in each cluster", + clusterTablets: [][]*vtadminpb.Tablet{ + // cluster0 + { + { + State: vtadminpb.Tablet_SERVING, + Tablet: &topodatapb.Tablet{ + Alias: &topodatapb.TabletAlias{ + Cell: "c0_cell1", + Uid: 100, + }, + Keyspace: "commerce", + }, + }, + }, + // cluster1 + { + { + State: vtadminpb.Tablet_SERVING, + Tablet: &topodatapb.Tablet{ + Alias: &topodatapb.TabletAlias{ + Cell: "c1_cell1", + Uid: 100, + }, + Keyspace: "commerce", + }, + }, + }, + }, + tabletSchemas: map[string]*tabletmanagerdata.SchemaDefinition{ + "c0_cell1-0000000100": { + DatabaseSchema: "CREATE DATABASE vt_testkeyspace", + TableDefinitions: []*tabletmanagerdata.TableDefinition{ + { + Name: "t1", + Schema: `CREATE TABLE t1 (id int(11) not null,PRIMARY KEY (id));`, + Type: "BASE", + Columns: []string{"id"}, + DataLength: 100, + RowCount: 50, + Fields: []*querypb.Field{ + { + Name: "id", + Type: querypb.Type_INT32, + }, + }, + }, + }, + }, + "c1_cell1-0000000100": { + DatabaseSchema: "CREATE DATABASE vt_testkeyspace", + TableDefinitions: []*tabletmanagerdata.TableDefinition{ + { + Name: "t2", + Schema: `CREATE TABLE t2 (id int(11) not null,PRIMARY KEY (id));`, + Type: "BASE", + Columns: []string{"id"}, + DataLength: 100, + RowCount: 50, + Fields: []*querypb.Field{ + { + Name: "id", + Type: querypb.Type_INT32, + }, + }, + }, + }, + }, + }, + req: &vtadminpb.GetSchemasRequest{}, + expected: &vtadminpb.GetSchemasResponse{ + Schemas: []*vtadminpb.Schema{ + { + Cluster: &vtadminpb.Cluster{ + Id: "c0", + Name: "cluster0", + }, + Keyspace: "commerce", + TableDefinitions: []*tabletmanagerdata.TableDefinition{ + { + Name: "t1", + Schema: `CREATE TABLE t1 (id int(11) not null,PRIMARY KEY (id));`, + Type: "BASE", + Columns: []string{"id"}, + DataLength: 100, + RowCount: 50, + Fields: []*querypb.Field{ + { + Name: "id", + Type: querypb.Type_INT32, + }, + }, + }, + }, + }, + { + Cluster: &vtadminpb.Cluster{ + Id: "c1", + Name: "cluster1", + }, + Keyspace: "commerce", + TableDefinitions: []*tabletmanagerdata.TableDefinition{ + { + Name: "t2", + Schema: `CREATE TABLE t2 (id int(11) not null,PRIMARY KEY (id));`, + Type: "BASE", + Columns: []string{"id"}, + DataLength: 100, + RowCount: 50, + Fields: []*querypb.Field{ + { + Name: "id", + Type: querypb.Type_INT32, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "filtered by cluster ID", + clusterTablets: [][]*vtadminpb.Tablet{ + // cluster0 + { + { + State: vtadminpb.Tablet_SERVING, + Tablet: &topodatapb.Tablet{ + Alias: &topodatapb.TabletAlias{ + Cell: "c0_cell1", + Uid: 100, + }, + Keyspace: "commerce", + }, + }, + }, + // cluster1 + { + { + State: vtadminpb.Tablet_SERVING, + Tablet: &topodatapb.Tablet{ + Alias: &topodatapb.TabletAlias{ + Cell: "c1_cell1", + Uid: 100, + }, + Keyspace: "commerce", + }, + }, + }, + }, + tabletSchemas: map[string]*tabletmanagerdata.SchemaDefinition{ + "c0_cell1-0000000100": { + DatabaseSchema: "CREATE DATABASE vt_testkeyspace", + TableDefinitions: []*tabletmanagerdata.TableDefinition{ + { + Name: "t1", + Schema: `CREATE TABLE t1 (id int(11) not null,PRIMARY KEY (id));`, + Type: "BASE", + Columns: []string{"id"}, + DataLength: 100, + RowCount: 50, + Fields: []*querypb.Field{ + { + Name: "id", + Type: querypb.Type_INT32, + }, + }, + }, + }, + }, + "c1_cell1-0000000100": { + DatabaseSchema: "CREATE DATABASE vt_testkeyspace", + TableDefinitions: []*tabletmanagerdata.TableDefinition{ + { + Name: "t2", + Schema: `CREATE TABLE t2 (id int(11) not null,PRIMARY KEY (id));`, + Type: "BASE", + Columns: []string{"id"}, + DataLength: 100, + RowCount: 50, + Fields: []*querypb.Field{ + { + Name: "id", + Type: querypb.Type_INT32, + }, + }, + }, + }, + }, + }, + req: &vtadminpb.GetSchemasRequest{ + ClusterIds: []string{"c1"}, + }, + expected: &vtadminpb.GetSchemasResponse{ + Schemas: []*vtadminpb.Schema{ + { + Cluster: &vtadminpb.Cluster{ + Id: "c1", + Name: "cluster1", + }, + Keyspace: "commerce", + TableDefinitions: []*tabletmanagerdata.TableDefinition{ + { + Name: "t2", + Schema: `CREATE TABLE t2 (id int(11) not null,PRIMARY KEY (id));`, + Type: "BASE", + Columns: []string{"id"}, + DataLength: 100, + RowCount: 50, + Fields: []*querypb.Field{ + { + Name: "id", + Type: querypb.Type_INT32, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "filtered by cluster ID that doesn't exist", + clusterTablets: [][]*vtadminpb.Tablet{ + // cluster0 + { + { + State: vtadminpb.Tablet_SERVING, + Tablet: &topodatapb.Tablet{ + Alias: &topodatapb.TabletAlias{ + Cell: "c0_cell1", + Uid: 100, + }, + Keyspace: "commerce", + }, + }, + }, + }, + tabletSchemas: map[string]*tabletmanagerdata.SchemaDefinition{ + "c0_cell1-0000000100": { + DatabaseSchema: "CREATE DATABASE vt_testkeyspace", + TableDefinitions: []*tabletmanagerdata.TableDefinition{ + { + Name: "t1", + Schema: `CREATE TABLE t1 (id int(11) not null,PRIMARY KEY (id));`, + Type: "BASE", + Columns: []string{"id"}, + DataLength: 100, + RowCount: 50, + Fields: []*querypb.Field{ + { + Name: "id", + Type: querypb.Type_INT32, + }, + }, + }, + }, + }, + }, + req: &vtadminpb.GetSchemasRequest{ + ClusterIds: []string{"nope"}, + }, + expected: &vtadminpb.GetSchemasResponse{ + Schemas: []*vtadminpb.Schema{}, + }, + }, + { + name: "no schemas for any cluster", + clusterTablets: [][]*vtadminpb.Tablet{ + // cluster0 + { + { + State: vtadminpb.Tablet_SERVING, + Tablet: &topodatapb.Tablet{ + Alias: &topodatapb.TabletAlias{ + Cell: "c0_cell1", + Uid: 100, + }, + Keyspace: "commerce", + }, + }, + }, + }, + tabletSchemas: map[string]*tabletmanagerdata.SchemaDefinition{}, + req: &vtadminpb.GetSchemasRequest{}, + expected: &vtadminpb.GetSchemasResponse{ + Schemas: []*vtadminpb.Schema{}, + }, + }, + { + name: "no serving tablets", + clusterTablets: [][]*vtadminpb.Tablet{ + // cluster0 + { + { + State: vtadminpb.Tablet_NOT_SERVING, + Tablet: &topodatapb.Tablet{ + Alias: &topodatapb.TabletAlias{ + Cell: "c0_cell1", + Uid: 100, + }, + Keyspace: "commerce", + }, + }, + }, + }, + tabletSchemas: map[string]*tabletmanagerdata.SchemaDefinition{ + "c0_cell1-0000000100": { + DatabaseSchema: "CREATE DATABASE vt_testkeyspace", + TableDefinitions: []*tabletmanagerdata.TableDefinition{ + { + Name: "t1", + Schema: `CREATE TABLE t1 (id int(11) not null,PRIMARY KEY (id));`, + Type: "BASE", + Columns: []string{"id"}, + DataLength: 100, + RowCount: 50, + Fields: []*querypb.Field{ + { + Name: "id", + Type: querypb.Type_INT32, + }, + }, + }, + }, + }, + }, + req: &vtadminpb.GetSchemasRequest{}, + expected: &vtadminpb.GetSchemasResponse{ + Schemas: []*vtadminpb.Schema{}, + }, + }, + } + + for _, tt := range tests { + testutil.TabletManagerClient.Schemas = map[string]*tabletmanagerdata.SchemaDefinition{} + + topos := []*topo.Server{ + memorytopo.NewServer("c0_cell1"), + memorytopo.NewServer("c1_cell1"), + } + + // Setting up WithTestServer in a generic, recursive way is... unpleasant, + // so all tests are set-up and run in the context of these two clusters. + testutil.WithTestServer(t, grpcvtctldserver.NewVtctldServer(topos[0]), func(t *testing.T, cluster0Client vtctldclient.VtctldClient) { + testutil.WithTestServer(t, grpcvtctldserver.NewVtctldServer(topos[1]), func(t *testing.T, cluster1Client vtctldclient.VtctldClient) { + // Put 'em in a slice so we can look them up by index + clusterClients := []vtctldclient.VtctldClient{cluster0Client, cluster1Client} + + // Build the clusters + clusters := make([]*cluster.Cluster, len(topos)) + for cdx, toposerver := range topos { + // Handle when a test doesn't define any tablets for a given cluster. + var cts []*vtadminpb.Tablet + if cdx < len(tt.clusterTablets) { + cts = tt.clusterTablets[cdx] + } + + for _, tablet := range cts { + // AddTablet also adds the keyspace + shard for us. + testutil.AddTablet(context.Background(), t, toposerver, tablet.Tablet) + + // Adds each SchemaDefinition to the fake TabletManagerClient, or nil + // if there are no schemas for that tablet. (All tablet aliases must + // exist in the map. Otherwise, TabletManagerClient will return an error when + // looking up the schema with tablet alias that doesn't exist.) + alias := topoproto.TabletAliasString(tablet.Tablet.Alias) + testutil.TabletManagerClient.Schemas[alias] = tt.tabletSchemas[alias] + } + + clusters[cdx] = buildCluster(cdx, clusterClients[cdx], cts, nil) + } + + api := NewAPI(clusters, grpcserver.Options{}, http.Options{}) + + resp, err := api.GetSchemas(context.Background(), tt.req) + require.NoError(t, err) + + vtadmintestutil.AssertSchemaSlicesEqual(t, tt.expected.Schemas, resp.Schemas, tt.name) + }) + }) + } +} + func TestGetTablets(t *testing.T) { tests := []struct { name string diff --git a/go/vt/vtadmin/http/schemas.go b/go/vt/vtadmin/http/schemas.go new file mode 100644 index 00000000000..8202c872bea --- /dev/null +++ b/go/vt/vtadmin/http/schemas.go @@ -0,0 +1,32 @@ +/* +Copyright 2021 The Vitess Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package http + +import ( + "context" + + vtadminpb "vitess.io/vitess/go/vt/proto/vtadmin" +) + +// GetSchemas implements the http wrapper for /schemas[?cluster=[&cluster=]&keyspace=[&keyspace=]]. +func GetSchemas(ctx context.Context, r Request, api *API) *JSONResponse { + schemas, err := api.server.GetSchemas(ctx, &vtadminpb.GetSchemasRequest{ + ClusterIds: r.URL.Query()["cluster"], + }) + + return NewJSONResponse(schemas, err) +} diff --git a/go/vt/vtadmin/testutil/proto_compare.go b/go/vt/vtadmin/testutil/proto_compare.go new file mode 100644 index 00000000000..dcad2bcaf9b --- /dev/null +++ b/go/vt/vtadmin/testutil/proto_compare.go @@ -0,0 +1,52 @@ +/* +Copyright 2021 The Vitess Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package testutil + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + vtadminpb "vitess.io/vitess/go/vt/proto/vtadmin" +) + +// AssertSchemaSlicesEqual is a convenience function to assert that two +// []*vtadminpb.Schema slices are equal, after clearing out any reserved +// proto XXX_ fields. +func AssertSchemaSlicesEqual(t *testing.T, expected []*vtadminpb.Schema, actual []*vtadminpb.Schema, msgAndArgs ...interface{}) { + t.Helper() + + for _, ss := range [][]*vtadminpb.Schema{expected, actual} { + for _, s := range ss { + if s.TableDefinitions != nil { + for _, td := range s.TableDefinitions { + td.XXX_sizecache = 0 + td.XXX_unrecognized = nil + + if td.Fields != nil { + for _, f := range td.Fields { + f.XXX_sizecache = 0 + f.XXX_unrecognized = nil + } + } + } + } + } + } + + assert.ElementsMatch(t, expected, actual, msgAndArgs...) +} diff --git a/proto/vtadmin.proto b/proto/vtadmin.proto index f209881dddb..39d94256d63 100644 --- a/proto/vtadmin.proto +++ b/proto/vtadmin.proto @@ -21,6 +21,7 @@ option go_package = "vitess.io/vitess/go/vt/proto/vtadmin"; package vtadmin; +import "tabletmanagerdata.proto"; import "topodata.proto"; import "vtctldata.proto"; @@ -35,6 +36,8 @@ service VTAdmin { rpc GetGates(GetGatesRequest) returns (GetGatesResponse) {}; // GetKeyspaces returns all keyspaces across the specified clusters. rpc GetKeyspaces(GetKeyspacesRequest) returns (GetKeyspacesResponse) {}; + // GetSchemas returns all schemas across the specified clusters. + rpc GetSchemas(GetSchemasRequest) returns (GetSchemasResponse) {}; // GetTablet looks up a tablet by hostname across all clusters and returns // the result. rpc GetTablet(GetTabletRequest) returns (Tablet) {}; @@ -57,6 +60,13 @@ message Keyspace { vtctldata.Keyspace keyspace = 2; } +message Schema { + Cluster cluster = 1; + string keyspace = 2; + + repeated tabletmanagerdata.TableDefinition table_definitions = 3; +} + // Tablet groups the topo information of a tablet together with the Vitess // cluster it belongs to. message Tablet { @@ -117,6 +127,14 @@ message GetKeyspacesResponse { repeated Keyspace keyspaces = 1; } +message GetSchemasRequest { + repeated string cluster_ids = 1; +} + +message GetSchemasResponse { + repeated Schema schemas = 1; +} + message GetTabletRequest { string hostname = 1; // ClusterIDs is an optional parameter to narrow the scope of the search, if