From 8341c7752dd7914f62510b8ceedc330e91ce2699 Mon Sep 17 00:00:00 2001 From: encalada Date: Tue, 28 Nov 2023 14:49:37 +0100 Subject: [PATCH 1/4] Add grpc samples Introduce grpc directory, containing a microservice architecture consisting of an internal grpc server, and a public client/server app. A run.sh script is provided, to easily deploy this architecture in Code Engine, as well as a README, explaining the ideas behind. Signed-off-by: Mahesh Kumawat Signed-off-by: encalada Signed-off-by: Mahesh Kumawat --- grpc/Dockerfile.client | 21 ++ grpc/Dockerfile.server | 21 ++ grpc/README.md | 39 +++ grpc/client/main.go | 121 +++++++ grpc/ecommerce/ecommerce.pb.go | 486 ++++++++++++++++++++++++++++ grpc/ecommerce/ecommerce.proto | 37 +++ grpc/ecommerce/ecommerce_grpc.pb.go | 177 ++++++++++ grpc/go.mod | 17 + grpc/go.sum | 23 ++ grpc/run.sh | 31 ++ grpc/server/main.go | 126 ++++++++ 11 files changed, 1099 insertions(+) create mode 100644 grpc/Dockerfile.client create mode 100644 grpc/Dockerfile.server create mode 100644 grpc/README.md create mode 100644 grpc/client/main.go create mode 100644 grpc/ecommerce/ecommerce.pb.go create mode 100644 grpc/ecommerce/ecommerce.proto create mode 100644 grpc/ecommerce/ecommerce_grpc.pb.go create mode 100644 grpc/go.mod create mode 100644 grpc/go.sum create mode 100755 grpc/run.sh create mode 100644 grpc/server/main.go 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) + } +} From 9f120d8c573ba5f194e0ceeed340fbe1aa7cd13d Mon Sep 17 00:00:00 2001 From: encalada Date: Tue, 5 Dec 2023 17:51:15 +0100 Subject: [PATCH 2/4] From PR review Addressing all good feedback, to inline with other CE samples. Signed-off-by: encalada --- grpc/Dockerfile.client | 4 +-- grpc/Dockerfile.server | 4 +-- grpc/README.md | 2 +- grpc/client/main.go | 39 +++++++++++++----------- grpc/regenerate-grpc-code.sh | 25 ++++++++++++++++ grpc/{run.sh => run} | 34 +++++++++++++++++++-- grpc/server/main.go | 58 ++++++++++++++++++++++++++++++------ 7 files changed, 131 insertions(+), 35 deletions(-) create mode 100755 grpc/regenerate-grpc-code.sh rename grpc/{run.sh => run} (53%) diff --git a/grpc/Dockerfile.client b/grpc/Dockerfile.client index 3b672657..f2a31868 100644 --- a/grpc/Dockerfile.client +++ b/grpc/Dockerfile.client @@ -1,4 +1,4 @@ -FROM golang:latest AS stage +FROM icr.io/codeengine/golang:latest AS stage WORKDIR /app/src @@ -12,7 +12,7 @@ COPY go.sum . RUN CGO_ENABLED=0 GOOS=linux go build -o client ./client -FROM golang:latest +FROM icr.io/codeengine/golang:latest WORKDIR /app/src diff --git a/grpc/Dockerfile.server b/grpc/Dockerfile.server index f53344e2..a38693c3 100644 --- a/grpc/Dockerfile.server +++ b/grpc/Dockerfile.server @@ -1,4 +1,4 @@ -FROM golang:latest AS stage +FROM icr.io/codeengine/golang:latest AS stage WORKDIR /app/src @@ -12,7 +12,7 @@ COPY go.sum . RUN CGO_ENABLED=0 GOOS=linux go build -o server ./server -FROM golang:latest +FROM icr.io/codeengine/golang:latest WORKDIR /app/src diff --git a/grpc/README.md b/grpc/README.md index 83fb2e50..929d538d 100644 --- a/grpc/README.md +++ b/grpc/README.md @@ -30,7 +30,7 @@ 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 +./run ``` ## Todo: diff --git a/grpc/client/main.go b/grpc/client/main.go index b6775a57..47389403 100644 --- a/grpc/client/main.go +++ b/grpc/client/main.go @@ -33,6 +33,12 @@ func BuyGroceryHandler(groceryClient ec.GroceryClient) func(w http.ResponseWrite } } +func Fail(w http.ResponseWriter, msg string, err error) { + fmt.Printf("%s: %v\n", msg, err) + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(fmt.Sprintf("%s\n", msg))) +} + func GetHandler(w http.ResponseWriter, r *http.Request, groceryClient ec.GroceryClient) { // Contact the server and print out its response. @@ -49,17 +55,12 @@ func GetHandler(w http.ResponseWriter, r *http.Request, groceryClient ec.Grocery 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")) + Fail(w, "failed to list groceries", err) return } - for _, item := range itemList.Item { - json.NewEncoder(w).Encode(fmt.Sprintf("Item Name: %v\n", item.Name)) - } - - w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(itemList.Item) } func BuyHandler(w http.ResponseWriter, r *http.Request, groceryClient ec.GroceryClient) { @@ -71,7 +72,7 @@ func BuyHandler(w http.ResponseWriter, r *http.Request, groceryClient ec.Grocery amount, err := strconv.ParseFloat(pAmount, 64) if err != nil { - http.Error(w, "invalid amount parameter", http.StatusBadRequest) + Fail(w, "invalid amount parameter", err) } category := ec.Category{ @@ -81,26 +82,28 @@ func BuyHandler(w http.ResponseWriter, r *http.Request, groceryClient ec.Grocery item, err := groceryClient.GetGrocery(context.Background(), &category) if err != nil { - w.WriteHeader(http.StatusBadRequest) + Fail(w, fmt.Sprintf("failed to get grocery item by name: %v", category.Itemname), err) + return } - 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() { + // if !paymentResponse.Success { + // Fail(w, "failed to buy grocery, not enough money", errors.New("not enough money")) + // return + // } - serverLocalEndpoint := "LOCAL_ENDPOINT_WITH_PORT" + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(paymentResponse) +} - localEndpoint := os.Getenv(serverLocalEndpoint) +func main() { + localEndpoint := os.Getenv("LOCAL_ENDPOINT_WITH_PORT") fmt.Printf("using local endpoint: %s\n", localEndpoint) conn, err := grpc.Dial(localEndpoint, grpc.WithTransportCredentials(insecure.NewCredentials())) diff --git a/grpc/regenerate-grpc-code.sh b/grpc/regenerate-grpc-code.sh new file mode 100755 index 00000000..8661e72f --- /dev/null +++ b/grpc/regenerate-grpc-code.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +set -euo pipefail + +BASEDIR="$(dirname "$0")" + +if ! hash protoc >/dev/null 2>&1; then + echo "[ERROR] protoc compiler plugin is not installed, bailing out." + echo "[INFO] refer to https://grpc.io/docs/languages/go/quickstart/#prerequisites for instalation guidelines." + echo + exit 1 +fi + +if ! hash protoc-gen-go >/dev/null 2>&1; then + echo "[ERROR] protoc-gen-go plugin is not installed, bailing out." + echo "[INFO] refer to https://grpc.io/docs/languages/go/quickstart/#prerequisites for instalation guidelines." + echo "[INFO] ensure your GOPATH is in your PATH". + exit 1 +fi + + +echo "[INFO] Recompiling .proto file, this will regenerate the *.pb.go files." +protoc --go_out=. --go_opt=paths=source_relative \ + --go-grpc_out=. --go-grpc_opt=paths=source_relative \ + ${BASEDIR}/ecommerce/ecommerce.proto diff --git a/grpc/run.sh b/grpc/run similarity index 53% rename from grpc/run.sh rename to grpc/run index d37c725d..6b7813fc 100755 --- a/grpc/run.sh +++ b/grpc/run @@ -5,6 +5,21 @@ set -euo pipefail SERVER_APP_NAME="a-grpc-server" CLIENT_APP_NAME="a-grpc-client" +# Clean up previous run +function clean() { + echo "[INFO] Deleting CE client/server application ${CLIENT_APP_NAME}" + ibmcloud ce app delete --name $CLIENT_APP_NAME --force --ignore-not-found > /dev/null 2>&1 + echo "[INFO] Deleting CE gRPC server application ${SERVER_APP_NAME}" + ibmcloud ce app delete --name $SERVER_APP_NAME --force --ignore-not-found > /dev/null 2>&1 +} + +if [ $# -ge 1 ] && [ -n "$1" ] +then + echo "[INFO] going to clean existing project resources" + clean + exit 0 +fi + # 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 @@ -26,6 +41,19 @@ echo "[INFO] Endpoint is: ${URL}" 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 +# Buy an item from food and pay with 5.0 dollars +echo "[INFO] Going to buy an apple, paying with 5.0 dollars" +JSON_REPONSE=$(curl -s "${URL}"/buygrocery/vegetables/apple/5.0 | jq '.success') +echo $JSON_REPONSE | jq . + +# Validate payment operation +echo "[INFO] Validating payment operation state" +OPERATION_SUCCEEDED=$(echo $JSON_REPONSE | jq '.success') +if [ "${OPERATION_SUCCEEDED}" == "null" ] +then + echo "[ERROR] Payment failed, bailing out." + exit 1 +else + echo "[INFO] Successful payment operation." + exit 0 +fi \ No newline at end of file diff --git a/grpc/server/main.go b/grpc/server/main.go index 761b0b29..472328cf 100644 --- a/grpc/server/main.go +++ b/grpc/server/main.go @@ -27,16 +27,46 @@ func initGroceryServer() *GroceryServer { return &GroceryServer{ Products: []Product{ { - Category: "vegetables", + Category: "food", Name: "carrot", - Quantity: "4", + Quantity: "1", Price: 0.5, }, { - Category: "fruit", + Category: "food", Name: "apple", - Quantity: "3", - Price: 0.6, + Quantity: "1", + Price: 0.5, + }, + { + Category: "food", + Name: "banana", + Quantity: "1", + Price: 0.5, + }, + { + Category: "food", + Name: "lemon", + Quantity: "1", + Price: 0.5, + }, + { + Category: "clothes", + Name: "tshirt", + Quantity: "1", + Price: 25.0, + }, + { + Category: "clothes", + Name: "pants", + Quantity: "1", + Price: 65.0, + }, + { + Category: "clothes", + Name: "socks", + Quantity: "1", + Price: 5.0, }, { Category: "electronics", @@ -44,6 +74,12 @@ func initGroceryServer() *GroceryServer { Quantity: "1", Price: 1200.99, }, + { + Category: "electronics", + Name: "keyboard", + Quantity: "1", + Price: 100.99, + }, { Category: "electronics", Name: "usb", @@ -68,7 +104,7 @@ func (gs *GroceryServer) GetGrocery(ctx context.Context, in *ec.Category) (*ec.I } } - return &ec.Item{}, nil + return &ec.Item{}, errors.New("category item not found") } func (gs *GroceryServer) ListGrocery(ctx context.Context, in *ec.Category) (*ec.ItemList, error) { @@ -95,14 +131,18 @@ func (gs *GroceryServer) MakePayment(ctx context.Context, in *ec.PaymentRequest) transactionSuccessfull := true var transactionDetails string + change := amount - purchasedItem.Price + + if change < 0 { + transactionSuccessfull = false + } + if transactionSuccessfull { transactionDetails = fmt.Sprintf("Transaction of amount %v is successful", amount) } else { - transactionDetails = fmt.Sprintf("Transaction of amount %v failed", amount) + transactionDetails = fmt.Sprintf("Transaction of amount %v failed, payment missmatch", amount) } - change := amount - purchasedItem.Price - return &ec.PaymentResponse{ Success: transactionSuccessfull, PurchasedItem: purchasedItem, From 0333a3fa0b45cb0277675119f5f5b59cb89fa777 Mon Sep 17 00:00:00 2001 From: encalada Date: Tue, 5 Dec 2023 18:30:49 +0100 Subject: [PATCH 3/4] More optimizations - Add architecture diagram to readme - Refactor proto interface, to inline fields - Add build script - Recompile proto files Signed-off-by: encalada --- grpc/README.md | 19 +++++++++++-------- grpc/build | 21 +++++++++++++++++++++ grpc/client/main.go | 7 +------ grpc/ecommerce/ecommerce.pb.go | 24 ++++++++++++------------ grpc/ecommerce/ecommerce.proto | 2 +- grpc/ecommerce/ecommerce_grpc.pb.go | 24 ++++++++++++------------ grpc/images/grpc-architecture.png | Bin 0 -> 72691 bytes grpc/regenerate-grpc-code.sh | 4 ++-- grpc/run | 12 ++++++------ grpc/server/main.go | 2 +- 10 files changed, 67 insertions(+), 48 deletions(-) create mode 100755 grpc/build create mode 100644 grpc/images/grpc-architecture.png diff --git a/grpc/README.md b/grpc/README.md index 929d538d..22d94ade 100644 --- a/grpc/README.md +++ b/grpc/README.md @@ -4,14 +4,15 @@ 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. +list and buy groceries 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: +See architecture diagram: +![Alt text](images/grpc-architecture.png) ## Source code @@ -19,8 +20,9 @@ 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. + interface for a set of remote procedures that can be called by clients. This directory + can be regenerated upon changes to the `.proto` file, by calling the `./regenerate-grpc-code.sh`. +- `/server` directory hosts the `grocery` interface implementation and creates a gRPC server. - `/client` directory defines an http server and calls the gRPC server via its different handlers. @@ -33,7 +35,8 @@ Once you have selected your Code Engine project, you only need to run: ./run ``` -## 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 +If you want to clean-up resources from this sample app, do not forget to run: + +```sh +./run clean +``` \ No newline at end of file diff --git a/grpc/build b/grpc/build new file mode 100755 index 00000000..cdbc03ef --- /dev/null +++ b/grpc/build @@ -0,0 +1,21 @@ +#!/bin/bash + +set -euo pipefail + +# Env Vars: +# REGISTRY: name of the image registry/namespace to store the images +# +# NOTE: to run this you MUST set the REGISTRY environment variable to +# your own image registry/namespace otherwise the `docker push` commands +# will fail due to an auth failure. Which means, you also need to be logged +# into that registry before you run it. + +export REGISTRY=${REGISTRY:-icr.io/codeengine} + +# Build the images +docker build -f Dockerfile.client -t "${REGISTRY}"/grpc-client . --platform linux/amd64 +docker build -f Dockerfile.server -t "${REGISTRY}"/grpc-server . --platform linux/amd64 + +# And push it +docker push "${REGISTRY}"/grpc-client +docker push "${REGISTRY}"/grpc-server diff --git a/grpc/client/main.go b/grpc/client/main.go index 47389403..34a250ed 100644 --- a/grpc/client/main.go +++ b/grpc/client/main.go @@ -91,12 +91,7 @@ func BuyHandler(w http.ResponseWriter, r *http.Request, groceryClient ec.Grocery Item: item, } - paymentResponse, _ := groceryClient.MakePayment(context.Background(), &paymentRequest) - - // if !paymentResponse.Success { - // Fail(w, "failed to buy grocery, not enough money", errors.New("not enough money")) - // return - // } + paymentResponse, _ := groceryClient.BuyGrocery(context.Background(), &paymentRequest) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(paymentResponse) diff --git a/grpc/ecommerce/ecommerce.pb.go b/grpc/ecommerce/ecommerce.pb.go index 7ec492f9..e33e052e 100644 --- a/grpc/ecommerce/ecommerce.pb.go +++ b/grpc/ecommerce/ecommerce.pb.go @@ -343,22 +343,22 @@ var file_ecommerce_ecommerce_proto_rawDesc = []byte{ 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, + 0xc1, 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, + 0x65, 0x2e, 0x49, 0x74, 0x65, 0x6d, 0x4c, 0x69, 0x73, 0x74, 0x22, 0x00, 0x12, 0x45, 0x0a, 0x0a, + 0x42, 0x75, 0x79, 0x47, 0x72, 0x6f, 0x63, 0x65, 0x72, 0x79, 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 ( @@ -387,10 +387,10 @@ var file_ecommerce_ecommerce_proto_depIdxs = []int32{ 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 + 3, // 5: ecommerce.grocery.BuyGrocery: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 + 4, // 8: ecommerce.grocery.BuyGrocery: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 diff --git a/grpc/ecommerce/ecommerce.proto b/grpc/ecommerce/ecommerce.proto index 8ba2af72..6c518b41 100644 --- a/grpc/ecommerce/ecommerce.proto +++ b/grpc/ecommerce/ecommerce.proto @@ -6,7 +6,7 @@ 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) {}; + rpc BuyGrocery(PaymentRequest) returns (PaymentResponse) {}; } message Category{ diff --git a/grpc/ecommerce/ecommerce_grpc.pb.go b/grpc/ecommerce/ecommerce_grpc.pb.go index c52a2ad9..c2729e99 100644 --- a/grpc/ecommerce/ecommerce_grpc.pb.go +++ b/grpc/ecommerce/ecommerce_grpc.pb.go @@ -24,7 +24,7 @@ const _ = grpc.SupportPackageIsVersion7 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) + BuyGrocery(ctx context.Context, in *PaymentRequest, opts ...grpc.CallOption) (*PaymentResponse, error) } type groceryClient struct { @@ -53,9 +53,9 @@ func (c *groceryClient) ListGrocery(ctx context.Context, in *Category, opts ...g return out, nil } -func (c *groceryClient) MakePayment(ctx context.Context, in *PaymentRequest, opts ...grpc.CallOption) (*PaymentResponse, error) { +func (c *groceryClient) BuyGrocery(ctx context.Context, in *PaymentRequest, opts ...grpc.CallOption) (*PaymentResponse, error) { out := new(PaymentResponse) - err := c.cc.Invoke(ctx, "/ecommerce.grocery/MakePayment", in, out, opts...) + err := c.cc.Invoke(ctx, "/ecommerce.grocery/BuyGrocery", in, out, opts...) if err != nil { return nil, err } @@ -68,7 +68,7 @@ func (c *groceryClient) MakePayment(ctx context.Context, in *PaymentRequest, opt type GroceryServer interface { GetGrocery(context.Context, *Category) (*Item, error) ListGrocery(context.Context, *Category) (*ItemList, error) - MakePayment(context.Context, *PaymentRequest) (*PaymentResponse, error) + BuyGrocery(context.Context, *PaymentRequest) (*PaymentResponse, error) mustEmbedUnimplementedGroceryServer() } @@ -82,8 +82,8 @@ func (UnimplementedGroceryServer) GetGrocery(context.Context, *Category) (*Item, 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) BuyGrocery(context.Context, *PaymentRequest) (*PaymentResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method BuyGrocery not implemented") } func (UnimplementedGroceryServer) mustEmbedUnimplementedGroceryServer() {} @@ -134,20 +134,20 @@ func _Grocery_ListGrocery_Handler(srv interface{}, ctx context.Context, dec func return interceptor(ctx, in, info, handler) } -func _Grocery_MakePayment_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { +func _Grocery_BuyGrocery_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) + return srv.(GroceryServer).BuyGrocery(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/ecommerce.grocery/MakePayment", + FullMethod: "/ecommerce.grocery/BuyGrocery", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(GroceryServer).MakePayment(ctx, req.(*PaymentRequest)) + return srv.(GroceryServer).BuyGrocery(ctx, req.(*PaymentRequest)) } return interceptor(ctx, in, info, handler) } @@ -168,8 +168,8 @@ var Grocery_ServiceDesc = grpc.ServiceDesc{ Handler: _Grocery_ListGrocery_Handler, }, { - MethodName: "MakePayment", - Handler: _Grocery_MakePayment_Handler, + MethodName: "BuyGrocery", + Handler: _Grocery_BuyGrocery_Handler, }, }, Streams: []grpc.StreamDesc{}, diff --git a/grpc/images/grpc-architecture.png b/grpc/images/grpc-architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..f988578039925e2ed8278b628745ebb6490895f5 GIT binary patch literal 72691 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%0A+IYz`AxO=Nn|nP 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 literal 0 HcmV?d00001 diff --git a/grpc/regenerate-grpc-code.sh b/grpc/regenerate-grpc-code.sh index 8661e72f..fb3ade58 100755 --- a/grpc/regenerate-grpc-code.sh +++ b/grpc/regenerate-grpc-code.sh @@ -6,14 +6,14 @@ BASEDIR="$(dirname "$0")" if ! hash protoc >/dev/null 2>&1; then echo "[ERROR] protoc compiler plugin is not installed, bailing out." - echo "[INFO] refer to https://grpc.io/docs/languages/go/quickstart/#prerequisites for instalation guidelines." + echo "[INFO] refer to https://grpc.io/docs/languages/go/quickstart/#prerequisites for installation guidelines." echo exit 1 fi if ! hash protoc-gen-go >/dev/null 2>&1; then echo "[ERROR] protoc-gen-go plugin is not installed, bailing out." - echo "[INFO] refer to https://grpc.io/docs/languages/go/quickstart/#prerequisites for instalation guidelines." + echo "[INFO] refer to https://grpc.io/docs/languages/go/quickstart/#prerequisites for installation guidelines." echo "[INFO] ensure your GOPATH is in your PATH". exit 1 fi diff --git a/grpc/run b/grpc/run index 6b7813fc..fb171b1e 100755 --- a/grpc/run +++ b/grpc/run @@ -37,18 +37,18 @@ 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 +# Query the list of groceries by food category +echo "[INFO] Retrieving available food items" +curl -q "${URL}"/listgroceries/food # Buy an item from food and pay with 5.0 dollars echo "[INFO] Going to buy an apple, paying with 5.0 dollars" -JSON_REPONSE=$(curl -s "${URL}"/buygrocery/vegetables/apple/5.0 | jq '.success') -echo $JSON_REPONSE | jq . +JSON_RESPONSE=$(curl -s "${URL}"/buygrocery/food/apple/5.0 | jq '.success') +echo "${JSON_RESPONSE}" | jq . # Validate payment operation echo "[INFO] Validating payment operation state" -OPERATION_SUCCEEDED=$(echo $JSON_REPONSE | jq '.success') +OPERATION_SUCCEEDED=$(echo "${JSON_RESPONSE}" | jq '.success') if [ "${OPERATION_SUCCEEDED}" == "null" ] then echo "[ERROR] Payment failed, bailing out." diff --git a/grpc/server/main.go b/grpc/server/main.go index 472328cf..adbe34a0 100644 --- a/grpc/server/main.go +++ b/grpc/server/main.go @@ -124,7 +124,7 @@ func (gs *GroceryServer) ListGrocery(ctx context.Context, in *ec.Category) (*ec. return &itemList, errors.New("category not found") } -func (gs *GroceryServer) MakePayment(ctx context.Context, in *ec.PaymentRequest) (*ec.PaymentResponse, error) { +func (gs *GroceryServer) BuyGrocery(ctx context.Context, in *ec.PaymentRequest) (*ec.PaymentResponse, error) { amount := in.GetAmount() purchasedItem := in.GetItem() From 6add6f41fe18a740d5603233f295052df9f59fed Mon Sep 17 00:00:00 2001 From: Enrico Regge Date: Wed, 6 Dec 2023 09:48:48 +0100 Subject: [PATCH 4/4] final touches on the script --- grpc/run | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/grpc/run b/grpc/run index fb171b1e..a654880b 100755 --- a/grpc/run +++ b/grpc/run @@ -1,6 +1,6 @@ #!/bin/bash -set -euo pipefail +set -eo pipefail SERVER_APP_NAME="a-grpc-server" CLIENT_APP_NAME="a-grpc-client" @@ -13,16 +13,15 @@ function clean() { ibmcloud ce app delete --name $SERVER_APP_NAME --force --ignore-not-found > /dev/null 2>&1 } -if [ $# -ge 1 ] && [ -n "$1" ] -then - echo "[INFO] going to clean existing project resources" - clean - exit 0 -fi +echo "[INFO] going to clean existing project resources" +clean + +# In case this script has been executed with `./run clean`, we stop right after the cleanup +[[ "$1" == "clean" ]] && exit 0 # 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 +ibmcloud ce app create --name "${SERVER_APP_NAME}" --port h2c:8080 --image icr.io/codeengine/grpc-server --min-scale 0 echo "[INFO] Retrieving gRPC server local endpoint" SERVER_INTERNAL_ENDPOINT=$(ibmcloud ce app get -n "${SERVER_APP_NAME}" -o project-url | sed 's/http:\/\///') @@ -30,7 +29,7 @@ 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" +ibmcloud ce app create --name "${CLIENT_APP_NAME}" --image icr.io/codeengine/grpc-client --min-scale 0 --env LOCAL_ENDPOINT_WITH_PORT="${SERVER_INTERNAL_ENDPOINT}:80" # Get the client server public endpoint echo "[INFO] Retrieving client/server public endpoint" @@ -43,17 +42,20 @@ curl -q "${URL}"/listgroceries/food # Buy an item from food and pay with 5.0 dollars echo "[INFO] Going to buy an apple, paying with 5.0 dollars" -JSON_RESPONSE=$(curl -s "${URL}"/buygrocery/food/apple/5.0 | jq '.success') -echo "${JSON_RESPONSE}" | jq . +JSON_RESPONSE=$(curl -s "${URL}"/buygrocery/food/apple/5.0) +echo "${JSON_RESPONSE}" # Validate payment operation echo "[INFO] Validating payment operation state" OPERATION_SUCCEEDED=$(echo "${JSON_RESPONSE}" | jq '.success') -if [ "${OPERATION_SUCCEEDED}" == "null" ] +if [ "${OPERATION_SUCCEEDED}" != "true" ] then echo "[ERROR] Payment failed, bailing out." + clean exit 1 -else - echo "[INFO] Successful payment operation." - exit 0 -fi \ No newline at end of file +fi + +clean + +echo "[INFO] Successful payment operation." +exit 0