diff --git a/grpc/Dockerfile.client b/grpc/Dockerfile.client new file mode 100644 index 00000000..3b672657 --- /dev/null +++ b/grpc/Dockerfile.client @@ -0,0 +1,21 @@ +FROM golang:latest AS stage + +WORKDIR /app/src + +COPY client/ ./client/ + +COPY ecommerce/ ./ecommerce/ + +COPY go.mod . + +COPY go.sum . + +RUN CGO_ENABLED=0 GOOS=linux go build -o client ./client + +FROM golang:latest + +WORKDIR /app/src + +COPY --from=stage /app/src/client/client . + +CMD [ "./client" ] \ No newline at end of file diff --git a/grpc/Dockerfile.server b/grpc/Dockerfile.server new file mode 100644 index 00000000..f53344e2 --- /dev/null +++ b/grpc/Dockerfile.server @@ -0,0 +1,21 @@ +FROM golang:latest AS stage + +WORKDIR /app/src + +COPY server/ ./server/ + +COPY ecommerce/ ./ecommerce/ + +COPY go.mod . + +COPY go.sum . + +RUN CGO_ENABLED=0 GOOS=linux go build -o server ./server + +FROM golang:latest + +WORKDIR /app/src + +COPY --from=stage /app/src/server/server . + +CMD [ "./server" ] \ No newline at end of file diff --git a/grpc/README.md b/grpc/README.md new file mode 100644 index 00000000..83fb2e50 --- /dev/null +++ b/grpc/README.md @@ -0,0 +1,39 @@ +# gRPC Application + +A basic gRPC microservice architecture, involving a gRPC server and a +client, that allow users to buy items from an online store. + +The ecommerce interface provided by the gRPC server, allows users to +list and buy grocery via the public client/server application. + +The client/server application is deployed as a Code Engine application, +with a public endpoint. While the gRPC server is deployed as a Code Engine +application only exposed to the Code Engine project. + +See image: + + +## Source code + +Check the source code if you want to understand how this works. In general, +we provided three directories: + +- `/ecommerce` directory hosts the protobuf files and declares the `grocery` + interface for a set of remote procedures that can be called by clients. +- `/server` directory hosts the `grocery` interface implementation and creates a server. +- `/client` directory defines an http server and calls the gRPC server via its different + handlers. + +## Try it! + +You can try to deploy this microservice architecture in your Code Engine project. +Once you have selected your Code Engine project, you only need to run: + +```sh +./run.sh +``` + +## Todo: +3. Extend initGroceryServer() content, in order to have more groceries. +4. Json Encoding on the client side, needs improvement +5. Error handling for queries validations, e.g. grocery not found, or category not found. \ No newline at end of file diff --git a/grpc/client/main.go b/grpc/client/main.go new file mode 100644 index 00000000..b6775a57 --- /dev/null +++ b/grpc/client/main.go @@ -0,0 +1,121 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "log" + "net/http" + "os" + "strconv" + "time" + + "github.com/gorilla/mux" + ec "github.com/qu1queee/CodeEngine/grpc/ecommerce" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" +) + +func indexHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=UFT-8") + json.NewEncoder(w).Encode("server is running") +} + +func GetGroceryHandler(groceryClient ec.GroceryClient) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + GetHandler(w, r, groceryClient) + } +} + +func BuyGroceryHandler(groceryClient ec.GroceryClient) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + BuyHandler(w, r, groceryClient) + } +} + +func GetHandler(w http.ResponseWriter, r *http.Request, groceryClient ec.GroceryClient) { + + // Contact the server and print out its response. + ctx, cancel := context.WithTimeout(context.Background(), time.Minute*1) + defer cancel() + + vars := mux.Vars(r) + + categoryName := vars["category"] + + category := ec.Category{ + Category: categoryName, + } + + itemList, err := groceryClient.ListGrocery(ctx, &category) + if err != nil { + fmt.Printf("failed to list grocery: %v", err) + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("failed to list grocery")) + return + } + + for _, item := range itemList.Item { + json.NewEncoder(w).Encode(fmt.Sprintf("Item Name: %v\n", item.Name)) + } + + w.WriteHeader(http.StatusOK) +} + +func BuyHandler(w http.ResponseWriter, r *http.Request, groceryClient ec.GroceryClient) { + vars := mux.Vars(r) + + itemName := vars["name"] + pAmount := vars["amount"] + categoryName := vars["category"] + + amount, err := strconv.ParseFloat(pAmount, 64) + if err != nil { + http.Error(w, "invalid amount parameter", http.StatusBadRequest) + } + + category := ec.Category{ + Category: categoryName, + Itemname: itemName, + } + + item, err := groceryClient.GetGrocery(context.Background(), &category) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + } + + json.NewEncoder(w).Encode(fmt.Sprintf("Grocery: \n Item: %v \n Quantity: %v\n", item.GetName(), item.GetQuantity())) + + paymentRequest := ec.PaymentRequest{ + Amount: amount, + Item: item, + } + + paymentResponse, _ := groceryClient.MakePayment(context.Background(), &paymentRequest) + json.NewEncoder(w).Encode(fmt.Sprintf("Payment Response: \n Purchase: %v \n Details: %v \n State: %v \n Change: %v\n", paymentResponse.PurchasedItem, paymentResponse.Details, paymentResponse.Success, paymentResponse.Change)) + w.WriteHeader(http.StatusOK) +} + +func main() { + + serverLocalEndpoint := "LOCAL_ENDPOINT_WITH_PORT" + + localEndpoint := os.Getenv(serverLocalEndpoint) + + fmt.Printf("using local endpoint: %s\n", localEndpoint) + conn, err := grpc.Dial(localEndpoint, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + log.Fatalf("failed to connect: %v", err) + } + + defer conn.Close() + + c := ec.NewGroceryClient(conn) + + r := mux.NewRouter() + r.HandleFunc("/", indexHandler).Methods("GET") + r.HandleFunc("/listgroceries/{category}", GetGroceryHandler(c)) + r.HandleFunc("/buygrocery/{category}/{name}/{amount:[0-9]+\\.[0-9]+}", BuyGroceryHandler(c)) + fmt.Println("server app is running on :8080 .....") + http.ListenAndServe(":8080", r) +} diff --git a/grpc/ecommerce/ecommerce.pb.go b/grpc/ecommerce/ecommerce.pb.go new file mode 100644 index 00000000..7ec492f9 --- /dev/null +++ b/grpc/ecommerce/ecommerce.pb.go @@ -0,0 +1,486 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.28.1 +// protoc v4.24.3 +// source: ecommerce/ecommerce.proto + +package ecommerce + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Category struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Category string `protobuf:"bytes,1,opt,name=category,proto3" json:"category,omitempty"` + Itemname string `protobuf:"bytes,2,opt,name=itemname,proto3" json:"itemname,omitempty"` +} + +func (x *Category) Reset() { + *x = Category{} + if protoimpl.UnsafeEnabled { + mi := &file_ecommerce_ecommerce_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Category) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Category) ProtoMessage() {} + +func (x *Category) ProtoReflect() protoreflect.Message { + mi := &file_ecommerce_ecommerce_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Category.ProtoReflect.Descriptor instead. +func (*Category) Descriptor() ([]byte, []int) { + return file_ecommerce_ecommerce_proto_rawDescGZIP(), []int{0} +} + +func (x *Category) GetCategory() string { + if x != nil { + return x.Category + } + return "" +} + +func (x *Category) GetItemname() string { + if x != nil { + return x.Itemname + } + return "" +} + +type ItemList struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Item []*Item `protobuf:"bytes,1,rep,name=item,proto3" json:"item,omitempty"` +} + +func (x *ItemList) Reset() { + *x = ItemList{} + if protoimpl.UnsafeEnabled { + mi := &file_ecommerce_ecommerce_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ItemList) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ItemList) ProtoMessage() {} + +func (x *ItemList) ProtoReflect() protoreflect.Message { + mi := &file_ecommerce_ecommerce_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ItemList.ProtoReflect.Descriptor instead. +func (*ItemList) Descriptor() ([]byte, []int) { + return file_ecommerce_ecommerce_proto_rawDescGZIP(), []int{1} +} + +func (x *ItemList) GetItem() []*Item { + if x != nil { + return x.Item + } + return nil +} + +type Item struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Quantity string `protobuf:"bytes,2,opt,name=quantity,proto3" json:"quantity,omitempty"` + Price float64 `protobuf:"fixed64,3,opt,name=price,proto3" json:"price,omitempty"` +} + +func (x *Item) Reset() { + *x = Item{} + if protoimpl.UnsafeEnabled { + mi := &file_ecommerce_ecommerce_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Item) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Item) ProtoMessage() {} + +func (x *Item) ProtoReflect() protoreflect.Message { + mi := &file_ecommerce_ecommerce_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Item.ProtoReflect.Descriptor instead. +func (*Item) Descriptor() ([]byte, []int) { + return file_ecommerce_ecommerce_proto_rawDescGZIP(), []int{2} +} + +func (x *Item) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Item) GetQuantity() string { + if x != nil { + return x.Quantity + } + return "" +} + +func (x *Item) GetPrice() float64 { + if x != nil { + return x.Price + } + return 0 +} + +type PaymentRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Amount float64 `protobuf:"fixed64,1,opt,name=amount,proto3" json:"amount,omitempty"` + Item *Item `protobuf:"bytes,2,opt,name=item,proto3" json:"item,omitempty"` +} + +func (x *PaymentRequest) Reset() { + *x = PaymentRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_ecommerce_ecommerce_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PaymentRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PaymentRequest) ProtoMessage() {} + +func (x *PaymentRequest) ProtoReflect() protoreflect.Message { + mi := &file_ecommerce_ecommerce_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PaymentRequest.ProtoReflect.Descriptor instead. +func (*PaymentRequest) Descriptor() ([]byte, []int) { + return file_ecommerce_ecommerce_proto_rawDescGZIP(), []int{3} +} + +func (x *PaymentRequest) GetAmount() float64 { + if x != nil { + return x.Amount + } + return 0 +} + +func (x *PaymentRequest) GetItem() *Item { + if x != nil { + return x.Item + } + return nil +} + +type PaymentResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` + PurchasedItem *Item `protobuf:"bytes,2,opt,name=purchasedItem,proto3" json:"purchasedItem,omitempty"` + Details string `protobuf:"bytes,3,opt,name=details,proto3" json:"details,omitempty"` + Change float64 `protobuf:"fixed64,4,opt,name=change,proto3" json:"change,omitempty"` +} + +func (x *PaymentResponse) Reset() { + *x = PaymentResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_ecommerce_ecommerce_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PaymentResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PaymentResponse) ProtoMessage() {} + +func (x *PaymentResponse) ProtoReflect() protoreflect.Message { + mi := &file_ecommerce_ecommerce_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PaymentResponse.ProtoReflect.Descriptor instead. +func (*PaymentResponse) Descriptor() ([]byte, []int) { + return file_ecommerce_ecommerce_proto_rawDescGZIP(), []int{4} +} + +func (x *PaymentResponse) GetSuccess() bool { + if x != nil { + return x.Success + } + return false +} + +func (x *PaymentResponse) GetPurchasedItem() *Item { + if x != nil { + return x.PurchasedItem + } + return nil +} + +func (x *PaymentResponse) GetDetails() string { + if x != nil { + return x.Details + } + return "" +} + +func (x *PaymentResponse) GetChange() float64 { + if x != nil { + return x.Change + } + return 0 +} + +var File_ecommerce_ecommerce_proto protoreflect.FileDescriptor + +var file_ecommerce_ecommerce_proto_rawDesc = []byte{ + 0x0a, 0x19, 0x65, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x72, 0x63, 0x65, 0x2f, 0x65, 0x63, 0x6f, 0x6d, + 0x6d, 0x65, 0x72, 0x63, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x09, 0x65, 0x63, 0x6f, + 0x6d, 0x6d, 0x65, 0x72, 0x63, 0x65, 0x22, 0x42, 0x0a, 0x08, 0x43, 0x61, 0x74, 0x65, 0x67, 0x6f, + 0x72, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x12, 0x1a, + 0x0a, 0x08, 0x69, 0x74, 0x65, 0x6d, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x08, 0x69, 0x74, 0x65, 0x6d, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x2f, 0x0a, 0x08, 0x49, 0x74, + 0x65, 0x6d, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x23, 0x0a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x18, 0x01, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x65, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x72, 0x63, 0x65, + 0x2e, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x22, 0x4c, 0x0a, 0x04, 0x49, + 0x74, 0x65, 0x6d, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x71, 0x75, 0x61, 0x6e, 0x74, + 0x69, 0x74, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x71, 0x75, 0x61, 0x6e, 0x74, + 0x69, 0x74, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x70, 0x72, 0x69, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x01, 0x52, 0x05, 0x70, 0x72, 0x69, 0x63, 0x65, 0x22, 0x4d, 0x0a, 0x0e, 0x50, 0x61, 0x79, + 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x61, + 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x01, 0x52, 0x06, 0x61, 0x6d, 0x6f, + 0x75, 0x6e, 0x74, 0x12, 0x23, 0x0a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x0f, 0x2e, 0x65, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x72, 0x63, 0x65, 0x2e, 0x49, 0x74, + 0x65, 0x6d, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x22, 0x94, 0x01, 0x0a, 0x0f, 0x50, 0x61, 0x79, + 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, + 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, + 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x12, 0x35, 0x0a, 0x0d, 0x70, 0x75, 0x72, 0x63, 0x68, 0x61, + 0x73, 0x65, 0x64, 0x49, 0x74, 0x65, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, + 0x65, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x72, 0x63, 0x65, 0x2e, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x0d, + 0x70, 0x75, 0x72, 0x63, 0x68, 0x61, 0x73, 0x65, 0x64, 0x49, 0x74, 0x65, 0x6d, 0x12, 0x18, 0x0a, + 0x07, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, + 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x63, 0x68, 0x61, 0x6e, 0x67, + 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x01, 0x52, 0x06, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x32, + 0xc2, 0x01, 0x0a, 0x07, 0x67, 0x72, 0x6f, 0x63, 0x65, 0x72, 0x79, 0x12, 0x34, 0x0a, 0x0a, 0x47, + 0x65, 0x74, 0x47, 0x72, 0x6f, 0x63, 0x65, 0x72, 0x79, 0x12, 0x13, 0x2e, 0x65, 0x63, 0x6f, 0x6d, + 0x6d, 0x65, 0x72, 0x63, 0x65, 0x2e, 0x43, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x1a, 0x0f, + 0x2e, 0x65, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x72, 0x63, 0x65, 0x2e, 0x49, 0x74, 0x65, 0x6d, 0x22, + 0x00, 0x12, 0x39, 0x0a, 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x47, 0x72, 0x6f, 0x63, 0x65, 0x72, 0x79, + 0x12, 0x13, 0x2e, 0x65, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x72, 0x63, 0x65, 0x2e, 0x43, 0x61, 0x74, + 0x65, 0x67, 0x6f, 0x72, 0x79, 0x1a, 0x13, 0x2e, 0x65, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x72, 0x63, + 0x65, 0x2e, 0x49, 0x74, 0x65, 0x6d, 0x4c, 0x69, 0x73, 0x74, 0x22, 0x00, 0x12, 0x46, 0x0a, 0x0b, + 0x4d, 0x61, 0x6b, 0x65, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x19, 0x2e, 0x65, 0x63, + 0x6f, 0x6d, 0x6d, 0x65, 0x72, 0x63, 0x65, 0x2e, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x65, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x72, + 0x63, 0x65, 0x2e, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x00, 0x42, 0x2f, 0x5a, 0x2d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, + 0x6f, 0x6d, 0x2f, 0x71, 0x75, 0x31, 0x71, 0x75, 0x65, 0x65, 0x65, 0x2f, 0x43, 0x6f, 0x64, 0x65, + 0x45, 0x6e, 0x67, 0x69, 0x6e, 0x65, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x2f, 0x65, 0x63, 0x6f, 0x6d, + 0x6d, 0x65, 0x72, 0x63, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_ecommerce_ecommerce_proto_rawDescOnce sync.Once + file_ecommerce_ecommerce_proto_rawDescData = file_ecommerce_ecommerce_proto_rawDesc +) + +func file_ecommerce_ecommerce_proto_rawDescGZIP() []byte { + file_ecommerce_ecommerce_proto_rawDescOnce.Do(func() { + file_ecommerce_ecommerce_proto_rawDescData = protoimpl.X.CompressGZIP(file_ecommerce_ecommerce_proto_rawDescData) + }) + return file_ecommerce_ecommerce_proto_rawDescData +} + +var file_ecommerce_ecommerce_proto_msgTypes = make([]protoimpl.MessageInfo, 5) +var file_ecommerce_ecommerce_proto_goTypes = []interface{}{ + (*Category)(nil), // 0: ecommerce.Category + (*ItemList)(nil), // 1: ecommerce.ItemList + (*Item)(nil), // 2: ecommerce.Item + (*PaymentRequest)(nil), // 3: ecommerce.PaymentRequest + (*PaymentResponse)(nil), // 4: ecommerce.PaymentResponse +} +var file_ecommerce_ecommerce_proto_depIdxs = []int32{ + 2, // 0: ecommerce.ItemList.item:type_name -> ecommerce.Item + 2, // 1: ecommerce.PaymentRequest.item:type_name -> ecommerce.Item + 2, // 2: ecommerce.PaymentResponse.purchasedItem:type_name -> ecommerce.Item + 0, // 3: ecommerce.grocery.GetGrocery:input_type -> ecommerce.Category + 0, // 4: ecommerce.grocery.ListGrocery:input_type -> ecommerce.Category + 3, // 5: ecommerce.grocery.MakePayment:input_type -> ecommerce.PaymentRequest + 2, // 6: ecommerce.grocery.GetGrocery:output_type -> ecommerce.Item + 1, // 7: ecommerce.grocery.ListGrocery:output_type -> ecommerce.ItemList + 4, // 8: ecommerce.grocery.MakePayment:output_type -> ecommerce.PaymentResponse + 6, // [6:9] is the sub-list for method output_type + 3, // [3:6] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name +} + +func init() { file_ecommerce_ecommerce_proto_init() } +func file_ecommerce_ecommerce_proto_init() { + if File_ecommerce_ecommerce_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_ecommerce_ecommerce_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Category); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_ecommerce_ecommerce_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ItemList); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_ecommerce_ecommerce_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Item); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_ecommerce_ecommerce_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*PaymentRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_ecommerce_ecommerce_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*PaymentResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_ecommerce_ecommerce_proto_rawDesc, + NumEnums: 0, + NumMessages: 5, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_ecommerce_ecommerce_proto_goTypes, + DependencyIndexes: file_ecommerce_ecommerce_proto_depIdxs, + MessageInfos: file_ecommerce_ecommerce_proto_msgTypes, + }.Build() + File_ecommerce_ecommerce_proto = out.File + file_ecommerce_ecommerce_proto_rawDesc = nil + file_ecommerce_ecommerce_proto_goTypes = nil + file_ecommerce_ecommerce_proto_depIdxs = nil +} diff --git a/grpc/ecommerce/ecommerce.proto b/grpc/ecommerce/ecommerce.proto new file mode 100644 index 00000000..8ba2af72 --- /dev/null +++ b/grpc/ecommerce/ecommerce.proto @@ -0,0 +1,37 @@ +syntax = "proto3"; + +package ecommerce; +option go_package="github.com/qu1queee/CodeEngine/grpc/ecommerce"; + +service grocery { + rpc GetGrocery(Category) returns (Item) {}; + rpc ListGrocery(Category) returns (ItemList) {}; + rpc MakePayment(PaymentRequest) returns (PaymentResponse) {}; +} + +message Category{ + string category = 1; + string itemname = 2; +} + +message ItemList{ + repeated Item item =1; +} + +message Item{ + string name = 1; + string quantity = 2; + double price = 3; +} + +message PaymentRequest { + double amount = 1; + Item item = 2; +} + +message PaymentResponse { + bool success = 1; + Item purchasedItem = 2; + string details = 3; + double change = 4; +} \ No newline at end of file diff --git a/grpc/ecommerce/ecommerce_grpc.pb.go b/grpc/ecommerce/ecommerce_grpc.pb.go new file mode 100644 index 00000000..c52a2ad9 --- /dev/null +++ b/grpc/ecommerce/ecommerce_grpc.pb.go @@ -0,0 +1,177 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.2.0 +// - protoc v4.24.3 +// source: ecommerce/ecommerce.proto + +package ecommerce + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +// GroceryClient is the client API for Grocery service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type GroceryClient interface { + GetGrocery(ctx context.Context, in *Category, opts ...grpc.CallOption) (*Item, error) + ListGrocery(ctx context.Context, in *Category, opts ...grpc.CallOption) (*ItemList, error) + MakePayment(ctx context.Context, in *PaymentRequest, opts ...grpc.CallOption) (*PaymentResponse, error) +} + +type groceryClient struct { + cc grpc.ClientConnInterface +} + +func NewGroceryClient(cc grpc.ClientConnInterface) GroceryClient { + return &groceryClient{cc} +} + +func (c *groceryClient) GetGrocery(ctx context.Context, in *Category, opts ...grpc.CallOption) (*Item, error) { + out := new(Item) + err := c.cc.Invoke(ctx, "/ecommerce.grocery/GetGrocery", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *groceryClient) ListGrocery(ctx context.Context, in *Category, opts ...grpc.CallOption) (*ItemList, error) { + out := new(ItemList) + err := c.cc.Invoke(ctx, "/ecommerce.grocery/ListGrocery", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *groceryClient) MakePayment(ctx context.Context, in *PaymentRequest, opts ...grpc.CallOption) (*PaymentResponse, error) { + out := new(PaymentResponse) + err := c.cc.Invoke(ctx, "/ecommerce.grocery/MakePayment", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// GroceryServer is the server API for Grocery service. +// All implementations must embed UnimplementedGroceryServer +// for forward compatibility +type GroceryServer interface { + GetGrocery(context.Context, *Category) (*Item, error) + ListGrocery(context.Context, *Category) (*ItemList, error) + MakePayment(context.Context, *PaymentRequest) (*PaymentResponse, error) + mustEmbedUnimplementedGroceryServer() +} + +// UnimplementedGroceryServer must be embedded to have forward compatible implementations. +type UnimplementedGroceryServer struct { +} + +func (UnimplementedGroceryServer) GetGrocery(context.Context, *Category) (*Item, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetGrocery not implemented") +} +func (UnimplementedGroceryServer) ListGrocery(context.Context, *Category) (*ItemList, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListGrocery not implemented") +} +func (UnimplementedGroceryServer) MakePayment(context.Context, *PaymentRequest) (*PaymentResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method MakePayment not implemented") +} +func (UnimplementedGroceryServer) mustEmbedUnimplementedGroceryServer() {} + +// UnsafeGroceryServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to GroceryServer will +// result in compilation errors. +type UnsafeGroceryServer interface { + mustEmbedUnimplementedGroceryServer() +} + +func RegisterGroceryServer(s grpc.ServiceRegistrar, srv GroceryServer) { + s.RegisterService(&Grocery_ServiceDesc, srv) +} + +func _Grocery_GetGrocery_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(Category) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GroceryServer).GetGrocery(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/ecommerce.grocery/GetGrocery", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GroceryServer).GetGrocery(ctx, req.(*Category)) + } + return interceptor(ctx, in, info, handler) +} + +func _Grocery_ListGrocery_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(Category) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GroceryServer).ListGrocery(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/ecommerce.grocery/ListGrocery", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GroceryServer).ListGrocery(ctx, req.(*Category)) + } + return interceptor(ctx, in, info, handler) +} + +func _Grocery_MakePayment_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(PaymentRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GroceryServer).MakePayment(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/ecommerce.grocery/MakePayment", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GroceryServer).MakePayment(ctx, req.(*PaymentRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// Grocery_ServiceDesc is the grpc.ServiceDesc for Grocery service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Grocery_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "ecommerce.grocery", + HandlerType: (*GroceryServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "GetGrocery", + Handler: _Grocery_GetGrocery_Handler, + }, + { + MethodName: "ListGrocery", + Handler: _Grocery_ListGrocery_Handler, + }, + { + MethodName: "MakePayment", + Handler: _Grocery_MakePayment_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "ecommerce/ecommerce.proto", +} diff --git a/grpc/go.mod b/grpc/go.mod new file mode 100644 index 00000000..ccc6f9f3 --- /dev/null +++ b/grpc/go.mod @@ -0,0 +1,17 @@ +module github.com/qu1queee/CodeEngine/grpc + +go 1.21.0 + +require ( + github.com/gorilla/mux v1.8.1 + google.golang.org/grpc v1.59.0 + google.golang.org/protobuf v1.31.0 +) + +require ( + github.com/golang/protobuf v1.5.3 // indirect + golang.org/x/net v0.19.0 // indirect + golang.org/x/sys v0.15.0 // indirect + golang.org/x/text v0.14.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect +) diff --git a/grpc/go.sum b/grpc/go.sum new file mode 100644 index 00000000..86400282 --- /dev/null +++ b/grpc/go.sum @@ -0,0 +1,23 @@ +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d h1:uvYuEyMHKNt+lT4K3bN6fGswmK8qSvcreM3BwjDh+y4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M= +google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= +google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= diff --git a/grpc/run.sh b/grpc/run.sh new file mode 100755 index 00000000..d37c725d --- /dev/null +++ b/grpc/run.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +set -euo pipefail + +SERVER_APP_NAME="a-grpc-server" +CLIENT_APP_NAME="a-grpc-client" + +# Create the gRPC server app +echo "[INFO] Creating CE gRPC server application ${SERVER_APP_NAME}" +ibmcloud ce app create --name "${SERVER_APP_NAME}" --port h2c:8080 --min-scale 1 --build-source . --build-dockerfile Dockerfile.server + +echo "[INFO] Retrieving gRPC server local endpoint" +SERVER_INTERNAL_ENDPOINT=$(ibmcloud ce app get -n "${SERVER_APP_NAME}" -o project-url | sed 's/http:\/\///') +echo "[INFO] Local endpoint is: ${SERVER_INTERNAL_ENDPOINT}" + +# Create the client server app +echo "[INFO] Creating CE client/server application ${CLIENT_APP_NAME}" +ibmcloud ce app create --name "${CLIENT_APP_NAME}" --min-scale 1 --build-source . --build-dockerfile Dockerfile.client --env LOCAL_ENDPOINT_WITH_PORT="${SERVER_INTERNAL_ENDPOINT}:80" + +# Get the client server public endpoint +echo "[INFO] Retrieving client/server public endpoint" +URL=$(ibmcloud ce app get -n "${CLIENT_APP_NAME}" -o url) +echo "[INFO] Endpoint is: ${URL}" + +# Query the list of groceries by electronics category +echo "[INFO] Retrieving available electronic items" +curl -q "${URL}"/listgroceries/electronics + +# Buy an item from electronics and pay with 2000 +echo "[INFO] Going to buy an iphone" +curl -q "${URL}"/buygrocery/electronics/iphone/2000.0 \ No newline at end of file diff --git a/grpc/server/main.go b/grpc/server/main.go new file mode 100644 index 00000000..761b0b29 --- /dev/null +++ b/grpc/server/main.go @@ -0,0 +1,126 @@ +package main + +import ( + "context" + "errors" + "fmt" + "log" + "net" + + ec "github.com/qu1queee/CodeEngine/grpc/ecommerce" + "google.golang.org/grpc" +) + +type GroceryServer struct { + Products []Product + ec.UnimplementedGroceryServer +} + +type Product struct { + Category string + Name string + Quantity string + Price float64 +} + +func initGroceryServer() *GroceryServer { + return &GroceryServer{ + Products: []Product{ + { + Category: "vegetables", + Name: "carrot", + Quantity: "4", + Price: 0.5, + }, + { + Category: "fruit", + Name: "apple", + Quantity: "3", + Price: 0.6, + }, + { + Category: "electronics", + Name: "iphone", + Quantity: "1", + Price: 1200.99, + }, + { + Category: "electronics", + Name: "usb", + Quantity: "1", + Price: 2.5, + }, + }, + } +} + +func (gs *GroceryServer) GetGrocery(ctx context.Context, in *ec.Category) (*ec.Item, error) { + for _, p := range gs.Products { + if in.Category == p.Category { + if in.Itemname == p.Name { + // return the Item + return &ec.Item{ + Name: p.Name, + Quantity: p.Quantity, + Price: p.Price, + }, nil + } + } + } + + return &ec.Item{}, nil +} + +func (gs *GroceryServer) ListGrocery(ctx context.Context, in *ec.Category) (*ec.ItemList, error) { + itemList := ec.ItemList{} + for _, p := range gs.Products { + if in.Category == p.Category { + itemList.Item = append(itemList.Item, &ec.Item{ + Name: p.Name, + Quantity: p.Quantity, + Price: p.Price, + }) + } + } + if itemList.Item != nil { + return &itemList, nil + } + return &itemList, errors.New("category not found") +} + +func (gs *GroceryServer) MakePayment(ctx context.Context, in *ec.PaymentRequest) (*ec.PaymentResponse, error) { + amount := in.GetAmount() + purchasedItem := in.GetItem() + + transactionSuccessfull := true + var transactionDetails string + + if transactionSuccessfull { + transactionDetails = fmt.Sprintf("Transaction of amount %v is successful", amount) + } else { + transactionDetails = fmt.Sprintf("Transaction of amount %v failed", amount) + } + + change := amount - purchasedItem.Price + + return &ec.PaymentResponse{ + Success: transactionSuccessfull, + PurchasedItem: purchasedItem, + Details: transactionDetails, + Change: change, + }, nil +} + +func main() { + lis, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:%d", 8080)) + if err != nil { + log.Fatalf("failed to listen: %v", err) + } + + s := grpc.NewServer() + ec.RegisterGroceryServer(s, initGroceryServer()) + + if err := s.Serve(lis); err != nil { + log.Fatalf("failed to start server %v", err) + } +}