diff --git a/.codeclimate.yml b/.codeclimate.yml index 11506b096..265816d5e 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -14,3 +14,8 @@ plugins: enabled: true govet: enabled: true + +checks: + return-statements: + config: + threshold: 6 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 7716f533d..736708df8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,16 @@ FROM golang:1.10.3-stretch AS build WORKDIR src/github.com/mesg-foundation/core COPY . . - ARG version RUN go build -o mesg-core \ -ldflags="-X 'github.com/mesg-foundation/core/version.Version=$version'" \ core/main.go FROM ubuntu:18.04 +RUN apt-get update && \ + apt-get install -y --no-install-recommends ca-certificates=20180409 && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* WORKDIR /app COPY --from=build /go/src/github.com/mesg-foundation/core/mesg-core . CMD ["./mesg-core"] diff --git a/Gopkg.lock b/Gopkg.lock index 205094da1..fc6ec1f91 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -18,12 +18,12 @@ revision = "7d2e70ef918f16bd6455529af38304d6d025c952" [[projects]] - branch = "master" - digest = "1:3c2529252615b209e6cf92c89a77505016f192372d279d28f49513fe7b69367c" + digest = "1:89e98f6335a3ca6d6ae179bb030d7d73326b809c3ba87e9a4b7dffd458ab85d7" name = "github.com/briandowns/spinner" packages = ["."] pruneopts = "UT" - revision = "5b875a9171af19dbde37e70a8fcbe2ebd7285e05" + revision = "9f016caa1359c8ecdc1d95243e92c0d28b524368" + version = "1.2" [[projects]] branch = "master" diff --git a/Gopkg.toml b/Gopkg.toml index 9b44498e0..061461a21 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -36,10 +36,6 @@ branch = "master" name = "github.com/asaskevich/govalidator" -[[constraint]] - branch = "master" - name = "github.com/briandowns/spinner" - [[constraint]] branch = "master" name = "github.com/cnf/structhash" @@ -68,10 +64,6 @@ branch = "master" name = "github.com/spf13/viper" -[[constraint]] - branch = "master" - name = "github.com/stvp/assert" - [[constraint]] branch = "master" name = "github.com/syndtr/goleveldb" diff --git a/api/core/api.pb.go b/api/core/api.pb.go index 1b4b480db..6b808de15 100644 --- a/api/core/api.pb.go +++ b/api/core/api.pb.go @@ -50,6 +50,31 @@ var _ = math.Inf // proto package needs to be updated. const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package +type DeployServiceReply_Status_Type int32 + +const ( + // RUNNING indicates that status message belongs to an active state. + DeployServiceReply_Status_RUNNING DeployServiceReply_Status_Type = 0 + // DONE indicates that status message belongs to completed state. + DeployServiceReply_Status_DONE DeployServiceReply_Status_Type = 1 +) + +var DeployServiceReply_Status_Type_name = map[int32]string{ + 0: "RUNNING", + 1: "DONE", +} +var DeployServiceReply_Status_Type_value = map[string]int32{ + "RUNNING": 0, + "DONE": 1, +} + +func (x DeployServiceReply_Status_Type) String() string { + return proto.EnumName(DeployServiceReply_Status_Type_name, int32(x)) +} +func (DeployServiceReply_Status_Type) EnumDescriptor() ([]byte, []int) { + return fileDescriptor0, []int{11, 0, 0} +} + // The request's data for the `ListenEvent` stream's API. // // **Example** @@ -402,7 +427,10 @@ func (*StopServiceReply) Descriptor() ([]byte, []int) { return fileDescriptor0, // } // ``` type DeployServiceRequest struct { - Service *service.Service `protobuf:"bytes,1,opt,name=service" json:"service,omitempty"` + // Types that are valid to be assigned to Value: + // *DeployServiceRequest_Url + // *DeployServiceRequest_Chunk + Value isDeployServiceRequest_Value `protobuf_oneof:"value"` } func (m *DeployServiceRequest) Reset() { *m = DeployServiceRequest{} } @@ -410,13 +438,107 @@ func (m *DeployServiceRequest) String() string { return proto.Compact func (*DeployServiceRequest) ProtoMessage() {} func (*DeployServiceRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{10} } -func (m *DeployServiceRequest) GetService() *service.Service { +type isDeployServiceRequest_Value interface { + isDeployServiceRequest_Value() +} + +type DeployServiceRequest_Url struct { + Url string `protobuf:"bytes,2,opt,name=url,oneof"` +} +type DeployServiceRequest_Chunk struct { + Chunk []byte `protobuf:"bytes,3,opt,name=chunk,proto3,oneof"` +} + +func (*DeployServiceRequest_Url) isDeployServiceRequest_Value() {} +func (*DeployServiceRequest_Chunk) isDeployServiceRequest_Value() {} + +func (m *DeployServiceRequest) GetValue() isDeployServiceRequest_Value { if m != nil { - return m.Service + return m.Value + } + return nil +} + +func (m *DeployServiceRequest) GetUrl() string { + if x, ok := m.GetValue().(*DeployServiceRequest_Url); ok { + return x.Url + } + return "" +} + +func (m *DeployServiceRequest) GetChunk() []byte { + if x, ok := m.GetValue().(*DeployServiceRequest_Chunk); ok { + return x.Chunk } return nil } +// XXX_OneofFuncs is for the internal use of the proto package. +func (*DeployServiceRequest) XXX_OneofFuncs() (func(msg proto.Message, b *proto.Buffer) error, func(msg proto.Message, tag, wire int, b *proto.Buffer) (bool, error), func(msg proto.Message) (n int), []interface{}) { + return _DeployServiceRequest_OneofMarshaler, _DeployServiceRequest_OneofUnmarshaler, _DeployServiceRequest_OneofSizer, []interface{}{ + (*DeployServiceRequest_Url)(nil), + (*DeployServiceRequest_Chunk)(nil), + } +} + +func _DeployServiceRequest_OneofMarshaler(msg proto.Message, b *proto.Buffer) error { + m := msg.(*DeployServiceRequest) + // value + switch x := m.Value.(type) { + case *DeployServiceRequest_Url: + b.EncodeVarint(2<<3 | proto.WireBytes) + b.EncodeStringBytes(x.Url) + case *DeployServiceRequest_Chunk: + b.EncodeVarint(3<<3 | proto.WireBytes) + b.EncodeRawBytes(x.Chunk) + case nil: + default: + return fmt.Errorf("DeployServiceRequest.Value has unexpected type %T", x) + } + return nil +} + +func _DeployServiceRequest_OneofUnmarshaler(msg proto.Message, tag, wire int, b *proto.Buffer) (bool, error) { + m := msg.(*DeployServiceRequest) + switch tag { + case 2: // value.url + if wire != proto.WireBytes { + return true, proto.ErrInternalBadWireType + } + x, err := b.DecodeStringBytes() + m.Value = &DeployServiceRequest_Url{x} + return true, err + case 3: // value.chunk + if wire != proto.WireBytes { + return true, proto.ErrInternalBadWireType + } + x, err := b.DecodeRawBytes(true) + m.Value = &DeployServiceRequest_Chunk{x} + return true, err + default: + return false, nil + } +} + +func _DeployServiceRequest_OneofSizer(msg proto.Message) (n int) { + m := msg.(*DeployServiceRequest) + // value + switch x := m.Value.(type) { + case *DeployServiceRequest_Url: + n += proto.SizeVarint(2<<3 | proto.WireBytes) + n += proto.SizeVarint(uint64(len(x.Url))) + n += len(x.Url) + case *DeployServiceRequest_Chunk: + n += proto.SizeVarint(3<<3 | proto.WireBytes) + n += proto.SizeVarint(uint64(len(x.Chunk))) + n += len(x.Chunk) + case nil: + default: + panic(fmt.Sprintf("proto: unexpected type %T in oneof", x)) + } + return n +} + // The reply's data of `DeployService` API. // // **Example** @@ -426,7 +548,11 @@ func (m *DeployServiceRequest) GetService() *service.Service { // } // ``` type DeployServiceReply struct { - ServiceID string `protobuf:"bytes,1,opt,name=serviceID" json:"serviceID,omitempty"` + // Types that are valid to be assigned to Value: + // *DeployServiceReply_Status_ + // *DeployServiceReply_ServiceID + // *DeployServiceReply_ValidationError + Value isDeployServiceReply_Value `protobuf_oneof:"value"` } func (m *DeployServiceReply) Reset() { *m = DeployServiceReply{} } @@ -434,13 +560,163 @@ func (m *DeployServiceReply) String() string { return proto.CompactTe func (*DeployServiceReply) ProtoMessage() {} func (*DeployServiceReply) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{11} } +type isDeployServiceReply_Value interface { + isDeployServiceReply_Value() +} + +type DeployServiceReply_Status_ struct { + Status *DeployServiceReply_Status `protobuf:"bytes,2,opt,name=status,oneof"` +} +type DeployServiceReply_ServiceID struct { + ServiceID string `protobuf:"bytes,3,opt,name=serviceID,oneof"` +} +type DeployServiceReply_ValidationError struct { + ValidationError string `protobuf:"bytes,4,opt,name=validationError,oneof"` +} + +func (*DeployServiceReply_Status_) isDeployServiceReply_Value() {} +func (*DeployServiceReply_ServiceID) isDeployServiceReply_Value() {} +func (*DeployServiceReply_ValidationError) isDeployServiceReply_Value() {} + +func (m *DeployServiceReply) GetValue() isDeployServiceReply_Value { + if m != nil { + return m.Value + } + return nil +} + +func (m *DeployServiceReply) GetStatus() *DeployServiceReply_Status { + if x, ok := m.GetValue().(*DeployServiceReply_Status_); ok { + return x.Status + } + return nil +} + func (m *DeployServiceReply) GetServiceID() string { + if x, ok := m.GetValue().(*DeployServiceReply_ServiceID); ok { + return x.ServiceID + } + return "" +} + +func (m *DeployServiceReply) GetValidationError() string { + if x, ok := m.GetValue().(*DeployServiceReply_ValidationError); ok { + return x.ValidationError + } + return "" +} + +// XXX_OneofFuncs is for the internal use of the proto package. +func (*DeployServiceReply) XXX_OneofFuncs() (func(msg proto.Message, b *proto.Buffer) error, func(msg proto.Message, tag, wire int, b *proto.Buffer) (bool, error), func(msg proto.Message) (n int), []interface{}) { + return _DeployServiceReply_OneofMarshaler, _DeployServiceReply_OneofUnmarshaler, _DeployServiceReply_OneofSizer, []interface{}{ + (*DeployServiceReply_Status_)(nil), + (*DeployServiceReply_ServiceID)(nil), + (*DeployServiceReply_ValidationError)(nil), + } +} + +func _DeployServiceReply_OneofMarshaler(msg proto.Message, b *proto.Buffer) error { + m := msg.(*DeployServiceReply) + // value + switch x := m.Value.(type) { + case *DeployServiceReply_Status_: + b.EncodeVarint(2<<3 | proto.WireBytes) + if err := b.EncodeMessage(x.Status); err != nil { + return err + } + case *DeployServiceReply_ServiceID: + b.EncodeVarint(3<<3 | proto.WireBytes) + b.EncodeStringBytes(x.ServiceID) + case *DeployServiceReply_ValidationError: + b.EncodeVarint(4<<3 | proto.WireBytes) + b.EncodeStringBytes(x.ValidationError) + case nil: + default: + return fmt.Errorf("DeployServiceReply.Value has unexpected type %T", x) + } + return nil +} + +func _DeployServiceReply_OneofUnmarshaler(msg proto.Message, tag, wire int, b *proto.Buffer) (bool, error) { + m := msg.(*DeployServiceReply) + switch tag { + case 2: // value.status + if wire != proto.WireBytes { + return true, proto.ErrInternalBadWireType + } + msg := new(DeployServiceReply_Status) + err := b.DecodeMessage(msg) + m.Value = &DeployServiceReply_Status_{msg} + return true, err + case 3: // value.serviceID + if wire != proto.WireBytes { + return true, proto.ErrInternalBadWireType + } + x, err := b.DecodeStringBytes() + m.Value = &DeployServiceReply_ServiceID{x} + return true, err + case 4: // value.validationError + if wire != proto.WireBytes { + return true, proto.ErrInternalBadWireType + } + x, err := b.DecodeStringBytes() + m.Value = &DeployServiceReply_ValidationError{x} + return true, err + default: + return false, nil + } +} + +func _DeployServiceReply_OneofSizer(msg proto.Message) (n int) { + m := msg.(*DeployServiceReply) + // value + switch x := m.Value.(type) { + case *DeployServiceReply_Status_: + s := proto.Size(x.Status) + n += proto.SizeVarint(2<<3 | proto.WireBytes) + n += proto.SizeVarint(uint64(s)) + n += s + case *DeployServiceReply_ServiceID: + n += proto.SizeVarint(3<<3 | proto.WireBytes) + n += proto.SizeVarint(uint64(len(x.ServiceID))) + n += len(x.ServiceID) + case *DeployServiceReply_ValidationError: + n += proto.SizeVarint(4<<3 | proto.WireBytes) + n += proto.SizeVarint(uint64(len(x.ValidationError))) + n += len(x.ValidationError) + case nil: + default: + panic(fmt.Sprintf("proto: unexpected type %T in oneof", x)) + } + return n +} + +type DeployServiceReply_Status struct { + // message is status message. + Message string `protobuf:"bytes,1,opt,name=message" json:"message,omitempty"` + // type is the type of status message. + Type DeployServiceReply_Status_Type `protobuf:"varint,2,opt,name=type,enum=api.DeployServiceReply_Status_Type" json:"type,omitempty"` +} + +func (m *DeployServiceReply_Status) Reset() { *m = DeployServiceReply_Status{} } +func (m *DeployServiceReply_Status) String() string { return proto.CompactTextString(m) } +func (*DeployServiceReply_Status) ProtoMessage() {} +func (*DeployServiceReply_Status) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{11, 0} } + +func (m *DeployServiceReply_Status) GetMessage() string { if m != nil { - return m.ServiceID + return m.Message } return "" } +func (m *DeployServiceReply_Status) GetType() DeployServiceReply_Status_Type { + if m != nil { + return m.Type + } + return DeployServiceReply_Status_RUNNING +} + // Request's data of the `DeleteService` API. // // **Example** @@ -614,12 +890,14 @@ func init() { proto.RegisterType((*StopServiceReply)(nil), "api.StopServiceReply") proto.RegisterType((*DeployServiceRequest)(nil), "api.DeployServiceRequest") proto.RegisterType((*DeployServiceReply)(nil), "api.DeployServiceReply") + proto.RegisterType((*DeployServiceReply_Status)(nil), "api.DeployServiceReply.Status") proto.RegisterType((*DeleteServiceRequest)(nil), "api.DeleteServiceRequest") proto.RegisterType((*DeleteServiceReply)(nil), "api.DeleteServiceReply") proto.RegisterType((*ListServicesRequest)(nil), "api.ListServicesRequest") proto.RegisterType((*ListServicesReply)(nil), "api.ListServicesReply") proto.RegisterType((*GetServiceRequest)(nil), "api.GetServiceRequest") proto.RegisterType((*GetServiceReply)(nil), "api.GetServiceReply") + proto.RegisterEnum("api.DeployServiceReply_Status_Type", DeployServiceReply_Status_Type_name, DeployServiceReply_Status_Type_value) } // Reference imports to suppress errors if they are not otherwise used. @@ -644,7 +922,7 @@ type CoreClient interface { // Stop a service. The service must be already deployed to [Core](../guide/start-here/core.md). StopService(ctx context.Context, in *StopServiceRequest, opts ...grpc.CallOption) (*StopServiceReply, error) // Deploy a service to [Core](../guide/start-here/core.md). This will give you an unique identifier which is used to interact with the service. - DeployService(ctx context.Context, in *DeployServiceRequest, opts ...grpc.CallOption) (*DeployServiceReply, error) + DeployService(ctx context.Context, opts ...grpc.CallOption) (Core_DeployServiceClient, error) // Delete a service from Core. This function only deletes a deployed service in [Core](../guide/start-here/core.md). If the service's code is on your computer, the source code will not be deleted. DeleteService(ctx context.Context, in *DeleteServiceRequest, opts ...grpc.CallOption) (*DeleteServiceReply, error) // List all services already deployed in [Core](../guide/start-here/core.md). @@ -752,13 +1030,35 @@ func (c *coreClient) StopService(ctx context.Context, in *StopServiceRequest, op return out, nil } -func (c *coreClient) DeployService(ctx context.Context, in *DeployServiceRequest, opts ...grpc.CallOption) (*DeployServiceReply, error) { - out := new(DeployServiceReply) - err := grpc.Invoke(ctx, "/api.Core/DeployService", in, out, c.cc, opts...) +func (c *coreClient) DeployService(ctx context.Context, opts ...grpc.CallOption) (Core_DeployServiceClient, error) { + stream, err := grpc.NewClientStream(ctx, &_Core_serviceDesc.Streams[2], c.cc, "/api.Core/DeployService", opts...) if err != nil { return nil, err } - return out, nil + x := &coreDeployServiceClient{stream} + return x, nil +} + +type Core_DeployServiceClient interface { + Send(*DeployServiceRequest) error + Recv() (*DeployServiceReply, error) + grpc.ClientStream +} + +type coreDeployServiceClient struct { + grpc.ClientStream +} + +func (x *coreDeployServiceClient) Send(m *DeployServiceRequest) error { + return x.ClientStream.SendMsg(m) +} + +func (x *coreDeployServiceClient) Recv() (*DeployServiceReply, error) { + m := new(DeployServiceReply) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil } func (c *coreClient) DeleteService(ctx context.Context, in *DeleteServiceRequest, opts ...grpc.CallOption) (*DeleteServiceReply, error) { @@ -802,7 +1102,7 @@ type CoreServer interface { // Stop a service. The service must be already deployed to [Core](../guide/start-here/core.md). StopService(context.Context, *StopServiceRequest) (*StopServiceReply, error) // Deploy a service to [Core](../guide/start-here/core.md). This will give you an unique identifier which is used to interact with the service. - DeployService(context.Context, *DeployServiceRequest) (*DeployServiceReply, error) + DeployService(Core_DeployServiceServer) error // Delete a service from Core. This function only deletes a deployed service in [Core](../guide/start-here/core.md). If the service's code is on your computer, the source code will not be deleted. DeleteService(context.Context, *DeleteServiceRequest) (*DeleteServiceReply, error) // List all services already deployed in [Core](../guide/start-here/core.md). @@ -911,22 +1211,30 @@ func _Core_StopService_Handler(srv interface{}, ctx context.Context, dec func(in return interceptor(ctx, in, info, handler) } -func _Core_DeployService_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(DeployServiceRequest) - if err := dec(in); err != nil { +func _Core_DeployService_Handler(srv interface{}, stream grpc.ServerStream) error { + return srv.(CoreServer).DeployService(&coreDeployServiceServer{stream}) +} + +type Core_DeployServiceServer interface { + Send(*DeployServiceReply) error + Recv() (*DeployServiceRequest, error) + grpc.ServerStream +} + +type coreDeployServiceServer struct { + grpc.ServerStream +} + +func (x *coreDeployServiceServer) Send(m *DeployServiceReply) error { + return x.ServerStream.SendMsg(m) +} + +func (x *coreDeployServiceServer) Recv() (*DeployServiceRequest, error) { + m := new(DeployServiceRequest) + if err := x.ServerStream.RecvMsg(m); err != nil { return nil, err } - if interceptor == nil { - return srv.(CoreServer).DeployService(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/api.Core/DeployService", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(CoreServer).DeployService(ctx, req.(*DeployServiceRequest)) - } - return interceptor(ctx, in, info, handler) + return m, nil } func _Core_DeleteService_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { @@ -999,10 +1307,6 @@ var _Core_serviceDesc = grpc.ServiceDesc{ MethodName: "StopService", Handler: _Core_StopService_Handler, }, - { - MethodName: "DeployService", - Handler: _Core_DeployService_Handler, - }, { MethodName: "DeleteService", Handler: _Core_DeleteService_Handler, @@ -1027,6 +1331,12 @@ var _Core_serviceDesc = grpc.ServiceDesc{ Handler: _Core_ListenResult_Handler, ServerStreams: true, }, + { + StreamName: "DeployService", + Handler: _Core_DeployService_Handler, + ServerStreams: true, + ClientStreams: true, + }, }, Metadata: "github.com/mesg-foundation/core/api/core/api.proto", } @@ -1034,45 +1344,55 @@ var _Core_serviceDesc = grpc.ServiceDesc{ func init() { proto.RegisterFile("github.com/mesg-foundation/core/api/core/api.proto", fileDescriptor0) } var fileDescriptor0 = []byte{ - // 635 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x55, 0xcf, 0x6e, 0xd3, 0x4e, - 0x10, 0x8e, 0x7f, 0xf1, 0xaf, 0x6d, 0x26, 0x29, 0x4d, 0x36, 0x29, 0x35, 0x16, 0x42, 0xd1, 0x8a, - 0x03, 0x42, 0x90, 0x40, 0x5a, 0x2e, 0x88, 0xaa, 0x22, 0x24, 0x20, 0x04, 0xa7, 0x24, 0x27, 0x6e, - 0x6e, 0x58, 0x82, 0x55, 0xd7, 0x36, 0xde, 0x75, 0x45, 0xde, 0x82, 0x1b, 0x2f, 0xc1, 0xfb, 0xf0, - 0x3a, 0x68, 0xff, 0xd8, 0x5e, 0x67, 0xad, 0x36, 0x39, 0x25, 0xfb, 0xcd, 0xcc, 0x37, 0xdf, 0x8e, - 0x3f, 0x8f, 0x61, 0xb4, 0xf2, 0xd9, 0xf7, 0xf4, 0x72, 0xb0, 0x8c, 0xae, 0x87, 0xd7, 0x84, 0xae, - 0x9e, 0x7f, 0x8b, 0xd2, 0xf0, 0xab, 0xc7, 0xfc, 0x28, 0x1c, 0x2e, 0xa3, 0x84, 0x0c, 0xbd, 0xd8, - 0xcf, 0xff, 0x0c, 0xe2, 0x24, 0x62, 0x11, 0xaa, 0x7b, 0xb1, 0xef, 0xbe, 0xba, 0xab, 0x90, 0x92, - 0xe4, 0xc6, 0x5f, 0xe6, 0xbf, 0xb2, 0x16, 0x2f, 0x00, 0x7d, 0xf6, 0x29, 0x23, 0xe1, 0xf4, 0x86, - 0x84, 0x6c, 0x46, 0x7e, 0xa4, 0x84, 0x32, 0xf4, 0x10, 0x1a, 0x2a, 0xed, 0xe3, 0xc4, 0xb1, 0xfa, - 0xd6, 0x93, 0xc6, 0xac, 0x00, 0x50, 0x1f, 0x9a, 0x84, 0x67, 0xbf, 0xf7, 0x03, 0x46, 0x12, 0xe7, - 0x3f, 0x11, 0xd7, 0x21, 0x3c, 0x85, 0x86, 0xe0, 0x9b, 0x78, 0xcc, 0x43, 0x2e, 0x1c, 0x88, 0xd8, - 0x27, 0xb2, 0x56, 0x5c, 0xf9, 0x99, 0x37, 0x22, 0x59, 0xa2, 0x22, 0x2a, 0x00, 0xfc, 0xdb, 0x82, - 0xae, 0x54, 0x37, 0x23, 0x34, 0x0d, 0xb6, 0x94, 0xf7, 0x08, 0x80, 0x79, 0xf4, 0xaa, 0xa4, 0x4e, - 0x43, 0x10, 0x86, 0x56, 0x94, 0xb2, 0x38, 0xcd, 0xf4, 0xd7, 0x45, 0x46, 0x09, 0x93, 0x1c, 0x2b, - 0x79, 0xa0, 0x8e, 0xdd, 0xaf, 0x4b, 0x8e, 0x0c, 0xc1, 0x7f, 0x2c, 0x00, 0xa9, 0x49, 0x5c, 0x91, - 0x4f, 0xe4, 0x27, 0x59, 0xa6, 0x7c, 0xda, 0xb9, 0x24, 0x1d, 0x42, 0x0e, 0xec, 0x73, 0x09, 0x7c, - 0x06, 0x52, 0x51, 0x76, 0xe4, 0x97, 0x91, 0xad, 0x79, 0x4c, 0x6a, 0x29, 0x00, 0x2e, 0x44, 0x1e, - 0xc4, 0x84, 0x6c, 0x79, 0x99, 0x02, 0x41, 0x8f, 0xe1, 0x30, 0x6f, 0xb3, 0xf0, 0x56, 0xd4, 0xf9, - 0x5f, 0x68, 0x2d, 0x83, 0xf8, 0x97, 0x05, 0x68, 0x2a, 0x10, 0xb2, 0xf0, 0xe8, 0xd5, 0x76, 0x73, - 0xbc, 0x55, 0xb2, 0x1f, 0x66, 0x9a, 0x94, 0xe4, 0x1c, 0x30, 0x25, 0xd9, 0x55, 0x92, 0xce, 0xa0, - 0x5d, 0x52, 0x14, 0x07, 0xeb, 0xbb, 0xc7, 0x88, 0x4f, 0xa1, 0x3b, 0x67, 0x5e, 0xc2, 0xe6, 0x52, - 0xe5, 0x56, 0x17, 0xc1, 0x5d, 0xe8, 0x94, 0x8b, 0xe2, 0x60, 0x8d, 0x47, 0x80, 0xe6, 0x2c, 0x8a, - 0x77, 0x22, 0x42, 0xd0, 0x2e, 0xd5, 0x70, 0x9e, 0x31, 0xf4, 0x26, 0x24, 0x0e, 0xa2, 0xf5, 0x06, - 0xd3, 0x53, 0xd8, 0x57, 0x85, 0x82, 0xa7, 0x39, 0x6a, 0x0f, 0xb2, 0x37, 0x2f, 0xcb, 0xcc, 0x12, - 0xb8, 0x96, 0x0d, 0x0e, 0x3e, 0x8d, 0xdb, 0xb5, 0x9c, 0xf1, 0xbe, 0x01, 0x61, 0x64, 0xa7, 0x1b, - 0xf4, 0x78, 0xa7, 0x52, 0x15, 0xbf, 0xc3, 0xb1, 0x7c, 0xcd, 0x14, 0x46, 0x15, 0x15, 0x7e, 0x0b, - 0x9d, 0x32, 0xcc, 0x55, 0x3d, 0x83, 0x03, 0x45, 0x47, 0x1d, 0xab, 0x5f, 0xaf, 0xbc, 0x58, 0x9e, - 0x81, 0x5f, 0x42, 0xe7, 0x03, 0xd9, 0xed, 0x69, 0x9d, 0xc3, 0x91, 0x5e, 0xc2, 0x7b, 0xee, 0x30, - 0xcb, 0xd1, 0x5f, 0x1b, 0xec, 0x77, 0x51, 0x42, 0xd0, 0x6b, 0x68, 0x6a, 0x9b, 0x0d, 0x9d, 0x0c, - 0xf8, 0xc2, 0x34, 0x77, 0x9d, 0x7b, 0x4f, 0x04, 0xf2, 0x75, 0x85, 0x6b, 0x2f, 0x2c, 0x74, 0x0e, - 0x2d, 0x7d, 0xef, 0x20, 0x47, 0x2b, 0x2e, 0xad, 0x22, 0xf7, 0x48, 0x44, 0x8a, 0x55, 0x20, 0xca, - 0x2f, 0xa0, 0xa9, 0x79, 0x5b, 0xb5, 0x36, 0xdf, 0x3f, 0xf7, 0xd8, 0x0c, 0xf0, 0xc7, 0x51, 0x43, - 0x63, 0x68, 0xe9, 0x8e, 0x55, 0xfd, 0x2b, 0x9c, 0xef, 0xde, 0xaf, 0x88, 0x48, 0x8e, 0x0b, 0x68, - 0x6a, 0x66, 0x55, 0x22, 0x4c, 0xcb, 0x2b, 0x11, 0x86, 0xaf, 0x6b, 0x68, 0x0a, 0x87, 0x25, 0x57, - 0xa2, 0x07, 0x22, 0xb3, 0xca, 0xed, 0xee, 0x49, 0x55, 0x48, 0xa3, 0xd1, 0x2c, 0x97, 0xd3, 0x98, - 0xe6, 0xcd, 0x69, 0x0c, 0x87, 0x8a, 0x91, 0xe8, 0x66, 0xd4, 0x1e, 0xc9, 0x86, 0x6d, 0xd5, 0x48, - 0x0c, 0xe7, 0xe2, 0x1a, 0x7a, 0x03, 0x50, 0x58, 0x0b, 0xc9, 0x3c, 0xc3, 0x9e, 0x6e, 0xcf, 0xc0, - 0x45, 0xf5, 0x78, 0xef, 0x8b, 0xcd, 0x3f, 0xa4, 0x97, 0x7b, 0xe2, 0xcb, 0x79, 0xfa, 0x2f, 0x00, - 0x00, 0xff, 0xff, 0x5b, 0x79, 0x02, 0x9e, 0xab, 0x07, 0x00, 0x00, + // 794 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x56, 0xdd, 0x4e, 0xdb, 0x48, + 0x14, 0xb6, 0x89, 0x49, 0xe0, 0x24, 0x40, 0x18, 0xfe, 0xb2, 0xd6, 0x2e, 0x8a, 0x66, 0xf7, 0x02, + 0xa1, 0xdd, 0xc0, 0x06, 0xaa, 0x56, 0x55, 0x11, 0x2a, 0x4d, 0x4a, 0x10, 0x55, 0x2a, 0x39, 0xe9, + 0x4d, 0xef, 0x4c, 0x3a, 0x0d, 0x16, 0x26, 0x76, 0x3d, 0xe3, 0xa8, 0xe9, 0x53, 0xf4, 0xae, 0x97, + 0x7d, 0x81, 0xbe, 0x51, 0x5f, 0xa6, 0x9a, 0x1f, 0x3b, 0xe3, 0x38, 0xe5, 0xe7, 0x2a, 0x99, 0xef, + 0x9c, 0xf3, 0xcd, 0x37, 0xdf, 0x9c, 0x39, 0x32, 0x34, 0x87, 0x1e, 0xbb, 0x8e, 0xaf, 0x1a, 0x83, + 0xe0, 0xf6, 0xe0, 0x96, 0xd0, 0xe1, 0x7f, 0x1f, 0x83, 0x78, 0xf4, 0xc1, 0x65, 0x5e, 0x30, 0x3a, + 0x18, 0x04, 0x11, 0x39, 0x70, 0x43, 0x2f, 0xfd, 0xd3, 0x08, 0xa3, 0x80, 0x05, 0xa8, 0xe0, 0x86, + 0x9e, 0xfd, 0xe4, 0xbe, 0x42, 0x4a, 0xa2, 0xb1, 0x37, 0x48, 0x7f, 0x65, 0x2d, 0xee, 0x03, 0x7a, + 0xe3, 0x51, 0x46, 0x46, 0xed, 0x31, 0x19, 0x31, 0x87, 0x7c, 0x8a, 0x09, 0x65, 0xe8, 0x4f, 0x58, + 0x56, 0x69, 0x17, 0xad, 0x9a, 0x59, 0x37, 0xf7, 0x96, 0x9d, 0x29, 0x80, 0xea, 0x50, 0x26, 0x3c, + 0xfb, 0xb5, 0xe7, 0x33, 0x12, 0xd5, 0x16, 0x44, 0x5c, 0x87, 0x70, 0x1b, 0x96, 0x05, 0x5f, 0xcb, + 0x65, 0x2e, 0xb2, 0x61, 0x49, 0xc4, 0x2e, 0xc9, 0x44, 0x71, 0xa5, 0x6b, 0xbe, 0x11, 0x49, 0x12, + 0x15, 0xd1, 0x14, 0xc0, 0xdf, 0x4c, 0xd8, 0x90, 0xea, 0x1c, 0x42, 0x63, 0xff, 0x81, 0xf2, 0x76, + 0x01, 0x98, 0x4b, 0x6f, 0x32, 0xea, 0x34, 0x04, 0x61, 0xa8, 0x04, 0x31, 0x0b, 0xe3, 0x44, 0x7f, + 0x41, 0x64, 0x64, 0x30, 0xc9, 0x31, 0x94, 0x0b, 0x5a, 0xb3, 0xea, 0x05, 0xc9, 0x91, 0x20, 0xf8, + 0x87, 0x09, 0x20, 0x35, 0x89, 0x23, 0x72, 0x47, 0x3e, 0x93, 0x41, 0xcc, 0xdd, 0x4e, 0x25, 0xe9, + 0x10, 0xaa, 0x41, 0x89, 0x4b, 0xe0, 0x1e, 0x48, 0x45, 0xc9, 0x92, 0x1f, 0x46, 0x6e, 0xcd, 0x63, + 0x52, 0xcb, 0x14, 0xe0, 0x42, 0xe4, 0x42, 0x38, 0x64, 0xc9, 0xc3, 0x4c, 0x11, 0xf4, 0x0f, 0xac, + 0xa4, 0xdb, 0xf4, 0xdd, 0x21, 0xad, 0x2d, 0x0a, 0xad, 0x59, 0x10, 0x7f, 0x35, 0x01, 0xb5, 0x05, + 0x42, 0xfa, 0x2e, 0xbd, 0x79, 0x98, 0x8f, 0x77, 0x4a, 0xf6, 0x46, 0x89, 0x26, 0x25, 0x39, 0x05, + 0xf2, 0x92, 0xac, 0x79, 0x92, 0x8e, 0xa1, 0x9a, 0x51, 0x14, 0xfa, 0x93, 0xfb, 0x6d, 0xc4, 0x47, + 0xb0, 0xd1, 0x63, 0x6e, 0xc4, 0x7a, 0x52, 0xe5, 0x83, 0x0e, 0x82, 0x37, 0x60, 0x3d, 0x5b, 0x14, + 0xfa, 0x13, 0xdc, 0x04, 0xd4, 0x63, 0x41, 0xf8, 0x28, 0x22, 0x04, 0xd5, 0x4c, 0x0d, 0xe7, 0xb9, + 0x84, 0xcd, 0x16, 0x09, 0xfd, 0x60, 0x32, 0xc3, 0x84, 0xa0, 0x10, 0x47, 0xbe, 0x74, 0xae, 0x63, + 0x38, 0x7c, 0x81, 0xb6, 0x61, 0x71, 0x70, 0x1d, 0x8f, 0x6e, 0x84, 0x67, 0x95, 0x8e, 0xe1, 0xc8, + 0xe5, 0x59, 0x09, 0x16, 0xc7, 0xae, 0x1f, 0x13, 0xfc, 0x7d, 0x01, 0xd0, 0x0c, 0x1b, 0xf7, 0xe5, + 0x19, 0x14, 0x29, 0x73, 0x59, 0x4c, 0x05, 0x5d, 0xb9, 0xb9, 0xdb, 0xe0, 0x8f, 0x3f, 0x9f, 0xd8, + 0xe8, 0x89, 0xac, 0x8e, 0xe1, 0xa8, 0x7c, 0xb4, 0xab, 0x9f, 0xa7, 0xa0, 0xb4, 0x68, 0x77, 0xbc, + 0x0f, 0x6b, 0x63, 0xd7, 0xf7, 0xe4, 0x9c, 0x68, 0x47, 0x51, 0x10, 0xc9, 0x1e, 0xeb, 0x18, 0xce, + 0x6c, 0xc0, 0xfe, 0x02, 0x45, 0xc9, 0xcf, 0x3b, 0xe3, 0x96, 0x50, 0xea, 0x0e, 0x89, 0xf2, 0x28, + 0x59, 0xa2, 0xa7, 0x60, 0xb1, 0x49, 0x48, 0x84, 0xce, 0xd5, 0xe6, 0xdf, 0x77, 0xeb, 0x6c, 0xf4, + 0x27, 0x21, 0x71, 0x44, 0x01, 0xfe, 0x0b, 0x2c, 0xbe, 0x42, 0x65, 0x28, 0x39, 0xef, 0xba, 0xdd, + 0x8b, 0xee, 0x79, 0xd5, 0x40, 0x4b, 0x60, 0xb5, 0xde, 0x76, 0xdb, 0x55, 0x73, 0xea, 0xd0, 0x31, + 0xb7, 0xdb, 0x27, 0x8c, 0x3c, 0xea, 0xe2, 0x36, 0xb9, 0xad, 0x99, 0x2a, 0x7e, 0x75, 0x5b, 0x72, + 0xba, 0x28, 0x8c, 0x2a, 0x2a, 0xfc, 0x12, 0xd6, 0xb3, 0x30, 0xbf, 0x82, 0x7f, 0x61, 0x49, 0xd1, + 0xd1, 0x9a, 0x59, 0x2f, 0xec, 0x95, 0x9b, 0xd5, 0x46, 0x32, 0x49, 0x13, 0xd2, 0x34, 0x03, 0xff, + 0x0f, 0xeb, 0xe7, 0xe4, 0x71, 0x4d, 0x7a, 0x02, 0x6b, 0x7a, 0x09, 0xdf, 0x73, 0x1f, 0x4a, 0x2a, + 0x2e, 0xd2, 0xe7, 0x6d, 0x99, 0x24, 0x34, 0x7f, 0x5a, 0x60, 0xbd, 0x0a, 0x22, 0x82, 0x9e, 0x43, + 0x59, 0x1b, 0xe8, 0x68, 0x47, 0x5c, 0x41, 0x7e, 0xc4, 0xdb, 0xab, 0x22, 0x90, 0x4e, 0x69, 0x6c, + 0x1c, 0x9a, 0xe8, 0x04, 0x2a, 0xfa, 0xb8, 0x45, 0x35, 0xad, 0x38, 0x33, 0x81, 0xed, 0x35, 0x11, + 0x99, 0x4e, 0x40, 0x51, 0x7e, 0x0a, 0x65, 0xed, 0x49, 0xab, 0xad, 0xf3, 0x63, 0xc7, 0xde, 0xca, + 0x07, 0xf8, 0x75, 0x18, 0xe8, 0x0c, 0x2a, 0xfa, 0x43, 0x55, 0xfb, 0xcf, 0x79, 0xf0, 0xf6, 0xf6, + 0x9c, 0x88, 0xe4, 0x38, 0x85, 0xb2, 0xf6, 0x46, 0x95, 0x88, 0xfc, 0x4b, 0x57, 0x22, 0x72, 0xcf, + 0xd9, 0x40, 0x17, 0xb0, 0x92, 0xe9, 0x58, 0xf4, 0xc7, 0xbc, 0x2e, 0x96, 0x24, 0x3b, 0xbf, 0x69, + 0x70, 0x6c, 0xec, 0x99, 0x87, 0x26, 0x6a, 0x73, 0x2a, 0xad, 0xed, 0x52, 0xaa, 0x7c, 0x03, 0xa7, + 0x54, 0xb9, 0x2e, 0x15, 0xb6, 0xe8, 0x0d, 0xa9, 0x5d, 0xcb, 0x4c, 0xeb, 0x2a, 0x5b, 0x72, 0xdd, + 0x8b, 0x0d, 0xf4, 0x02, 0x60, 0xda, 0x5e, 0x48, 0xe6, 0xe5, 0x5a, 0xd4, 0xde, 0xcc, 0xe1, 0xa2, + 0xfa, 0xac, 0xf8, 0xde, 0xe2, 0xdf, 0x10, 0x57, 0x45, 0xf1, 0xd1, 0x70, 0xf4, 0x2b, 0x00, 0x00, + 0xff, 0xff, 0x62, 0x1f, 0x31, 0xe7, 0xa6, 0x08, 0x00, 0x00, } diff --git a/api/core/api.proto b/api/core/api.proto index 1a553d867..b4bdbdb19 100644 --- a/api/core/api.proto +++ b/api/core/api.proto @@ -31,7 +31,7 @@ service Core { rpc StopService (StopServiceRequest) returns (StopServiceReply) {} // Deploy a service to [Core](../guide/start-here/core.md). This will give you an unique identifier which is used to interact with the service. - rpc DeployService (DeployServiceRequest) returns (DeployServiceReply) {} + rpc DeployService (stream DeployServiceRequest) returns (stream DeployServiceReply) {} // Delete a service from Core. This function only deletes a deployed service in [Core](../guide/start-here/core.md). If the service's code is on your computer, the source code will not be deleted. rpc DeleteService (DeleteServiceRequest) returns (DeleteServiceReply) {} @@ -205,7 +205,15 @@ message StopServiceReply { // } // ``` message DeployServiceRequest { - service.Service service = 1; // The service's definition to deploy. [Details here](./service-type.md). + oneof value { + // Git repo url of service. If url provided, stream will be closed after + // first receive. + string url = 2; + + // Chunks of gzipped tar archive of service. If chunk provided, stream will + // be closed after all chunks sent. + bytes chunk = 3; + } } // The reply's data of `DeployService` API. @@ -217,7 +225,34 @@ message DeployServiceRequest { // } // ``` message DeployServiceReply { - string serviceID = 1; // The generated identifier of the deployed service. Use this ID with other APIs. + message Status { + enum Type { + // RUNNING indicates that status message belongs to an active state. + RUNNING = 0; + + // DONE indicates that status message belongs to completed state. + DONE = 1; + } + + // message is status message. + string message = 1; + + // type is the type of status message. + Type type = 2; + } + + oneof value { + // status will be sent after each status change. + Status status = 2; + + // serviceID will be sent as the last message of stream when + // service deployed successfully. + string serviceID = 3; + + // validationError will be sent as the last message of stream when + // there is a validation error. + string validationError = 4; + } } // Request's data of the `DeleteService` API. diff --git a/api/core/core_test.go b/api/core/core_test.go new file mode 100644 index 000000000..5ea2ce99d --- /dev/null +++ b/api/core/core_test.go @@ -0,0 +1,38 @@ +package core + +import ( + "testing" + + "github.com/mesg-foundation/core/container" + "github.com/mesg-foundation/core/container/dockertest" + "github.com/mesg-foundation/core/mesg" + "github.com/stretchr/testify/require" +) + +func newServer(t *testing.T) *Server { + container, err := container.New() + require.Nil(t, err) + + m, err := mesg.New(mesg.ContainerOption(container)) + require.Nil(t, err) + + server, err := NewServer(MESGOption(m)) + require.Nil(t, err) + + return server +} + +func newServerAndDockerTest(t *testing.T) (*Server, *dockertest.Testing) { + dt := dockertest.New() + + container, err := container.New(container.ClientOption(dt.Client())) + require.Nil(t, err) + + m, err := mesg.New(mesg.ContainerOption(container)) + require.Nil(t, err) + + server, err := NewServer(MESGOption(m)) + require.Nil(t, err) + + return server, dt +} diff --git a/api/core/delete_test.go b/api/core/delete_test.go index 790f1b25f..9372e439e 100644 --- a/api/core/delete_test.go +++ b/api/core/delete_test.go @@ -13,22 +13,18 @@ var serverdelete = new(Server) func TestDeleteService(t *testing.T) { emptyService := service.Service{} - service := service.Service{ - Name: "TestDeleteService", - Dependencies: map[string]*service.Dependency{ - "test": { - Image: "nginx", - }, - }, - } - deployment, _ := serverdelete.DeployService(context.Background(), &DeployServiceRequest{ - Service: &service, - }) + + url := "https://github.com/mesg-foundation/service-webhook" + + server := newServer(t) + stream := newTestDeployStream(url) + server.DeployService(stream) + reply, err := serverdelete.DeleteService(context.Background(), &DeleteServiceRequest{ - ServiceID: deployment.ServiceID, + ServiceID: stream.serviceID, }) require.Nil(t, err) require.NotNil(t, reply) - x, _ := services.Get(deployment.ServiceID) - require.Equal(t, x, emptyService) + x, _ := services.Get(stream.serviceID) + require.Equal(t, emptyService, x) } diff --git a/api/core/deploy.go b/api/core/deploy.go index de99b12ce..a69d32d25 100644 --- a/api/core/deploy.go +++ b/api/core/deploy.go @@ -1,19 +1,100 @@ package core import ( - "context" - - "github.com/mesg-foundation/core/database/services" + "github.com/mesg-foundation/core/mesg" + service "github.com/mesg-foundation/core/service" + "github.com/mesg-foundation/core/service/importer" ) -// DeployService saves a service in the database and returns the hash of this service. -func (s *Server) DeployService(ctx context.Context, request *DeployServiceRequest) (*DeployServiceReply, error) { - service := request.Service - hash, err := services.Save(service) +// DeployService deploys a service from Git URL or service.tar.gz file. It'll send status +// events during the process and finish with sending service id or validation error. +func (s *Server) DeployService(stream Core_DeployServiceServer) error { + statuses := make(chan mesg.DeployStatus, 0) + go sendDeployStatus(statuses, stream) + + var ( + service *service.Service + validationError *importer.ValidationError + err error + ) + + sr := newDeployServiceStreamReader(stream) + url, err := sr.GetURL() + if err != nil { + return err + } + if url != "" { + service, validationError, err = s.mesg.DeployServiceFromURL(url, mesg.DeployServiceStatusOption(statuses)) + } else { + service, validationError, err = s.mesg.DeployService(sr, mesg.DeployServiceStatusOption(statuses)) + } + if err != nil { - return nil, err + return err + } + if validationError != nil { + return stream.Send(&DeployServiceReply{ + Value: &DeployServiceReply_ValidationError{ValidationError: validationError.Error()}, + }) + } + + return stream.Send(&DeployServiceReply{ + Value: &DeployServiceReply_ServiceID{ServiceID: service.Id}, + }) +} + +func sendDeployStatus(statuses chan mesg.DeployStatus, stream Core_DeployServiceServer) { + for status := range statuses { + var typ DeployServiceReply_Status_Type + switch status.Type { + case mesg.RUNNING: + typ = DeployServiceReply_Status_RUNNING + case mesg.DONE: + typ = DeployServiceReply_Status_DONE + } + + stream.Send(&DeployServiceReply{ + Value: &DeployServiceReply_Status_{ + Status: &DeployServiceReply_Status{ + Message: status.Message, + Type: typ, + }, + }, + }) + } +} + +type deployServiceStreamReader struct { + stream Core_DeployServiceServer + + data []byte + i int64 +} + +func newDeployServiceStreamReader(stream Core_DeployServiceServer) *deployServiceStreamReader { + return &deployServiceStreamReader{stream: stream} +} + +func (r *deployServiceStreamReader) GetURL() (url string, err error) { + message, err := r.stream.Recv() + if err != nil { + return "", err + } + r.data = message.GetChunk() + return message.GetUrl(), err +} + +func (r *deployServiceStreamReader) Read(p []byte) (n int, err error) { + if r.i >= int64(len(r.data)) { + message, err := r.stream.Recv() + if err != nil { + return 0, err + } + r.data = message.GetChunk() + r.i = 0 + return r.Read(p) } - return &DeployServiceReply{ - ServiceID: hash, - }, nil + n = copy(p, r.data[r.i:]) + r.i += int64(n) + return n, nil } diff --git a/api/core/deploy_integration_test.go b/api/core/deploy_integration_test.go new file mode 100644 index 000000000..8b3dfb444 --- /dev/null +++ b/api/core/deploy_integration_test.go @@ -0,0 +1,30 @@ +// +build integration + +package core + +import ( + "fmt" + "testing" + + "github.com/cnf/structhash" + "github.com/logrusorgru/aurora" + "github.com/mesg-foundation/core/database/services" + "github.com/mesg-foundation/core/mesg" + "github.com/stretchr/testify/require" +) + +func TestIntegrationDeployService(t *testing.T) { + url := "https://github.com/mesg-foundation/service-webhook" + + server := newServer(t) + stream := newTestDeployStream(url) + + require.Nil(t, server.DeployService(stream)) + defer services.Delete(stream.serviceID) + + require.Equal(t, 1, structhash.Version(stream.serviceID)) + require.Contains(t, stream.statuses, mesg.DeployStatus{ + Message: fmt.Sprintf("%s Completed.", aurora.Green("✔")), + Type: mesg.DONE, + }) +} diff --git a/api/core/deploy_test.go b/api/core/deploy_test.go index 802c23d4d..fca5dc3ff 100644 --- a/api/core/deploy_test.go +++ b/api/core/deploy_test.go @@ -1,30 +1,70 @@ package core import ( - "context" + "fmt" + "io/ioutil" + "strings" "testing" - "github.com/mesg-foundation/core/database/services" - "github.com/mesg-foundation/core/service" + "github.com/cnf/structhash" + "github.com/logrusorgru/aurora" + "github.com/mesg-foundation/core/mesg" "github.com/stretchr/testify/require" + grpc "google.golang.org/grpc" ) -var serverdeploy = new(Server) - func TestDeployService(t *testing.T) { - service := service.Service{ - Name: "TestDeployService", - Dependencies: map[string]*service.Dependency{ - "test": { - Image: "nginx", - }, - }, - } - deployment, err := serverdeploy.DeployService(context.Background(), &DeployServiceRequest{ - Service: &service, + url := "https://github.com/mesg-foundation/service-webhook" + + server, dt := newServerAndDockerTest(t) + dt.ProvideImageBuild(ioutil.NopCloser(strings.NewReader(`{"stream":"sha256:x"}`)), nil) + + stream := newTestDeployStream(url) + require.Nil(t, server.DeployService(stream)) + require.Equal(t, 1, structhash.Version(stream.serviceID)) + + require.Contains(t, stream.statuses, mesg.DeployStatus{ + Message: fmt.Sprintf("%s Completed.", aurora.Green("✔")), + Type: mesg.DONE, }) - require.Nil(t, err) - require.NotNil(t, deployment) - require.NotEqual(t, "", deployment.ServiceID) - services.Delete(deployment.ServiceID) +} + +// TODO(ilgooz) also add tests for receiving chunks. +type testDeployStream struct { + url string // Git repo url. + err error + serviceID string + statuses []mesg.DeployStatus + grpc.ServerStream +} + +func newTestDeployStream(url string) *testDeployStream { + return &testDeployStream{url: url} +} + +func (s *testDeployStream) Send(m *DeployServiceReply) error { + s.serviceID = m.GetServiceID() + + status := m.GetStatus() + if status != nil { + var typ mesg.StatusType + switch status.Type { + case DeployServiceReply_Status_RUNNING: + typ = mesg.RUNNING + case DeployServiceReply_Status_DONE: + typ = mesg.DONE + } + s.statuses = append(s.statuses, mesg.DeployStatus{ + Message: status.Message, + Type: typ, + }) + } + + return nil +} + +func (s *testDeployStream) Recv() (*DeployServiceRequest, error) { + return &DeployServiceRequest{ + Value: &DeployServiceRequest_Url{Url: s.url}, + }, s.err } diff --git a/api/core/execute_test.go b/api/core/execute_test.go index 2fcd6ebb6..a7b5e9698 100644 --- a/api/core/execute_test.go +++ b/api/core/execute_test.go @@ -13,73 +13,68 @@ import ( var serverexecute = new(Server) func TestExecute(t *testing.T) { - srv := service.Service{ - Name: "TestExecute", - Tasks: map[string]*service.Task{ - "test": &service.Task{}, - }, - Dependencies: map[string]*service.Dependency{ - "test": { - Image: "nginx", - }, - }, - } - deployment, _ := serverexecute.DeployService(context.Background(), &DeployServiceRequest{ - Service: &srv, + url := "https://github.com/mesg-foundation/service-webhook" + taskKey := "call" + data := `{"url": "https://mesg.tech", "data": {}, "headers": {}}` + + server := newServer(t) + stream := newTestDeployStream(url) + + server.DeployService(stream) + defer services.Delete(stream.serviceID) + + serverexecute.StartService(context.Background(), &StartServiceRequest{ + ServiceID: stream.serviceID, }) - defer services.Delete(deployment.ServiceID) - srv.Start() - defer srv.Stop() + defer serverexecute.StopService(context.Background(), &StopServiceRequest{ + ServiceID: stream.serviceID, + }) + reply, err := serverexecute.ExecuteTask(context.Background(), &ExecuteTaskRequest{ - ServiceID: deployment.ServiceID, - TaskKey: "test", - InputData: "{}", + ServiceID: stream.serviceID, + TaskKey: taskKey, + InputData: data, }) require.Nil(t, err) - require.NotNil(t, reply) + require.NotEqual(t, "", reply.ExecutionID) } func TestExecuteWithInvalidJSON(t *testing.T) { - deployment, _ := serverexecute.DeployService(context.Background(), &DeployServiceRequest{ - Service: &service.Service{ - Name: "TestExecuteWithInvalidJSON", - Tasks: map[string]*service.Task{ - "test": {}, - }, - }, - }) + url := "https://github.com/mesg-foundation/service-webhook" + + server := newServer(t) + stream := newTestDeployStream(url) + server.DeployService(stream) + _, err := serverexecute.ExecuteTask(context.Background(), &ExecuteTaskRequest{ - ServiceID: deployment.ServiceID, + ServiceID: stream.serviceID, TaskKey: "test", InputData: "", }) - require.NotNil(t, err) require.Equal(t, err.Error(), "unexpected end of JSON input") - services.Delete(deployment.ServiceID) + services.Delete(stream.serviceID) } func TestExecuteWithInvalidTask(t *testing.T) { - srv := service.Service{ - Name: "TestExecuteWithInvalidTask", - Tasks: map[string]*service.Task{ - "test": {}, - }, - Dependencies: map[string]*service.Dependency{ - "test": { - Image: "nginx", - }, - }, - } - deployment, _ := serverexecute.DeployService(context.Background(), &DeployServiceRequest{ - Service: &srv, + url := "https://github.com/mesg-foundation/service-webhook" + + server := newServer(t) + stream := newTestDeployStream(url) + + server.DeployService(stream) + defer services.Delete(stream.serviceID) + + serverexecute.StartService(context.Background(), &StartServiceRequest{ + ServiceID: stream.serviceID, }) - defer services.Delete(deployment.ServiceID) - srv.Start() - defer srv.Stop() + defer serverexecute.StopService(context.Background(), &StopServiceRequest{ + ServiceID: stream.serviceID, + }) + _, err := serverexecute.ExecuteTask(context.Background(), &ExecuteTaskRequest{ - ServiceID: deployment.ServiceID, + ServiceID: stream.serviceID, TaskKey: "error", InputData: "{}", }) @@ -89,18 +84,16 @@ func TestExecuteWithInvalidTask(t *testing.T) { } func TestExecuteWithNonRunningService(t *testing.T) { - srv := service.Service{ - Name: "TestExecuteWithNonRunningService", - Tasks: map[string]*service.Task{ - "test": &service.Task{}, - }, - } - deployment, _ := serverexecute.DeployService(context.Background(), &DeployServiceRequest{ - Service: &srv, - }) - defer services.Delete(deployment.ServiceID) + url := "https://github.com/mesg-foundation/service-webhook" + + server := newServer(t) + stream := newTestDeployStream(url) + + server.DeployService(stream) + defer services.Delete(stream.serviceID) + _, err := serverexecute.ExecuteTask(context.Background(), &ExecuteTaskRequest{ - ServiceID: deployment.ServiceID, + ServiceID: stream.serviceID, TaskKey: "test", InputData: "{}", }) diff --git a/api/core/get_service_test.go b/api/core/get_service_test.go index 7c999a367..75813ecde 100644 --- a/api/core/get_service_test.go +++ b/api/core/get_service_test.go @@ -12,12 +12,13 @@ import ( var servergetservice = new(Server) func TestGetService(t *testing.T) { - hash, _ := services.Save(&service.Service{ + service := &service.Service{ Name: "TestGetService", - }) - defer services.Delete(hash) + } + services.Save(service) + defer services.Delete(service.Id) reply, err := servergetservice.GetService(context.Background(), &GetServiceRequest{ - ServiceID: hash, + ServiceID: service.Id, }) require.Nil(t, err) require.NotNil(t, reply) diff --git a/api/core/start_test.go b/api/core/start_test.go index a1f19b68d..572b26286 100644 --- a/api/core/start_test.go +++ b/api/core/start_test.go @@ -12,24 +12,20 @@ import ( var serverstart = new(Server) func TestStartService(t *testing.T) { - deployment, _ := serverstart.DeployService(context.Background(), &DeployServiceRequest{ - Service: &service.Service{ - Name: "TestStartService", - Dependencies: map[string]*service.Dependency{ - "test": { - Image: "nginx", - }, - }, - }, - }) - s, _ := services.Get(deployment.ServiceID) + url := "https://github.com/mesg-foundation/service-webhook" + + server := newServer(t) + stream := newTestDeployStream(url) + server.DeployService(stream) + + s, _ := services.Get(stream.serviceID) reply, err := serverstart.StartService(context.Background(), &StartServiceRequest{ - ServiceID: deployment.ServiceID, + ServiceID: stream.serviceID, }) require.Nil(t, err) status, _ := s.Status() require.Equal(t, service.RUNNING, status) require.NotNil(t, reply) s.Stop() - services.Delete(deployment.ServiceID) + services.Delete(stream.serviceID) } diff --git a/api/core/stop_test.go b/api/core/stop_test.go index f20314f18..b1c6c3457 100644 --- a/api/core/stop_test.go +++ b/api/core/stop_test.go @@ -12,24 +12,20 @@ import ( var serverstop = new(Server) func TestStopService(t *testing.T) { - deployment, _ := serverstop.DeployService(context.Background(), &DeployServiceRequest{ - Service: &service.Service{ - Name: "TestStopService", - Dependencies: map[string]*service.Dependency{ - "test": { - Image: "nginx", - }, - }, - }, - }) - s, _ := services.Get(deployment.ServiceID) + url := "https://github.com/mesg-foundation/service-webhook" + + server := newServer(t) + stream := newTestDeployStream(url) + server.DeployService(stream) + + s, _ := services.Get(stream.serviceID) s.Start() reply, err := serverstop.StopService(context.Background(), &StopServiceRequest{ - ServiceID: deployment.ServiceID, + ServiceID: stream.serviceID, }) status, _ := s.Status() require.Equal(t, service.STOPPED, status) require.Nil(t, err) require.NotNil(t, reply) - services.Delete(deployment.ServiceID) + services.Delete(stream.serviceID) } diff --git a/api/core/type.go b/api/core/type.go index 3dd313d26..0e6e3901c 100644 --- a/api/core/type.go +++ b/api/core/type.go @@ -1,4 +1,34 @@ package core +import ( + "errors" + + "github.com/mesg-foundation/core/mesg" +) + // Server is the type to aggregate all the APIs. -type Server struct{} +type Server struct { + mesg *mesg.MESG +} + +// Option is a configuration func for Server. +type Option func(*Server) + +// NewServer creates a new Server with given options. +func NewServer(options ...Option) (*Server, error) { + s := &Server{} + for _, option := range options { + option(s) + } + if s.mesg == nil { + return nil, errors.New("mesg should be provided") + } + return s, nil +} + +// MESGOption configures underlying mesg access API. +func MESGOption(mesg *mesg.MESG) Option { + return func(s *Server) { + s.mesg = mesg + } +} diff --git a/api/server.go b/api/server.go index 1f49521d9..d57ae935f 100644 --- a/api/server.go +++ b/api/server.go @@ -9,6 +9,7 @@ import ( "github.com/grpc-ecosystem/go-grpc-middleware/logging/logrus" "github.com/mesg-foundation/core/api/core" "github.com/mesg-foundation/core/api/service" + "github.com/mesg-foundation/core/mesg" "github.com/sirupsen/logrus" "google.golang.org/grpc" "google.golang.org/grpc/reflection" @@ -45,7 +46,9 @@ func (s *Server) Serve() error { grpc_logrus.UnaryServerInterceptor(logrus.NewEntry(logrus.StandardLogger())), )), ) - s.register() + if err := s.register(); err != nil { + return err + } logrus.Info("Server listens on ", s.listener.Addr()) @@ -62,9 +65,20 @@ func (s *Server) Stop() { } // register all server -func (s *Server) register() { +func (s *Server) register() error { + m, err := mesg.New() + if err != nil { + return err + } + + coreServer, err := core.NewServer(core.MESGOption(m)) + if err != nil { + return err + } + service.RegisterServiceServer(s.instance, &service.Server{}) - core.RegisterCoreServer(s.instance, &core.Server{}) + core.RegisterCoreServer(s.instance, coreServer) reflection.Register(s.instance) + return nil } diff --git a/api/service/emit_event_test.go b/api/service/emit_event_test.go index 256b8393c..022304ffb 100644 --- a/api/service/emit_event_test.go +++ b/api/service/emit_event_test.go @@ -24,8 +24,9 @@ func TestEmit(t *testing.T) { }, }, } - hash, _ := services.Save(&service) - defer services.Delete(hash) + services.Save(&service) + service.Id = "" // TODO(ilgooz) remove this when Service type created by hand. + defer services.Delete(service.Id) subscription := pubsub.Subscribe(service.EventSubscriptionChannel()) @@ -41,8 +42,9 @@ func TestEmit(t *testing.T) { func TestEmitNoData(t *testing.T) { service := service.Service{} - hash, _ := services.Save(&service) - defer services.Delete(hash) + services.Save(&service) + service.Id = "" // TODO(ilgooz) remove this when Service type created by hand. + defer services.Delete(service.Id) _, err := serveremit.EmitEvent(context.Background(), &EmitEventRequest{ Token: service.Hash(), EventKey: "test", @@ -52,8 +54,9 @@ func TestEmitNoData(t *testing.T) { func TestEmitWrongData(t *testing.T) { service := service.Service{} - hash, _ := services.Save(&service) - defer services.Delete(hash) + services.Save(&service) + service.Id = "" // TODO(ilgooz) remove this when Service type created by hand. + defer services.Delete(service.Id) _, err := serveremit.EmitEvent(context.Background(), &EmitEventRequest{ Token: service.Hash(), EventKey: "test", @@ -64,8 +67,9 @@ func TestEmitWrongData(t *testing.T) { func TestEmitWrongEvent(t *testing.T) { srv := service.Service{Name: "TestEmitWrongEvent"} - hash, _ := services.Save(&srv) - defer services.Delete(hash) + services.Save(&srv) + srv.Id = "" // TODO(ilgooz) remove this when Service type created by hand. + defer services.Delete(srv.Id) _, err := serveremit.EmitEvent(context.Background(), &EmitEventRequest{ Token: srv.Hash(), EventKey: "test", diff --git a/cmd/service/deploy.go b/cmd/service/deploy.go index 59c73fb5b..79b2bdb98 100644 --- a/cmd/service/deploy.go +++ b/cmd/service/deploy.go @@ -3,7 +3,12 @@ package service import ( "context" "fmt" + "io" + "os" + "github.com/asaskevich/govalidator" + "github.com/briandowns/spinner" + "github.com/docker/docker/pkg/archive" "github.com/logrusorgru/aurora" "github.com/mesg-foundation/core/api/core" "github.com/mesg-foundation/core/cmd/utils" @@ -24,11 +29,128 @@ To get more information, see the [deploy page from the documentation](https://do } func deployHandler(cmd *cobra.Command, args []string) { - service := prepareService(defaultPath(args)) - reply, err := cli().DeployService(context.Background(), &core.DeployServiceRequest{ - Service: service, - }) + path := defaultPath(args) + serviceID, isValid, err := deployService(path) + if !isValid { + os.Exit(1) + } utils.HandleError(err) - fmt.Println("Service deployed with ID:", aurora.Green(reply.ServiceID)) - fmt.Println("To start it, run the command: mesg-core service start " + reply.ServiceID) + + fmt.Println("Service deployed with ID:", aurora.Green(serviceID)) + fmt.Printf("To start it, run the command: mesg-core service start %s\n", serviceID) + +} + +func deployService(path string) (serviceID string, isValid bool, err error) { + stream, err := cli().DeployService(context.Background()) + if err != nil { + return "", true, err + } + + deployment := make(chan deploymentResult) + go readDeployReply(stream, deployment) + + if govalidator.IsURL(path) { + if err := stream.Send(&core.DeployServiceRequest{ + Value: &core.DeployServiceRequest_Url{Url: path}, + }); err != nil { + return "", true, err + } + } else { + if err := deployServiceSendServiceContext(path, stream); err != nil { + return "", true, err + } + } + + if err := stream.CloseSend(); err != nil { + return "", true, err + } + + result := <-deployment + return result.serviceID, result.isValid, result.err +} + +func deployServiceSendServiceContext(path string, stream core.Core_DeployServiceClient) error { + archive, err := archive.TarWithOptions(path, &archive.TarOptions{ + Compression: archive.Gzip, + }) + if err != nil { + return err + } + + buf := make([]byte, 1024) + for { + n, err := archive.Read(buf) + if err == io.EOF { + break + } + if err != nil { + return err + } + + if err := stream.Send(&core.DeployServiceRequest{ + Value: &core.DeployServiceRequest_Chunk{Chunk: buf[:n]}, + }); err != nil { + return err + } + } + + return nil +} + +type deploymentResult struct { + serviceID string + err error + isValid bool +} + +func readDeployReply(stream core.Core_DeployServiceClient, deployment chan deploymentResult) { + var ( + sp *spinner.Spinner + result = deploymentResult{isValid: true} + ) + + for { + message, err := stream.Recv() + if err != nil { + result.err = err + deployment <- result + return + } + + var ( + status = message.GetStatus() + serviceID = message.GetServiceID() + validationError = message.GetValidationError() + ) + + switch { + case status != nil: + switch status.Type { + case core.DeployServiceReply_Status_RUNNING: + sp = utils.StartSpinner(utils.SpinnerOptions{Text: status.Message}) + + case core.DeployServiceReply_Status_DONE: + sp.Stop() + fmt.Println(status.Message) + } + + case serviceID != "": + sp.Stop() + + result.serviceID = serviceID + deployment <- result + return + + case validationError != "": + sp.Stop() + + fmt.Println(aurora.Red(validationError)) + fmt.Println("Run the command 'service validate' for more details") + + result.isValid = false + deployment <- result + return + } + } } diff --git a/cmd/service/dev.go b/cmd/service/dev.go index 91ddf22e3..9c6210132 100644 --- a/cmd/service/dev.go +++ b/cmd/service/dev.go @@ -3,6 +3,7 @@ package service import ( "context" "fmt" + "os" "github.com/logrusorgru/aurora" "github.com/mesg-foundation/core/api/core" @@ -28,7 +29,10 @@ func init() { } func devHandler(cmd *cobra.Command, args []string) { - serviceID, err := createService(defaultPath(args)) + serviceID, isValid, err := createService(defaultPath(args)) + if !isValid { + os.Exit(1) + } utils.HandleError(err) fmt.Printf("%s Service started with success\n", aurora.Green("✔")) fmt.Printf("Service ID: %s\n", aurora.Bold(serviceID)) @@ -48,20 +52,18 @@ func devHandler(cmd *cobra.Command, args []string) { }) } -func createService(path string) (string, error) { - service := prepareService(path) - deployment, err := cli().DeployService(context.Background(), &core.DeployServiceRequest{ - Service: service, - }) - if err != nil { - return "", err +func createService(path string) (serviceID string, isValid bool, err error) { + serviceID, isValid, err = deployService(path) + if !isValid || err != nil { + return "", isValid, err } + utils.ShowSpinnerForFunc(utils.SpinnerOptions{Text: "Starting service..."}, func() { _, err = cli().StartService(context.Background(), &core.StartServiceRequest{ - ServiceID: deployment.ServiceID, + ServiceID: serviceID, }) }) - return deployment.ServiceID, err + return serviceID, true, err } func listenEvents(serviceID string, filter string) { diff --git a/cmd/service/utils.go b/cmd/service/utils.go index 13092598f..1e809d6f6 100644 --- a/cmd/service/utils.go +++ b/cmd/service/utils.go @@ -3,17 +3,13 @@ package service import ( "fmt" "io/ioutil" - "log" "net/url" "os" - "github.com/asaskevich/govalidator" "github.com/logrusorgru/aurora" "github.com/mesg-foundation/core/api/core" "github.com/mesg-foundation/core/cmd/utils" "github.com/mesg-foundation/core/config" - "github.com/mesg-foundation/core/container" - "github.com/mesg-foundation/core/service" "github.com/mesg-foundation/core/service/importer" "github.com/spf13/viper" "google.golang.org/grpc" @@ -21,18 +17,6 @@ import ( "gopkg.in/src-d/go-git.v4/plumbing" ) -// TODO(ilgooz): remove this after service package made Newable. -var defaultContainer *container.Container - -// TODO(ilgooz): remove this after service package made Newable. -func init() { - c, err := container.New() - if err != nil { - log.Fatal(err) - } - defaultContainer = c -} - func cli() core.CoreClient { connection, err := grpc.Dial(viper.GetString(config.APIClientTarget), grpc.WithInsecure()) utils.HandleError(err) @@ -55,38 +39,6 @@ func handleValidationError(err error) { } } -// prepareService downloads if needed, create the service, build it and inject configuration -func prepareService(path string) *service.Service { - path, didDownload, err := downloadServiceIfNeeded(path) - utils.HandleError(err) - if didDownload { - defer os.RemoveAll(path) - fmt.Printf("%s Service downloaded with success\n", aurora.Green("✔")) - } - importedService, err := importer.From(path) - handleValidationError(err) - utils.HandleError(err) - imageHash, err := buildDockerImage(path) - utils.HandleError(err) - fmt.Printf("%s Image built with success\n", aurora.Green("✔")) - injectConfigurationInDependencies(importedService, imageHash) - return importedService -} - -func downloadServiceIfNeeded(path string) (newPath string, didDownload bool, err error) { - if !govalidator.IsURL(path) { - return path, false, nil - } - newPath, err = createTempFolder() - if err != nil { - return "", false, err - } - if err := gitClone(path, newPath, "Downloading service..."); err != nil { - return "", false, err - } - return newPath, true, nil -} - func gitClone(repoURL string, path string, message string) error { u, err := url.Parse(repoURL) if err != nil { @@ -110,28 +62,3 @@ func gitClone(repoURL string, path string, message string) error { func createTempFolder() (path string, err error) { return ioutil.TempDir("", "mesg-") } - -func buildDockerImage(path string) (imageHash string, err error) { - utils.ShowSpinnerForFunc(utils.SpinnerOptions{Text: "Building image..."}, func() { - imageHash, err = defaultContainer.Build(path) - }) - return -} - -func injectConfigurationInDependencies(s *service.Service, imageHash string) { - config := s.Configuration - if config == nil { - config = &service.Dependency{} - } - dependency := &service.Dependency{ - Command: config.Command, - Ports: config.Ports, - Volumes: config.Volumes, - Volumesfrom: config.Volumesfrom, - Image: imageHash, - } - if s.Dependencies == nil { - s.Dependencies = make(map[string]*service.Dependency) - } - s.Dependencies["service"] = dependency -} diff --git a/cmd/service/utils_test.go b/cmd/service/utils_test.go index 3cf62bd56..cf872084c 100644 --- a/cmd/service/utils_test.go +++ b/cmd/service/utils_test.go @@ -1,12 +1,9 @@ package service import ( - "os" "testing" - "github.com/mesg-foundation/core/service" "github.com/stretchr/testify/require" - git "gopkg.in/src-d/go-git.v4" ) func TestDefaultPath(t *testing.T) { @@ -14,116 +11,3 @@ func TestDefaultPath(t *testing.T) { require.Equal(t, defaultPath([]string{"foo"}), "foo") require.Equal(t, defaultPath([]string{"foo", "bar"}), "foo") } - -func TestBuildDockerImagePathDoNotExist(t *testing.T) { - _, err := buildDockerImage("/doNotExist") - require.NotNil(t, err) -} - -func TestGitCloneRepositoryDoNotExist(t *testing.T) { - path, _ := createTempFolder() - defer os.RemoveAll(path) - err := gitClone("/doNotExist", path, "testing...") - require.NotNil(t, err) -} - -func TestGitCloneWithoutURLSchema(t *testing.T) { - path, _ := createTempFolder() - defer os.RemoveAll(path) - err := gitClone("github.com/mesg-foundation/awesome.git", path, "testing...") - require.Nil(t, err) -} - -func TestGitCloneCustomBranch(t *testing.T) { - branchName := "5-generic-service" - path, _ := createTempFolder() - defer os.RemoveAll(path) - err := gitClone("github.com/mesg-foundation/service-ethereum-erc20#"+branchName, path, "testing...") - require.Nil(t, err) - repo, err := git.PlainOpen(path) - require.Nil(t, err) - branch, err := repo.Branch(branchName) - require.Nil(t, err) - require.NotNil(t, branch) -} - -func TestDownloadServiceIfNeededAbsolutePath(t *testing.T) { - path := "/users/paul/service-js-function" - newPath, didDownload, err := downloadServiceIfNeeded(path) - require.Nil(t, err) - require.Equal(t, path, newPath) - require.Equal(t, false, didDownload) -} - -func TestDownloadServiceIfNeededRelativePath(t *testing.T) { - path := "./service-js-function" - newPath, didDownload, err := downloadServiceIfNeeded(path) - require.Nil(t, err) - require.Equal(t, path, newPath) - require.Equal(t, false, didDownload) -} - -func TestDownloadServiceIfNeededUrl(t *testing.T) { - path := "https://github.com/mesg-foundation/awesome.git" - newPath, didDownload, err := downloadServiceIfNeeded(path) - defer os.RemoveAll(newPath) - require.Nil(t, err) - require.NotEqual(t, path, newPath) - require.Equal(t, true, didDownload) -} - -func TestCreateTempFolder(t *testing.T) { - path, err := createTempFolder() - defer os.RemoveAll(path) - require.Nil(t, err) - require.NotEqual(t, "", path) -} - -func TestRemoveTempFolder(t *testing.T) { - path, _ := createTempFolder() - err := os.RemoveAll(path) - require.Nil(t, err) -} - -func TestInjectConfigurationInDependencies(t *testing.T) { - s := &service.Service{} - injectConfigurationInDependencies(s, "TestInjectConfigurationInDependencies") - require.Equal(t, s.Dependencies["service"].Image, "TestInjectConfigurationInDependencies") -} - -func TestInjectConfigurationInDependenciesWithConfig(t *testing.T) { - s := &service.Service{ - Configuration: &service.Dependency{ - Command: "xxx", - Image: "yyy", - }, - } - injectConfigurationInDependencies(s, "TestInjectConfigurationInDependenciesWithConfig") - require.Equal(t, s.Dependencies["service"].Image, "TestInjectConfigurationInDependenciesWithConfig") - require.Equal(t, s.Dependencies["service"].Command, "xxx") -} - -func TestInjectConfigurationInDependenciesWithDependency(t *testing.T) { - s := &service.Service{ - Dependencies: map[string]*service.Dependency{ - "test": { - Image: "xxx", - }, - }, - } - injectConfigurationInDependencies(s, "TestInjectConfigurationInDependenciesWithDependency") - require.Equal(t, s.Dependencies["service"].Image, "TestInjectConfigurationInDependenciesWithDependency") - require.Equal(t, s.Dependencies["test"].Image, "xxx") -} - -func TestInjectConfigurationInDependenciesWithDependencyOverride(t *testing.T) { - s := &service.Service{ - Dependencies: map[string]*service.Dependency{ - "service": { - Image: "xxx", - }, - }, - } - injectConfigurationInDependencies(s, "TestInjectConfigurationInDependenciesWithDependencyOverride") - require.Equal(t, s.Dependencies["service"].Image, "TestInjectConfigurationInDependenciesWithDependencyOverride") -} diff --git a/cmd/utils/spinner.go b/cmd/utils/spinner.go index b702593f5..6ca22d3e0 100644 --- a/cmd/utils/spinner.go +++ b/cmd/utils/spinner.go @@ -3,7 +3,15 @@ package utils import ( "time" - spinnerPkg "github.com/briandowns/spinner" + "github.com/briandowns/spinner" +) + +var ( + // spinnerCharset is the default animation. + spinnerCharset = spinner.CharSets[11] + + // spinnerDuration is the default duration for spinning. + spinnerDuration = 100 * time.Millisecond ) // SpinnerOptions contains all details for the spinner @@ -13,8 +21,8 @@ type SpinnerOptions struct { } // StartSpinner creates new spinner for terminal. -func StartSpinner(opts SpinnerOptions) (spinner *spinnerPkg.Spinner) { - spinner = spinnerPkg.New(spinnerPkg.CharSets[11], 100*time.Millisecond) +func StartSpinner(opts SpinnerOptions) *spinner.Spinner { + spinner := spinner.New(spinnerCharset, spinnerDuration) spinner.Start() if opts.Color != "" { spinner.Color(opts.Color) @@ -22,7 +30,7 @@ func StartSpinner(opts SpinnerOptions) (spinner *spinnerPkg.Spinner) { if opts.Text != "" { spinner.Suffix = " " + opts.Text } - return + return spinner } // ShowSpinnerForFunc shows a spinner during the execution of the function. @@ -30,5 +38,4 @@ func ShowSpinnerForFunc(opts SpinnerOptions, function func()) { s := StartSpinner(opts) defer s.Stop() function() - return } diff --git a/database/services/all_test.go b/database/services/all_test.go index ab4a1d7a3..9841478ed 100644 --- a/database/services/all_test.go +++ b/database/services/all_test.go @@ -8,8 +8,9 @@ import ( ) func TestAll(t *testing.T) { - hash, _ := Save(&service.Service{Name: "Service1"}) - defer Delete(hash) + service := &service.Service{Name: "Service1"} + Save(service) + defer Delete(service.Id) services, err := All() founded := false for _, s := range services { diff --git a/database/services/db_test.go b/database/services/db_test.go index f2bcb12be..fc3424346 100644 --- a/database/services/db_test.go +++ b/database/services/db_test.go @@ -22,12 +22,12 @@ func TestConcurrency(t *testing.T) { service := &service.Service{ Name: "TestConcurrency", } - hash, _ := Save(service) - defer Delete(hash) + Save(service) + defer Delete(service.Id) for i := 0; i < 5; i++ { wg.Add(1) go func() { - s, err := Get(hash) + s, err := Get(service.Id) require.Nil(t, err) require.Equal(t, s.Name, service.Name) wg.Done() diff --git a/database/services/delete_test.go b/database/services/delete_test.go index b57c9b90b..549e315bf 100644 --- a/database/services/delete_test.go +++ b/database/services/delete_test.go @@ -8,9 +8,10 @@ import ( ) func TestDelete(t *testing.T) { - hash, _ := Save(&service.Service{ + service := &service.Service{ Name: "TestDelete", - }) - err := Delete(hash) + } + Save(service) + err := Delete(service.Id) require.Nil(t, err) } diff --git a/database/services/get_test.go b/database/services/get_test.go index 8559e5b5a..7a37e5d82 100644 --- a/database/services/get_test.go +++ b/database/services/get_test.go @@ -8,13 +8,14 @@ import ( ) func TestGet(t *testing.T) { - hash, _ := Save(&service.Service{ + service := &service.Service{ Name: "TestGet", - }) - defer Delete(hash) - service, err := Get(hash) + } + Save(service) + defer Delete(service.Id) + srv, err := Get(service.Id) require.Nil(t, err) - require.Equal(t, service.Name, "TestGet") + require.Equal(t, srv.Name, "TestGet") } func TestGetMissing(t *testing.T) { diff --git a/database/services/save.go b/database/services/save.go index 7487ec192..758b7cf97 100644 --- a/database/services/save.go +++ b/database/services/save.go @@ -6,16 +6,17 @@ import ( ) // Save stores a service in the database and returns a hash or an error. -func Save(service *service.Service) (hash string, err error) { +func Save(service *service.Service) error { bytes, err := proto.Marshal(service) if err != nil { - return "", err + return err } db, err := open() defer close() if err != nil { - return "", err + return err } - hash = service.Hash() - return hash, db.Put([]byte(hash), bytes, nil) + hash := service.Hash() + service.Id = hash + return db.Put([]byte(hash), bytes, nil) } diff --git a/database/services/save_test.go b/database/services/save_test.go index 2d0de58ea..cf43423da 100644 --- a/database/services/save_test.go +++ b/database/services/save_test.go @@ -12,17 +12,19 @@ func TestSaveReturningHash(t *testing.T) { Name: "TestSaveReturningHash", } calculatedHash := service.Hash() - hash, err := Save(service) - defer Delete(hash) + + err := Save(service) + defer Delete(service.Id) require.Nil(t, err) - require.Equal(t, hash, calculatedHash) + require.Equal(t, calculatedHash, service.Id) } func TestSaveAndPresentInDB(t *testing.T) { - hash, _ := Save(&service.Service{ + service := &service.Service{ Name: "TestSaveAndPresentInDB", - }) - defer Delete(hash) - service, _ := Get(hash) - require.Equal(t, service.Name, "TestSaveAndPresentInDB") + } + Save(service) + defer Delete(service.Id) + srv, _ := Get(service.Id) + require.Equal(t, srv.Name, "TestSaveAndPresentInDB") } diff --git a/dev-core b/dev-core index 876047157..fcc299f74 100755 --- a/dev-core +++ b/dev-core @@ -4,7 +4,8 @@ echo "Building mesg/core:local..." docker build -t mesg/core:local --build-arg version="local" . -./dev-cli stop -./dev-cli start +./dev-cli stop && \ +./dev-cli start && \ ./dev-cli logs + ./dev-cli stop \ No newline at end of file diff --git a/docs/api/core.md b/docs/api/core.md index 5f2c2e56e..3809fec51 100644 --- a/docs/api/core.md +++ b/docs/api/core.md @@ -51,6 +51,8 @@ Subscribe to a stream that listens for events from a service. + + @@ -101,6 +103,8 @@ The request's data for the `ListenEvent` stream's API. + + #### EventData The data received from the stream of the `ListenEvent` API. The data will be received over time as long as the stream is open. @@ -182,6 +186,8 @@ Subscribe to a stream that listens for task's result from a service. + + @@ -249,6 +255,8 @@ The request's data for the `ListenResult` stream API. + + @@ -312,6 +320,8 @@ Execute a service's task through [Core](../guide/start-here/core.md). + + #### ExecuteTaskRequest The request's data for the `ExecuteTask` API. @@ -375,6 +385,8 @@ The request's data for the `ExecuteTask` API. + + #### ExecuteTaskReply The reply's data of the `ExecuteTask` API. @@ -457,6 +469,8 @@ Start a service. The service must be already deployed to [Core](../guide/start-h + + @@ -514,6 +528,8 @@ The request's data for the `StartService` API. + + @@ -573,6 +589,8 @@ Stop a service. The service must be already deployed to [Core](../guide/start-he + + @@ -630,6 +648,8 @@ The request's data for the `StopService` API. + + @@ -660,6 +680,8 @@ Deploy a service to [Core](../guide/start-here/core.md). This will give you an u + + #### DeployServiceRequest The request's data for `DeployService` API. @@ -696,7 +718,10 @@ The request's data for `DeployService` API. | Field | Type | Description | | ----- | ---- | ----------- | -| service | [service.Service](#service.Service) | The service's definition to deploy. [Details here](./service-type.md). | +| url | [string](#string) | Git repo url of service. If url provided, stream will be closed after +first receive. | +| chunk | [bytes](#bytes) | Chunks of gzipped tar archive of service. If chunk provided, stream will +be closed after all chunks sent. | @@ -753,7 +778,13 @@ The reply's data of `DeployService` API. | Field | Type | Description | | ----- | ---- | ----------- | -| serviceID | [string](#string) | The generated identifier of the deployed service. Use this ID with other APIs. | +| status | [DeployServiceReply.Status](#api.DeployServiceReply.Status) | status will be sent after each status change. | +| serviceID | [string](#string) | serviceID will be sent as the last message of stream when +service deployed successfully. | +| validationError | [string](#string) | validationError will be sent as the last message of stream when +there is a validation error. | + + @@ -851,6 +882,8 @@ Request's data of the `DeleteService` API. + + @@ -898,6 +931,8 @@ Reply of `DeleteService` API doesn't contain any data. + + @@ -930,6 +965,8 @@ List all services already deployed in [Core](../guide/start-here/core.md). + + @@ -975,6 +1012,8 @@ Reply of `ListServices` API doesn't contain any data. + + @@ -1063,6 +1102,8 @@ Get the definition of an already-deployed service from its ID. + + #### GetServiceRequest The request's data for the `GetService` API. @@ -1121,6 +1162,8 @@ The request's data for the `GetService` API. + + #### GetServiceReply The reply's data of the `GetService` API. diff --git a/docs/api/service-type.md b/docs/api/service-type.md index 4178e5830..c6bd07bfa 100644 --- a/docs/api/service-type.md +++ b/docs/api/service-type.md @@ -34,13 +34,14 @@ This is the definition of a MESG Service. | Field | Type | Description | | ----- | ---- | ----------- | -| name | [string](#string) | The service's name. | -| description | [string](#string) | The service's description. | +| id | [string](#string) | Service's unique id hash. | +| name | [string](#string) | Service's name. | +| description | [string](#string) | Service's description. | | tasks | [Service.TasksEntry](#service.Service.TasksEntry)[] | The list of tasks this service can execute. | | events | [Service.EventsEntry](#service.Service.EventsEntry)[] | The list of events this service can emit. | | dependencies | [Service.DependenciesEntry](#service.Service.DependenciesEntry)[] | The Docker dependencies this service requires. | -| configuration | [Dependency](#service.Dependency) | The service's Docker configuration. | -| repository | [string](#string) | The service's repository that contain its source code. | +| configuration | [Dependency](#service.Dependency) | Service's Docker configuration. | +| repository | [string](#string) | Service's repository that contain its source code. | diff --git a/mesg/deploy.go b/mesg/deploy.go new file mode 100644 index 000000000..14bc53752 --- /dev/null +++ b/mesg/deploy.go @@ -0,0 +1,39 @@ +package mesg + +import ( + "io" + + "github.com/mesg-foundation/core/service" + "github.com/mesg-foundation/core/service/importer" +) + +// DeployServiceOption is a configuration func for Deploy methods. +type DeployServiceOption func(*serviceDeployer) + +// DeployServiceStatusOption receives chan statuses to send deploy statuses. +func DeployServiceStatusOption(statuses chan DeployStatus) DeployServiceOption { + return func(deployer *serviceDeployer) { + deployer.Statuses = statuses + } +} + +// DeployService deploys a service from a gzipped tarball. +func (m *MESG) DeployService(r io.Reader, options ...DeployServiceOption) (*service.Service, *importer.ValidationError, error) { + deployer := newServiceDeployer(m) + for _, option := range options { + option(deployer) + } + return deployer.FromGzippedTar(r) +} + +// DeployServiceFromURL deploys a service living at a Git host. +// Supported URL types: +// - https://github.com/mesg-foundation/service-ethereum +// - https://github.com/mesg-foundation/service-ethereum#branchName +func (m *MESG) DeployServiceFromURL(url string, options ...DeployServiceOption) (*service.Service, *importer.ValidationError, error) { + deployer := newServiceDeployer(m) + for _, option := range options { + option(deployer) + } + return deployer.FromGitURL(url) +} diff --git a/mesg/deploy_deployer.go b/mesg/deploy_deployer.go new file mode 100644 index 000000000..3b9357b8e --- /dev/null +++ b/mesg/deploy_deployer.go @@ -0,0 +1,163 @@ +package mesg + +import ( + "fmt" + "io" + "io/ioutil" + "net/url" + "os" + + "github.com/docker/docker/pkg/archive" + "github.com/logrusorgru/aurora" + "github.com/mesg-foundation/core/database/services" + "github.com/mesg-foundation/core/service" + "github.com/mesg-foundation/core/service/importer" + uuid "github.com/satori/go.uuid" + git "gopkg.in/src-d/go-git.v4" + "gopkg.in/src-d/go-git.v4/plumbing" +) + +type serviceDeployer struct { + Statuses chan DeployStatus + + mesg *MESG +} + +// StatusType indicates the type of status message. +type StatusType int + +const ( + // RUNNING indicates that status message belongs to an active state. + RUNNING StatusType = iota + 1 + + // DONE indicates that status message belongs to completed state. + DONE +) + +// DeployStatus represents the deployment status. +type DeployStatus struct { + Message string + Type StatusType +} + +func newServiceDeployer(mesg *MESG) *serviceDeployer { + return &serviceDeployer{ + mesg: mesg, + } +} + +// FromGitURL deploys a service hosted at a Git url. +func (d *serviceDeployer) FromGitURL(url string) (*service.Service, *importer.ValidationError, error) { + d.sendStatus("Downloading service...", RUNNING) + path, err := d.createTempDir() + if err != nil { + return nil, nil, err + } + if err := d.gitClone(url, path); err != nil { + return nil, nil, err + } + d.sendStatus(fmt.Sprintf("%s Service downloaded with success.", aurora.Green("✔")), DONE) + return d.deploy(path) +} + +// FromGzippedTar deploys a service from a gzipped tarball. +func (d *serviceDeployer) FromGzippedTar(r io.Reader) (*service.Service, *importer.ValidationError, error) { + d.sendStatus("Sending service context to core daemon...", RUNNING) + path, err := d.createTempDir() + if err != nil { + return nil, nil, err + } + if err := archive.Untar(r, path, &archive.TarOptions{ + Compression: archive.Gzip, + }); err != nil { + return nil, nil, err + } + d.sendStatus(fmt.Sprintf("%s Service context sent to core daemon with success.", aurora.Green("✔")), DONE) + return d.deploy(path) +} + +// gitClone clones a repo hosted at repoURL to path. +func (d *serviceDeployer) gitClone(repoURL string, path string) error { + u, err := url.Parse(repoURL) + if err != nil { + return err + } + if u.Scheme == "" { + u.Scheme = "https" + } + options := &git.CloneOptions{} + if u.Fragment != "" { + options.ReferenceName = plumbing.ReferenceName("refs/heads/" + u.Fragment) + u.Fragment = "" + } + options.URL = u.String() + _, err = git.PlainClone(path, false, options) + return err +} + +// deploy deploys a service in path. +func (d *serviceDeployer) deploy(path string) (*service.Service, *importer.ValidationError, error) { + defer os.RemoveAll(path) + + service, err := importer.From(path) + validationErr, err := d.assertValidationError(err) + if err != nil { + return nil, nil, err + } + if validationErr != nil { + return nil, validationErr, nil + } + + d.sendStatus("Building Docker image...", RUNNING) + imageHash, err := d.mesg.container.Build(path) + if err != nil { + return nil, nil, err + } + d.sendStatus(fmt.Sprintf("%s Image built with success.", aurora.Green("✔")), DONE) + d.injectConfigurationInDependencies(service, imageHash) + + d.sendStatus(fmt.Sprintf("%s Completed.", aurora.Green("✔")), DONE) + return service, nil, services.Save(service) +} + +func (d *serviceDeployer) injectConfigurationInDependencies(s *service.Service, imageHash string) { + config := s.Configuration + if config == nil { + config = &service.Dependency{} + } + dependency := &service.Dependency{ + Command: config.Command, + Ports: config.Ports, + Volumes: config.Volumes, + Volumesfrom: config.Volumesfrom, + Image: imageHash, + } + if s.Dependencies == nil { + s.Dependencies = make(map[string]*service.Dependency) + } + s.Dependencies["service"] = dependency +} + +func (d *serviceDeployer) createTempDir() (path string, err error) { + return ioutil.TempDir("", "mesg-"+uuid.NewV4().String()) +} + +// sendStatus sends a status message. +func (d *serviceDeployer) sendStatus(message string, typ StatusType) { + if d.Statuses != nil { + d.Statuses <- DeployStatus{ + Message: message, + Type: typ, + } + } +} + +func (d *serviceDeployer) assertValidationError(err error) (*importer.ValidationError, error) { + if err == nil { + return nil, nil + } + if validationError, ok := err.(*importer.ValidationError); ok { + return validationError, nil + } + return nil, err +} diff --git a/mesg/deploy_deployer_test.go b/mesg/deploy_deployer_test.go new file mode 100644 index 000000000..32fd78464 --- /dev/null +++ b/mesg/deploy_deployer_test.go @@ -0,0 +1,120 @@ +package mesg + +import ( + "os" + "testing" + + "github.com/mesg-foundation/core/service" + "github.com/stretchr/testify/require" + git "gopkg.in/src-d/go-git.v4" +) + +func TestGitCloneRepositoryDoNotExist(t *testing.T) { + m, _ := newMESGAndDockerTest(t) + deployer := newServiceDeployer(m) + + path, _ := deployer.createTempDir() + defer os.RemoveAll(path) + err := deployer.gitClone("/doNotExist", path) + require.NotNil(t, err) +} + +func TestGitCloneWithoutURLSchema(t *testing.T) { + m, _ := newMESGAndDockerTest(t) + deployer := newServiceDeployer(m) + + path, _ := deployer.createTempDir() + defer os.RemoveAll(path) + err := deployer.gitClone("github.com/mesg-foundation/awesome.git", path) + require.Nil(t, err) +} + +func TestGitCloneCustomBranch(t *testing.T) { + m, _ := newMESGAndDockerTest(t) + deployer := newServiceDeployer(m) + + branchName := "5-generic-service" + path, _ := deployer.createTempDir() + defer os.RemoveAll(path) + err := deployer.gitClone("github.com/mesg-foundation/service-ethereum-erc20#"+branchName, path) + require.Nil(t, err) + repo, err := git.PlainOpen(path) + require.Nil(t, err) + branch, err := repo.Branch(branchName) + require.Nil(t, err) + require.NotNil(t, branch) +} + +func TestCreateTempFolder(t *testing.T) { + m, _ := newMESGAndDockerTest(t) + deployer := newServiceDeployer(m) + + path, err := deployer.createTempDir() + defer os.RemoveAll(path) + require.Nil(t, err) + require.NotEqual(t, "", path) +} + +func TestRemoveTempFolder(t *testing.T) { + m, _ := newMESGAndDockerTest(t) + deployer := newServiceDeployer(m) + + path, _ := deployer.createTempDir() + err := os.RemoveAll(path) + require.Nil(t, err) +} + +func TestInjectConfigurationInDependencies(t *testing.T) { + m, _ := newMESGAndDockerTest(t) + deployer := newServiceDeployer(m) + + s := &service.Service{} + deployer.injectConfigurationInDependencies(s, "TestInjectConfigurationInDependencies") + require.Equal(t, s.Dependencies["service"].Image, "TestInjectConfigurationInDependencies") +} + +func TestInjectConfigurationInDependenciesWithConfig(t *testing.T) { + m, _ := newMESGAndDockerTest(t) + deployer := newServiceDeployer(m) + + s := &service.Service{ + Configuration: &service.Dependency{ + Command: "xxx", + Image: "yyy", + }, + } + deployer.injectConfigurationInDependencies(s, "TestInjectConfigurationInDependenciesWithConfig") + require.Equal(t, s.Dependencies["service"].Image, "TestInjectConfigurationInDependenciesWithConfig") + require.Equal(t, s.Dependencies["service"].Command, "xxx") +} + +func TestInjectConfigurationInDependenciesWithDependency(t *testing.T) { + m, _ := newMESGAndDockerTest(t) + deployer := newServiceDeployer(m) + + s := &service.Service{ + Dependencies: map[string]*service.Dependency{ + "test": { + Image: "xxx", + }, + }, + } + deployer.injectConfigurationInDependencies(s, "TestInjectConfigurationInDependenciesWithDependency") + require.Equal(t, s.Dependencies["service"].Image, "TestInjectConfigurationInDependenciesWithDependency") + require.Equal(t, s.Dependencies["test"].Image, "xxx") +} + +func TestInjectConfigurationInDependenciesWithDependencyOverride(t *testing.T) { + m, _ := newMESGAndDockerTest(t) + deployer := newServiceDeployer(m) + + s := &service.Service{ + Dependencies: map[string]*service.Dependency{ + "service": { + Image: "xxx", + }, + }, + } + deployer.injectConfigurationInDependencies(s, "TestInjectConfigurationInDependenciesWithDependencyOverride") + require.Equal(t, s.Dependencies["service"].Image, "TestInjectConfigurationInDependenciesWithDependencyOverride") +} diff --git a/mesg/deploy_test.go b/mesg/deploy_test.go new file mode 100644 index 000000000..05512ec9b --- /dev/null +++ b/mesg/deploy_test.go @@ -0,0 +1,139 @@ +package mesg + +import ( + "fmt" + "io/ioutil" + "strings" + "sync" + "testing" + + "github.com/cnf/structhash" + "github.com/docker/docker/pkg/archive" + "github.com/logrusorgru/aurora" + "github.com/mesg-foundation/core/service/importer" + "github.com/stretchr/testify/require" +) + +func TestDeployService(t *testing.T) { + path := "./service-test" + + mesg, dt := newMESGAndDockerTest(t) + dt.ProvideImageBuild(ioutil.NopCloser(strings.NewReader(`{"stream":"sha256:x"}`)), nil) + + statuses := make(chan DeployStatus) + var wg sync.WaitGroup + + wg.Add(1) + go func() { + defer wg.Done() + + archive, err := archive.TarWithOptions(path, &archive.TarOptions{ + Compression: archive.Gzip, + }) + require.Nil(t, err) + + service, validationError, err := mesg.DeployService(archive, DeployServiceStatusOption(statuses)) + require.Nil(t, validationError) + require.Nil(t, err) + require.Equal(t, 1, structhash.Version(service.Id)) + }() + + status := <-statuses + require.Equal(t, RUNNING, status.Type) + require.Equal(t, "Sending service context to core daemon...", status.Message) + + status = <-statuses + require.Equal(t, DONE, status.Type) + require.Equal(t, fmt.Sprintf("%s Service context sent to core daemon with success.", aurora.Green("✔")), status.Message) + + status = <-statuses + require.Equal(t, RUNNING, status.Type) + require.Equal(t, "Building Docker image...", status.Message) + + status = <-statuses + require.Equal(t, DONE, status.Type) + require.Equal(t, fmt.Sprintf("%s Image built with success.", aurora.Green("✔")), status.Message) + + status = <-statuses + require.Equal(t, DONE, status.Type) + require.Equal(t, fmt.Sprintf("%s Completed.", aurora.Green("✔")), status.Message) + + wg.Wait() +} + +func TestDeployInvalidService(t *testing.T) { + path := "./service-test-invalid" + + mesg, dt := newMESGAndDockerTest(t) + dt.ProvideImageBuild(ioutil.NopCloser(strings.NewReader(`{"stream":"sha256:x"}`)), nil) + + statuses := make(chan DeployStatus) + var wg sync.WaitGroup + + wg.Add(1) + go func() { + defer wg.Done() + + archive, err := archive.TarWithOptions(path, &archive.TarOptions{ + Compression: archive.Gzip, + }) + require.Nil(t, err) + + service, validationError, err := mesg.DeployService(archive, DeployServiceStatusOption(statuses)) + require.Nil(t, service) + require.Nil(t, err) + require.Equal(t, (&importer.ValidationError{}).Error(), validationError.Error()) + }() + + require.Equal(t, "Sending service context to core daemon...", (<-statuses).Message) + require.Equal(t, fmt.Sprintf("%s Service context sent to core daemon with success.", aurora.Green("✔")), (<-statuses).Message) + + select { + case <-statuses: + t.Error("should not send further status messages") + default: + } + + wg.Wait() +} + +func TestDeployServiceFromURL(t *testing.T) { + url := "https://github.com/mesg-foundation/service-webhook" + + mesg, dt := newMESGAndDockerTest(t) + dt.ProvideImageBuild(ioutil.NopCloser(strings.NewReader(`{"stream":"sha256:x"}`)), nil) + + statuses := make(chan DeployStatus) + var wg sync.WaitGroup + + wg.Add(1) + go func() { + defer wg.Done() + service, validationError, err := mesg.DeployServiceFromURL(url, DeployServiceStatusOption(statuses)) + require.Nil(t, validationError) + require.Nil(t, err) + require.Equal(t, 1, structhash.Version(service.Id)) + }() + + status := <-statuses + require.Equal(t, RUNNING, status.Type) + require.Equal(t, "Downloading service...", status.Message) + + status = <-statuses + require.Equal(t, DONE, status.Type) + require.Equal(t, fmt.Sprintf("%s Service downloaded with success.", aurora.Green("✔")), status.Message) + + status = <-statuses + require.Equal(t, RUNNING, status.Type) + require.Equal(t, "Building Docker image...", status.Message) + + status = <-statuses + require.Equal(t, DONE, status.Type) + require.Equal(t, fmt.Sprintf("%s Image built with success.", aurora.Green("✔")), status.Message) + + status = <-statuses + require.Equal(t, DONE, status.Type) + require.Equal(t, fmt.Sprintf("%s Completed.", aurora.Green("✔")), status.Message) + + wg.Wait() +} diff --git a/mesg/mesg.go b/mesg/mesg.go new file mode 100644 index 000000000..99116aa4c --- /dev/null +++ b/mesg/mesg.go @@ -0,0 +1,34 @@ +package mesg + +import "github.com/mesg-foundation/core/container" + +// MESG gives all functionalies of MESG core. +type MESG struct { + container *container.Container +} + +// Option is a configuration func for MESG. +type Option func(*MESG) + +// New creates a new MESG with given options. +func New(options ...Option) (*MESG, error) { + m := &MESG{} + for _, option := range options { + option(m) + } + var err error + if m.container == nil { + m.container, err = container.New() + if err != nil { + return nil, err + } + } + return m, nil +} + +// ContainerOption configures underlying container access API. +func ContainerOption(container *container.Container) Option { + return func(m *MESG) { + m.container = container + } +} diff --git a/mesg/mesg_test.go b/mesg/mesg_test.go new file mode 100644 index 000000000..775dbd781 --- /dev/null +++ b/mesg/mesg_test.go @@ -0,0 +1,21 @@ +package mesg + +import ( + "testing" + + "github.com/mesg-foundation/core/container" + "github.com/mesg-foundation/core/container/dockertest" + "github.com/stretchr/testify/require" +) + +func newMESGAndDockerTest(t *testing.T) (*MESG, *dockertest.Testing) { + dt := dockertest.New() + + container, err := container.New(container.ClientOption(dt.Client())) + require.Nil(t, err) + + m, err := New(ContainerOption(container)) + require.Nil(t, err) + + return m, dt +} diff --git a/mesg/service-test-invalid/empty b/mesg/service-test-invalid/empty new file mode 100644 index 000000000..e69de29bb diff --git a/mesg/service-test/Dockerfile b/mesg/service-test/Dockerfile new file mode 100644 index 000000000..e69de29bb diff --git a/mesg/service-test/mesg.yml b/mesg/service-test/mesg.yml new file mode 100644 index 000000000..286cd1f1a --- /dev/null +++ b/mesg/service-test/mesg.yml @@ -0,0 +1,20 @@ +name: BlueLogger +description: 'Log your data to std output' +tasks: + log: + inputs: + serviceID: + description: 'service id that log belongs to' + type: String + data: + description: 'data to log' + type: Object + outputs: + success: + data: + ok: + type: Boolean + error: + data: + message: + type: String \ No newline at end of file diff --git a/service/hash_test.go b/service/hash_test.go index e25d7cfc6..7d3239bf8 100644 --- a/service/hash_test.go +++ b/service/hash_test.go @@ -11,7 +11,7 @@ func TestGenerateId(t *testing.T) { Name: "TestGenerateId", } hash := service.Hash() - require.Equal(t, string(hash), "v1_b3664cde5d7fcb2d37fe2ebb45acdd27") + require.Equal(t, string(hash), "v1_cc79b9b84d6647ddb42e1a860c89a2a8") } func TestNoCollision(t *testing.T) { diff --git a/service/service.pb.go b/service/service.pb.go index 927c6ebd3..6b76b6cb1 100644 --- a/service/service.pb.go +++ b/service/service.pb.go @@ -34,6 +34,7 @@ const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package // This is the definition of a MESG Service. type Service struct { + Id string `protobuf:"bytes,10,opt,name=id" json:"id,omitempty"` Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` Description string `protobuf:"bytes,2,opt,name=description" json:"description,omitempty"` Tasks map[string]*Task `protobuf:"bytes,5,rep,name=tasks" json:"tasks,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` @@ -48,6 +49,13 @@ func (m *Service) String() string { return proto.CompactTextString(m) func (*Service) ProtoMessage() {} func (*Service) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} } +func (m *Service) GetId() string { + if m != nil { + return m.Id + } + return "" +} + func (m *Service) GetName() string { if m != nil { return m.Name @@ -308,41 +316,41 @@ func init() { } var fileDescriptor0 = []byte{ - // 566 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x55, 0xdf, 0x8a, 0xd3, 0x4e, - 0x14, 0x26, 0x6d, 0xd2, 0x36, 0x27, 0xbb, 0xbf, 0x9f, 0x8e, 0x5e, 0xcc, 0x46, 0x91, 0x50, 0x15, - 0x22, 0xb8, 0x29, 0xd6, 0x15, 0xd4, 0xeb, 0x5d, 0x65, 0x59, 0x44, 0x89, 0xfb, 0x02, 0xb3, 0xc9, - 0xb4, 0x86, 0x6d, 0x32, 0x71, 0x32, 0x29, 0xf4, 0x25, 0x7c, 0x1b, 0x41, 0xbc, 0xf3, 0xcd, 0x24, - 0x33, 0xe9, 0x34, 0x63, 0x73, 0x55, 0xf1, 0xaa, 0xe7, 0xdf, 0xf7, 0x9d, 0x33, 0xdf, 0x39, 0x25, - 0xf0, 0x6a, 0x99, 0x89, 0x2f, 0xf5, 0x4d, 0x94, 0xb0, 0x7c, 0x96, 0xd3, 0x6a, 0x79, 0xba, 0x60, - 0x75, 0x91, 0x12, 0x91, 0xb1, 0x62, 0x96, 0x30, 0x4e, 0x67, 0x15, 0xe5, 0xeb, 0x2c, 0xd1, 0xbf, - 0x51, 0xc9, 0x99, 0x60, 0x68, 0xdc, 0xba, 0xd3, 0xef, 0x36, 0x8c, 0x3f, 0x2b, 0x1b, 0x21, 0xb0, - 0x0b, 0x92, 0x53, 0x6c, 0x05, 0x56, 0xe8, 0xc6, 0xd2, 0x46, 0x01, 0x78, 0x29, 0xad, 0x12, 0x9e, - 0x95, 0x0d, 0x25, 0x1e, 0xc8, 0x54, 0x37, 0x84, 0x5e, 0x80, 0x23, 0x48, 0x75, 0x5b, 0x61, 0x27, - 0x18, 0x86, 0xde, 0xfc, 0x41, 0xb4, 0xed, 0xd4, 0xd2, 0x46, 0xd7, 0x4d, 0xf6, 0xa2, 0x10, 0x7c, - 0x13, 0xab, 0x4a, 0x74, 0x06, 0x23, 0xba, 0xa6, 0x85, 0xa8, 0xf0, 0x48, 0x62, 0x1e, 0xee, 0x61, - 0x2e, 0x64, 0x5a, 0x81, 0xda, 0x5a, 0xf4, 0x0e, 0x8e, 0x52, 0x5a, 0xd2, 0x22, 0xa5, 0x45, 0x92, - 0xd1, 0x0a, 0x8f, 0x25, 0x76, 0xba, 0x87, 0x3d, 0xef, 0x14, 0x29, 0x06, 0x03, 0x87, 0xde, 0xc0, - 0x71, 0xc2, 0x8a, 0x45, 0xb6, 0xac, 0xb9, 0xd4, 0x09, 0x4f, 0x02, 0x2b, 0xf4, 0xe6, 0xf7, 0x34, - 0x91, 0x26, 0xd8, 0xc4, 0x66, 0x25, 0x7a, 0x04, 0xc0, 0x69, 0xc9, 0xaa, 0x4c, 0x30, 0xbe, 0xc1, - 0xae, 0x14, 0xa3, 0x13, 0xf1, 0xdf, 0x03, 0xec, 0x5e, 0x8b, 0xee, 0xc0, 0xf0, 0x96, 0x6e, 0x5a, - 0x39, 0x1b, 0x13, 0x3d, 0x06, 0x67, 0x4d, 0x56, 0x35, 0x95, 0x3a, 0x7a, 0xf3, 0x63, 0xdd, 0xb2, - 0x41, 0xc5, 0x2a, 0xf7, 0x76, 0xf0, 0xda, 0xf2, 0x2f, 0xc1, 0xeb, 0x48, 0xd0, 0xc3, 0xf4, 0xc4, - 0x64, 0xfa, 0x4f, 0x33, 0x49, 0x58, 0x97, 0xea, 0x1a, 0xee, 0xee, 0x29, 0xd2, 0x43, 0xf8, 0xcc, - 0x24, 0xec, 0x55, 0x63, 0xc7, 0x3a, 0xfd, 0x61, 0x81, 0x23, 0x5b, 0x1d, 0x78, 0x35, 0xcf, 0xc1, - 0x4e, 0x89, 0x20, 0x78, 0x28, 0x97, 0x88, 0xcd, 0xf1, 0xa3, 0x73, 0x22, 0x88, 0x5a, 0x9d, 0xac, - 0xf2, 0xaf, 0xc0, 0xd5, 0xa1, 0x9e, 0xd9, 0x43, 0x73, 0x76, 0xa4, 0xd9, 0x3e, 0x11, 0x4e, 0x72, - 0x2a, 0x28, 0xef, 0x8e, 0xfe, 0x6b, 0x00, 0x76, 0xa3, 0xf7, 0xc1, 0xf7, 0x3e, 0xca, 0x8a, 0xb2, - 0xd6, 0xc7, 0x7b, 0x62, 0x2c, 0x31, 0xba, 0x94, 0xb9, 0xf6, 0x72, 0x55, 0x21, 0x3a, 0x83, 0x31, - 0xab, 0x85, 0xc4, 0xa8, 0xa3, 0xf5, 0x4d, 0xcc, 0x47, 0x95, 0x54, 0xa0, 0x6d, 0xa9, 0xff, 0x01, - 0xbc, 0x0e, 0xd9, 0xdf, 0x3e, 0xdb, 0xbf, 0x82, 0xa3, 0x6e, 0x9f, 0x1e, 0xbe, 0xa7, 0x26, 0xdf, - 0xff, 0x9a, 0x4f, 0xe1, 0xba, 0x1a, 0xfe, 0xb4, 0x60, 0xa4, 0xa2, 0x07, 0xaa, 0x78, 0x6a, 0xec, - 0xff, 0xe4, 0x8f, 0x56, 0xff, 0xf6, 0x00, 0xbe, 0x82, 0xab, 0xe3, 0x07, 0x8e, 0x8f, 0xc0, 0x16, - 0x9b, 0x92, 0xe2, 0xa1, 0x42, 0x35, 0x36, 0xf2, 0x61, 0xc2, 0x64, 0x96, 0xac, 0xb0, 0x1d, 0x58, - 0xe1, 0x24, 0xd6, 0xfe, 0xf4, 0x9b, 0x05, 0xb0, 0xfb, 0x23, 0xa1, 0xfb, 0xe0, 0x64, 0x39, 0x59, - 0x6e, 0xbb, 0x2a, 0x07, 0x61, 0x18, 0xaf, 0xd9, 0xaa, 0xce, 0x69, 0x85, 0x07, 0xc1, 0x30, 0x74, - 0xe3, 0xad, 0xdb, 0x0c, 0xd4, 0x9a, 0x0b, 0xce, 0x72, 0x29, 0x9a, 0x1b, 0x77, 0x43, 0x0d, 0x63, - 0xc9, 0xb8, 0xa8, 0xb0, 0x2d, 0x73, 0xca, 0x69, 0x18, 0x13, 0x96, 0xe7, 0xa4, 0x48, 0xb1, 0x23, - 0x3b, 0x6d, 0xdd, 0x9b, 0x91, 0xfc, 0x0e, 0xbc, 0xfc, 0x1d, 0x00, 0x00, 0xff, 0xff, 0x37, 0x3c, - 0xfb, 0x90, 0x40, 0x06, 0x00, 0x00, + // 575 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x55, 0xdd, 0x6e, 0xd3, 0x30, + 0x14, 0x56, 0xd2, 0xa4, 0x5d, 0x4e, 0xb6, 0x01, 0x86, 0x0b, 0x2f, 0x20, 0x14, 0x15, 0x90, 0x82, + 0xc4, 0x52, 0x51, 0x86, 0x04, 0x5c, 0x6f, 0xa0, 0x69, 0x42, 0xa0, 0xb0, 0x17, 0xf0, 0x12, 0xb7, + 0x58, 0x6b, 0xe2, 0xe0, 0x38, 0x95, 0xfa, 0x12, 0x3c, 0x0f, 0xe2, 0x8e, 0x0b, 0xde, 0x0b, 0xc5, + 0x4e, 0xdd, 0x84, 0xf6, 0xaa, 0x88, 0xab, 0x9d, 0xbf, 0xef, 0xfb, 0x4e, 0x3e, 0x9f, 0xa9, 0xf0, + 0x7a, 0xce, 0xe4, 0xd7, 0xfa, 0x26, 0x4e, 0x79, 0x3e, 0xc9, 0x69, 0x35, 0x3f, 0x9d, 0xf1, 0xba, + 0xc8, 0x88, 0x64, 0xbc, 0x98, 0xa4, 0x5c, 0xd0, 0x49, 0x45, 0xc5, 0x92, 0xa5, 0xe6, 0x6f, 0x5c, + 0x0a, 0x2e, 0x39, 0x1a, 0xb5, 0xe9, 0xf8, 0xb7, 0x03, 0xa3, 0x2f, 0x3a, 0x46, 0xc7, 0x60, 0xb3, + 0x0c, 0x43, 0x68, 0x45, 0x5e, 0x62, 0xb3, 0x0c, 0x21, 0x70, 0x0a, 0x92, 0x53, 0x6c, 0xa9, 0x8a, + 0x8a, 0x51, 0x08, 0x7e, 0x46, 0xab, 0x54, 0xb0, 0xb2, 0x91, 0xc0, 0xb6, 0x6a, 0x75, 0x4b, 0xe8, + 0x25, 0xb8, 0x92, 0x54, 0xb7, 0x15, 0x76, 0xc3, 0x41, 0xe4, 0x4f, 0x1f, 0xc6, 0x6b, 0xe5, 0x56, + 0x26, 0xbe, 0x6e, 0xba, 0x17, 0x85, 0x14, 0xab, 0x44, 0x4f, 0xa2, 0x33, 0x18, 0xd2, 0x25, 0x2d, + 0x64, 0x85, 0x87, 0x0a, 0xf3, 0x68, 0x0b, 0x73, 0xa1, 0xda, 0x1a, 0xd4, 0xce, 0xa2, 0xf7, 0x70, + 0x98, 0xd1, 0x92, 0x16, 0x19, 0x2d, 0x52, 0x46, 0x2b, 0x3c, 0x52, 0xd8, 0xf1, 0x16, 0xf6, 0xbc, + 0x33, 0xa4, 0x19, 0x7a, 0x38, 0xf4, 0x16, 0x8e, 0x52, 0x5e, 0xcc, 0xd8, 0xbc, 0x16, 0xca, 0x37, + 0x7c, 0x10, 0x5a, 0x91, 0x3f, 0xbd, 0x6f, 0x88, 0x0c, 0xc1, 0x2a, 0xe9, 0x4f, 0xa2, 0xc7, 0x00, + 0x82, 0x96, 0xbc, 0x62, 0x92, 0x8b, 0x15, 0xf6, 0x94, 0x19, 0x9d, 0x4a, 0xf0, 0x01, 0x60, 0xf3, + 0xb5, 0xe8, 0x2e, 0x0c, 0x6e, 0xe9, 0xaa, 0xb5, 0xb3, 0x09, 0xd1, 0x13, 0x70, 0x97, 0x64, 0x51, + 0x53, 0xe5, 0xa3, 0x3f, 0x3d, 0x32, 0x92, 0x0d, 0x2a, 0xd1, 0xbd, 0x77, 0xf6, 0x1b, 0x2b, 0xb8, + 0x04, 0xbf, 0x63, 0xc1, 0x0e, 0xa6, 0xa7, 0x7d, 0xa6, 0x63, 0xc3, 0xa4, 0x60, 0x5d, 0xaa, 0x6b, + 0xb8, 0xb7, 0xe5, 0xc8, 0x0e, 0xc2, 0xe7, 0x7d, 0xc2, 0x9d, 0x6e, 0x6c, 0x58, 0xc7, 0x3f, 0x2c, + 0x70, 0x95, 0xd4, 0x9e, 0x57, 0xf3, 0x02, 0x9c, 0x8c, 0x48, 0x82, 0x07, 0xea, 0x11, 0x71, 0x7f, + 0xfd, 0xf8, 0x9c, 0x48, 0xa2, 0x9f, 0x4e, 0x4d, 0x05, 0x57, 0xe0, 0x99, 0xd2, 0x8e, 0xdd, 0xa3, + 0xfe, 0xee, 0xc8, 0xb0, 0x7d, 0x26, 0x82, 0xe4, 0x54, 0x52, 0xd1, 0x5d, 0xfd, 0x97, 0x0d, 0x4e, + 0xe3, 0xf7, 0xde, 0xf7, 0x3e, 0x64, 0x45, 0x59, 0x9b, 0xe3, 0x3d, 0xe9, 0x3d, 0x62, 0x7c, 0xa9, + 0x7a, 0xed, 0xe5, 0xea, 0x41, 0x74, 0x06, 0x23, 0x5e, 0x4b, 0x85, 0xd1, 0x47, 0x1b, 0xf4, 0x31, + 0x9f, 0x74, 0x53, 0x83, 0xd6, 0xa3, 0xc1, 0x47, 0xf0, 0x3b, 0x64, 0xff, 0xfa, 0xd9, 0xc1, 0x15, + 0x1c, 0x76, 0x75, 0x76, 0xf0, 0x3d, 0xeb, 0xf3, 0xdd, 0x31, 0x7c, 0x1a, 0xd7, 0xf5, 0xf0, 0xa7, + 0x05, 0x43, 0x5d, 0xdd, 0xd3, 0xc5, 0xd3, 0xde, 0xfb, 0x9f, 0xfc, 0x25, 0xf5, 0x7f, 0x0f, 0xe0, + 0x1b, 0x78, 0xa6, 0xbe, 0xe7, 0xfa, 0x08, 0x1c, 0xb9, 0x2a, 0x29, 0x1e, 0x68, 0x54, 0x13, 0xa3, + 0x00, 0x0e, 0xb8, 0xea, 0x92, 0x05, 0x76, 0x42, 0x2b, 0x3a, 0x48, 0x4c, 0x3e, 0xfe, 0x6e, 0x01, + 0x6c, 0xfe, 0x91, 0xd0, 0x03, 0x70, 0x59, 0x4e, 0xe6, 0x6b, 0x55, 0x9d, 0x20, 0x0c, 0xa3, 0x25, + 0x5f, 0xd4, 0x39, 0xad, 0xb0, 0x1d, 0x0e, 0x22, 0x2f, 0x59, 0xa7, 0xcd, 0x42, 0x6d, 0x38, 0x13, + 0x3c, 0x57, 0xa6, 0x79, 0x49, 0xb7, 0xd4, 0x30, 0x96, 0x5c, 0xc8, 0x0a, 0x3b, 0xaa, 0xa7, 0x93, + 0x86, 0x31, 0xe5, 0x79, 0x4e, 0x8a, 0x0c, 0xbb, 0x4a, 0x69, 0x9d, 0xde, 0x0c, 0xd5, 0xef, 0xc2, + 0xab, 0x3f, 0x01, 0x00, 0x00, 0xff, 0xff, 0x77, 0x1a, 0xb7, 0xfb, 0x50, 0x06, 0x00, 0x00, } diff --git a/service/service.proto b/service/service.proto index 9e73b01ba..41a04cb15 100644 --- a/service/service.proto +++ b/service/service.proto @@ -4,13 +4,14 @@ package service; // This is the definition of a MESG Service. message Service { - string name = 1; // The service's name. - string description = 2; // The service's description. + string id = 10; // Service's unique id hash. + string name = 1; // Service's name. + string description = 2; // Service's description. map tasks = 5; // The list of tasks this service can execute. map events = 6; // The list of events this service can emit. map dependencies = 7; // The Docker dependencies this service requires. - Dependency configuration = 8; // The service's Docker configuration. - string repository = 9; // The service's repository that contain its source code. + Dependency configuration = 8; // Service's Docker configuration. + string repository = 9; // Service's repository that contain its source code. } // Events are emitted by the service whenever the service wants. diff --git a/vendor/github.com/briandowns/spinner/spinner.go b/vendor/github.com/briandowns/spinner/spinner.go index 14e43a06e..84283c744 100644 --- a/vendor/github.com/briandowns/spinner/spinner.go +++ b/vendor/github.com/briandowns/spinner/spinner.go @@ -14,6 +14,7 @@ package spinner import ( + "encoding/hex" "errors" "fmt" "io" @@ -21,7 +22,6 @@ import ( "sync" "time" "unicode/utf8" - "encoding/hex" "github.com/fatih/color" ) @@ -299,7 +299,12 @@ func (s *Spinner) UpdateCharSet(cs []string) { func (s *Spinner) erase() { n := utf8.RuneCountInString(s.lastOutput) del, _ := hex.DecodeString("7f") - for _, c := range []string{"\b", string(del), "\b"} { + for _, c := range []string{ + "\b", + string(del), + "\b", + "\033[K", // for macOS Terminal + } { for i := 0; i < n; i++ { fmt.Fprintf(s.Writer, c) }