├── .gitignore ├── Dockerfile.catalog ├── Dockerfile.discount ├── README.md ├── catalog ├── ecommerce │ └── ecommerce.pb.go └── main.go ├── discount ├── ecommerce_pb2.py ├── ecommerce_pb2_grpc.py └── server.py ├── docker-compose.yml ├── ecommerce.proto └── keys ├── cert.pem ├── discount.key ├── discount.pem └── private.key /.gitignore: -------------------------------------------------------------------------------- 1 | venv 2 | *.pyc 3 | -------------------------------------------------------------------------------- /Dockerfile.catalog: -------------------------------------------------------------------------------- 1 | FROM golang:1.11-stretch 2 | ADD . /app/src/microservices-grpc-go-python 3 | ENV GOPATH=/app 4 | 5 | RUN go get -u google.golang.org/grpc \ 6 | && go get -u github.com/golang/protobuf/proto 7 | 8 | WORKDIR /app/src/microservices-grpc-go-python/catalog 9 | 10 | CMD ["go", "run", "main.go"] 11 | -------------------------------------------------------------------------------- /Dockerfile.discount: -------------------------------------------------------------------------------- 1 | FROM grpc/python 2 | ADD . /microservices-grpc-go-python 3 | WORKDIR /microservices-grpc-go-python/discount 4 | RUN pip install --upgrade pip \ 5 | && pip install grpcio grpcio-tools 6 | CMD ["python", "server.py", "11443"] 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | It is a proof of concept based in a fictional e-commerce platform. There are two microservices: 2 | 3 | 1) Catalog: It is written in Go and exposes a REST API that returns a product list. 4 | 2) Discount: It is consumed by Catalog, written in Python and returns the received product with 10% discount applied. 5 | 6 | The full text about it can be found in my blog https://gustavohenrique.com. 7 | 8 | ## Getting Started 9 | 10 | ``` 11 | # clone this repo 12 | cd $HOME 13 | git clone https://github.com/gustavohenrique/microservices-grpc-go-python.git 14 | cd microservices-grpc-go-python 15 | 16 | # rename the keys to work with Docker 17 | rm keys/cert.pem keys/private.key 18 | mv keys/discount.pem keys/cert.pem 19 | mv keys/discount.key keys/private.key 20 | 21 | # run docker compose 22 | docker-compose up -d 23 | ``` 24 | 25 | The ports used are 11443 and 11080. 26 | To test using curl: 27 | 28 | ``` 29 | curl -H 'X-USER-ID: 1' http://localhost:11080/products 30 | ``` 31 | -------------------------------------------------------------------------------- /catalog/ecommerce/ecommerce.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // source: ecommerce.proto 3 | 4 | package ecommerce 5 | 6 | import ( 7 | context "context" 8 | fmt "fmt" 9 | proto "github.com/golang/protobuf/proto" 10 | grpc "google.golang.org/grpc" 11 | math "math" 12 | ) 13 | 14 | // Reference imports to suppress errors if they are not otherwise used. 15 | var _ = proto.Marshal 16 | var _ = fmt.Errorf 17 | var _ = math.Inf 18 | 19 | // This is a compile-time assertion to ensure that this generated file 20 | // is compatible with the proto package it is being compiled against. 21 | // A compilation error at this line likely means your copy of the 22 | // proto package needs to be updated. 23 | const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package 24 | 25 | type Customer struct { 26 | Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` 27 | FirstName string `protobuf:"bytes,2,opt,name=first_name,json=firstName,proto3" json:"first_name,omitempty"` 28 | LastName string `protobuf:"bytes,3,opt,name=last_name,json=lastName,proto3" json:"last_name,omitempty"` 29 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 30 | XXX_unrecognized []byte `json:"-"` 31 | XXX_sizecache int32 `json:"-"` 32 | } 33 | 34 | func (m *Customer) Reset() { *m = Customer{} } 35 | func (m *Customer) String() string { return proto.CompactTextString(m) } 36 | func (*Customer) ProtoMessage() {} 37 | func (*Customer) Descriptor() ([]byte, []int) { 38 | return fileDescriptor_4ba438465af3fc23, []int{0} 39 | } 40 | 41 | func (m *Customer) XXX_Unmarshal(b []byte) error { 42 | return xxx_messageInfo_Customer.Unmarshal(m, b) 43 | } 44 | func (m *Customer) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 45 | return xxx_messageInfo_Customer.Marshal(b, m, deterministic) 46 | } 47 | func (m *Customer) XXX_Merge(src proto.Message) { 48 | xxx_messageInfo_Customer.Merge(m, src) 49 | } 50 | func (m *Customer) XXX_Size() int { 51 | return xxx_messageInfo_Customer.Size(m) 52 | } 53 | func (m *Customer) XXX_DiscardUnknown() { 54 | xxx_messageInfo_Customer.DiscardUnknown(m) 55 | } 56 | 57 | var xxx_messageInfo_Customer proto.InternalMessageInfo 58 | 59 | func (m *Customer) GetId() int32 { 60 | if m != nil { 61 | return m.Id 62 | } 63 | return 0 64 | } 65 | 66 | func (m *Customer) GetFirstName() string { 67 | if m != nil { 68 | return m.FirstName 69 | } 70 | return "" 71 | } 72 | 73 | func (m *Customer) GetLastName() string { 74 | if m != nil { 75 | return m.LastName 76 | } 77 | return "" 78 | } 79 | 80 | type Product struct { 81 | Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` 82 | Slug string `protobuf:"bytes,2,opt,name=slug,proto3" json:"slug,omitempty"` 83 | Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` 84 | PriceInCents int32 `protobuf:"varint,4,opt,name=price_in_cents,json=priceInCents,proto3" json:"price_in_cents,omitempty"` 85 | DiscountValue *DiscountValue `protobuf:"bytes,5,opt,name=discount_value,json=discountValue,proto3" json:"discount_value,omitempty"` 86 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 87 | XXX_unrecognized []byte `json:"-"` 88 | XXX_sizecache int32 `json:"-"` 89 | } 90 | 91 | func (m *Product) Reset() { *m = Product{} } 92 | func (m *Product) String() string { return proto.CompactTextString(m) } 93 | func (*Product) ProtoMessage() {} 94 | func (*Product) Descriptor() ([]byte, []int) { 95 | return fileDescriptor_4ba438465af3fc23, []int{1} 96 | } 97 | 98 | func (m *Product) XXX_Unmarshal(b []byte) error { 99 | return xxx_messageInfo_Product.Unmarshal(m, b) 100 | } 101 | func (m *Product) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 102 | return xxx_messageInfo_Product.Marshal(b, m, deterministic) 103 | } 104 | func (m *Product) XXX_Merge(src proto.Message) { 105 | xxx_messageInfo_Product.Merge(m, src) 106 | } 107 | func (m *Product) XXX_Size() int { 108 | return xxx_messageInfo_Product.Size(m) 109 | } 110 | func (m *Product) XXX_DiscardUnknown() { 111 | xxx_messageInfo_Product.DiscardUnknown(m) 112 | } 113 | 114 | var xxx_messageInfo_Product proto.InternalMessageInfo 115 | 116 | func (m *Product) GetId() int32 { 117 | if m != nil { 118 | return m.Id 119 | } 120 | return 0 121 | } 122 | 123 | func (m *Product) GetSlug() string { 124 | if m != nil { 125 | return m.Slug 126 | } 127 | return "" 128 | } 129 | 130 | func (m *Product) GetDescription() string { 131 | if m != nil { 132 | return m.Description 133 | } 134 | return "" 135 | } 136 | 137 | func (m *Product) GetPriceInCents() int32 { 138 | if m != nil { 139 | return m.PriceInCents 140 | } 141 | return 0 142 | } 143 | 144 | func (m *Product) GetDiscountValue() *DiscountValue { 145 | if m != nil { 146 | return m.DiscountValue 147 | } 148 | return nil 149 | } 150 | 151 | type DiscountValue struct { 152 | Pct float32 `protobuf:"fixed32,1,opt,name=pct,proto3" json:"pct,omitempty"` 153 | ValueInCents int32 `protobuf:"varint,2,opt,name=value_in_cents,json=valueInCents,proto3" json:"value_in_cents,omitempty"` 154 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 155 | XXX_unrecognized []byte `json:"-"` 156 | XXX_sizecache int32 `json:"-"` 157 | } 158 | 159 | func (m *DiscountValue) Reset() { *m = DiscountValue{} } 160 | func (m *DiscountValue) String() string { return proto.CompactTextString(m) } 161 | func (*DiscountValue) ProtoMessage() {} 162 | func (*DiscountValue) Descriptor() ([]byte, []int) { 163 | return fileDescriptor_4ba438465af3fc23, []int{2} 164 | } 165 | 166 | func (m *DiscountValue) XXX_Unmarshal(b []byte) error { 167 | return xxx_messageInfo_DiscountValue.Unmarshal(m, b) 168 | } 169 | func (m *DiscountValue) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 170 | return xxx_messageInfo_DiscountValue.Marshal(b, m, deterministic) 171 | } 172 | func (m *DiscountValue) XXX_Merge(src proto.Message) { 173 | xxx_messageInfo_DiscountValue.Merge(m, src) 174 | } 175 | func (m *DiscountValue) XXX_Size() int { 176 | return xxx_messageInfo_DiscountValue.Size(m) 177 | } 178 | func (m *DiscountValue) XXX_DiscardUnknown() { 179 | xxx_messageInfo_DiscountValue.DiscardUnknown(m) 180 | } 181 | 182 | var xxx_messageInfo_DiscountValue proto.InternalMessageInfo 183 | 184 | func (m *DiscountValue) GetPct() float32 { 185 | if m != nil { 186 | return m.Pct 187 | } 188 | return 0 189 | } 190 | 191 | func (m *DiscountValue) GetValueInCents() int32 { 192 | if m != nil { 193 | return m.ValueInCents 194 | } 195 | return 0 196 | } 197 | 198 | type DiscountRequest struct { 199 | Customer *Customer `protobuf:"bytes,1,opt,name=customer,proto3" json:"customer,omitempty"` 200 | Product *Product `protobuf:"bytes,2,opt,name=product,proto3" json:"product,omitempty"` 201 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 202 | XXX_unrecognized []byte `json:"-"` 203 | XXX_sizecache int32 `json:"-"` 204 | } 205 | 206 | func (m *DiscountRequest) Reset() { *m = DiscountRequest{} } 207 | func (m *DiscountRequest) String() string { return proto.CompactTextString(m) } 208 | func (*DiscountRequest) ProtoMessage() {} 209 | func (*DiscountRequest) Descriptor() ([]byte, []int) { 210 | return fileDescriptor_4ba438465af3fc23, []int{3} 211 | } 212 | 213 | func (m *DiscountRequest) XXX_Unmarshal(b []byte) error { 214 | return xxx_messageInfo_DiscountRequest.Unmarshal(m, b) 215 | } 216 | func (m *DiscountRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 217 | return xxx_messageInfo_DiscountRequest.Marshal(b, m, deterministic) 218 | } 219 | func (m *DiscountRequest) XXX_Merge(src proto.Message) { 220 | xxx_messageInfo_DiscountRequest.Merge(m, src) 221 | } 222 | func (m *DiscountRequest) XXX_Size() int { 223 | return xxx_messageInfo_DiscountRequest.Size(m) 224 | } 225 | func (m *DiscountRequest) XXX_DiscardUnknown() { 226 | xxx_messageInfo_DiscountRequest.DiscardUnknown(m) 227 | } 228 | 229 | var xxx_messageInfo_DiscountRequest proto.InternalMessageInfo 230 | 231 | func (m *DiscountRequest) GetCustomer() *Customer { 232 | if m != nil { 233 | return m.Customer 234 | } 235 | return nil 236 | } 237 | 238 | func (m *DiscountRequest) GetProduct() *Product { 239 | if m != nil { 240 | return m.Product 241 | } 242 | return nil 243 | } 244 | 245 | type DiscountResponse struct { 246 | Product *Product `protobuf:"bytes,1,opt,name=product,proto3" json:"product,omitempty"` 247 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 248 | XXX_unrecognized []byte `json:"-"` 249 | XXX_sizecache int32 `json:"-"` 250 | } 251 | 252 | func (m *DiscountResponse) Reset() { *m = DiscountResponse{} } 253 | func (m *DiscountResponse) String() string { return proto.CompactTextString(m) } 254 | func (*DiscountResponse) ProtoMessage() {} 255 | func (*DiscountResponse) Descriptor() ([]byte, []int) { 256 | return fileDescriptor_4ba438465af3fc23, []int{4} 257 | } 258 | 259 | func (m *DiscountResponse) XXX_Unmarshal(b []byte) error { 260 | return xxx_messageInfo_DiscountResponse.Unmarshal(m, b) 261 | } 262 | func (m *DiscountResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 263 | return xxx_messageInfo_DiscountResponse.Marshal(b, m, deterministic) 264 | } 265 | func (m *DiscountResponse) XXX_Merge(src proto.Message) { 266 | xxx_messageInfo_DiscountResponse.Merge(m, src) 267 | } 268 | func (m *DiscountResponse) XXX_Size() int { 269 | return xxx_messageInfo_DiscountResponse.Size(m) 270 | } 271 | func (m *DiscountResponse) XXX_DiscardUnknown() { 272 | xxx_messageInfo_DiscountResponse.DiscardUnknown(m) 273 | } 274 | 275 | var xxx_messageInfo_DiscountResponse proto.InternalMessageInfo 276 | 277 | func (m *DiscountResponse) GetProduct() *Product { 278 | if m != nil { 279 | return m.Product 280 | } 281 | return nil 282 | } 283 | 284 | func init() { 285 | proto.RegisterType((*Customer)(nil), "ecommerce.Customer") 286 | proto.RegisterType((*Product)(nil), "ecommerce.Product") 287 | proto.RegisterType((*DiscountValue)(nil), "ecommerce.DiscountValue") 288 | proto.RegisterType((*DiscountRequest)(nil), "ecommerce.DiscountRequest") 289 | proto.RegisterType((*DiscountResponse)(nil), "ecommerce.DiscountResponse") 290 | } 291 | 292 | func init() { proto.RegisterFile("ecommerce.proto", fileDescriptor_4ba438465af3fc23) } 293 | 294 | var fileDescriptor_4ba438465af3fc23 = []byte{ 295 | // 347 bytes of a gzipped FileDescriptorProto 296 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x84, 0x92, 0xdf, 0x4a, 0xfb, 0x30, 297 | 0x1c, 0xc5, 0x7f, 0xed, 0xb6, 0xdf, 0xda, 0x6f, 0xdd, 0x1f, 0xe2, 0x4d, 0xd9, 0x10, 0x4a, 0xf1, 298 | 0x62, 0x17, 0x32, 0xa1, 0x3e, 0x80, 0xca, 0x04, 0xd1, 0x0b, 0x91, 0x5c, 0xec, 0xb6, 0xd4, 0x34, 299 | 0x4a, 0xa0, 0x4d, 0x62, 0x92, 0x0a, 0xbe, 0x98, 0xcf, 0x27, 0xcd, 0xd2, 0xad, 0xe2, 0xc0, 0xbb, 300 | 0xf4, 0x9c, 0xc3, 0x27, 0xa7, 0xa7, 0x85, 0x19, 0x25, 0xa2, 0xae, 0xa9, 0x22, 0x74, 0x2d, 0x95, 301 | 0x30, 0x02, 0x85, 0x7b, 0x21, 0xdd, 0x42, 0xb0, 0x69, 0xb4, 0x11, 0x35, 0x55, 0x68, 0x0a, 0x3e, 302 | 0x2b, 0x63, 0x2f, 0xf1, 0x56, 0x23, 0xec, 0xb3, 0x12, 0x9d, 0x01, 0xbc, 0x32, 0xa5, 0x4d, 0xce, 303 | 0x8b, 0x9a, 0xc6, 0x7e, 0xe2, 0xad, 0x42, 0x1c, 0x5a, 0xe5, 0xa9, 0xa8, 0x29, 0x5a, 0x42, 0x58, 304 | 0x15, 0x9d, 0x3b, 0xb0, 0x6e, 0xd0, 0x0a, 0xad, 0x99, 0x7e, 0x79, 0x30, 0x7e, 0x56, 0xa2, 0x6c, 305 | 0x88, 0xf9, 0xc5, 0x45, 0x30, 0xd4, 0x55, 0xf3, 0xe6, 0x88, 0xf6, 0x8c, 0x12, 0x88, 0x4a, 0xaa, 306 | 0x89, 0x62, 0xd2, 0x30, 0xc1, 0x1d, 0xae, 0x2f, 0xa1, 0x73, 0x98, 0x4a, 0xc5, 0x08, 0xcd, 0x19, 307 | 0xcf, 0x09, 0xe5, 0x46, 0xc7, 0x43, 0x4b, 0x3c, 0xb1, 0xea, 0x03, 0xdf, 0xb4, 0x1a, 0xba, 0x86, 308 | 0x69, 0xc9, 0x34, 0x11, 0x0d, 0x37, 0xf9, 0x47, 0x51, 0x35, 0x34, 0x1e, 0x25, 0xde, 0x2a, 0xca, 309 | 0xe2, 0xf5, 0x61, 0x84, 0x3b, 0x17, 0xd8, 0xb6, 0x3e, 0x9e, 0x94, 0xfd, 0xc7, 0xf4, 0x1e, 0x26, 310 | 0x3f, 0x7c, 0x34, 0x87, 0x81, 0x24, 0xc6, 0xd6, 0xf7, 0x71, 0x7b, 0x6c, 0x9b, 0x58, 0xf4, 0xa1, 311 | 0x89, 0xbf, 0x6b, 0x62, 0x55, 0xd7, 0x24, 0x95, 0x30, 0xeb, 0x40, 0x98, 0xbe, 0x37, 0x54, 0x1b, 312 | 0x74, 0x09, 0x01, 0x71, 0x63, 0x5b, 0x5e, 0x94, 0x9d, 0xf6, 0x6a, 0x75, 0xdf, 0x01, 0xef, 0x43, 313 | 0xe8, 0x02, 0xc6, 0x72, 0x37, 0xa2, 0xbd, 0x22, 0xca, 0x50, 0x2f, 0xef, 0xe6, 0xc5, 0x5d, 0x24, 314 | 0xbd, 0x81, 0xf9, 0xe1, 0x46, 0x2d, 0x05, 0xd7, 0xb4, 0x4f, 0xf0, 0xfe, 0x24, 0x64, 0x5b, 0x08, 315 | 0x3a, 0x02, 0x7a, 0x84, 0xc9, 0xad, 0x94, 0xd5, 0xe7, 0x5e, 0x58, 0x1c, 0x99, 0xd0, 0xbd, 0xd9, 316 | 0x62, 0x79, 0xd4, 0xdb, 0x75, 0x48, 0xff, 0xbd, 0xfc, 0xb7, 0xff, 0xdd, 0xd5, 0x77, 0x00, 0x00, 317 | 0x00, 0xff, 0xff, 0x72, 0x4c, 0x08, 0x44, 0x8a, 0x02, 0x00, 0x00, 318 | } 319 | 320 | // Reference imports to suppress errors if they are not otherwise used. 321 | var _ context.Context 322 | var _ grpc.ClientConn 323 | 324 | // This is a compile-time assertion to ensure that this generated file 325 | // is compatible with the grpc package it is being compiled against. 326 | const _ = grpc.SupportPackageIsVersion4 327 | 328 | // DiscountClient is the client API for Discount service. 329 | // 330 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. 331 | type DiscountClient interface { 332 | ApplyDiscount(ctx context.Context, in *DiscountRequest, opts ...grpc.CallOption) (*DiscountResponse, error) 333 | } 334 | 335 | type discountClient struct { 336 | cc *grpc.ClientConn 337 | } 338 | 339 | func NewDiscountClient(cc *grpc.ClientConn) DiscountClient { 340 | return &discountClient{cc} 341 | } 342 | 343 | func (c *discountClient) ApplyDiscount(ctx context.Context, in *DiscountRequest, opts ...grpc.CallOption) (*DiscountResponse, error) { 344 | out := new(DiscountResponse) 345 | err := c.cc.Invoke(ctx, "/ecommerce.Discount/ApplyDiscount", in, out, opts...) 346 | if err != nil { 347 | return nil, err 348 | } 349 | return out, nil 350 | } 351 | 352 | // DiscountServer is the server API for Discount service. 353 | type DiscountServer interface { 354 | ApplyDiscount(context.Context, *DiscountRequest) (*DiscountResponse, error) 355 | } 356 | 357 | func RegisterDiscountServer(s *grpc.Server, srv DiscountServer) { 358 | s.RegisterService(&_Discount_serviceDesc, srv) 359 | } 360 | 361 | func _Discount_ApplyDiscount_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 362 | in := new(DiscountRequest) 363 | if err := dec(in); err != nil { 364 | return nil, err 365 | } 366 | if interceptor == nil { 367 | return srv.(DiscountServer).ApplyDiscount(ctx, in) 368 | } 369 | info := &grpc.UnaryServerInfo{ 370 | Server: srv, 371 | FullMethod: "/ecommerce.Discount/ApplyDiscount", 372 | } 373 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 374 | return srv.(DiscountServer).ApplyDiscount(ctx, req.(*DiscountRequest)) 375 | } 376 | return interceptor(ctx, in, info, handler) 377 | } 378 | 379 | var _Discount_serviceDesc = grpc.ServiceDesc{ 380 | ServiceName: "ecommerce.Discount", 381 | HandlerType: (*DiscountServer)(nil), 382 | Methods: []grpc.MethodDesc{ 383 | { 384 | MethodName: "ApplyDiscount", 385 | Handler: _Discount_ApplyDiscount_Handler, 386 | }, 387 | }, 388 | Streams: []grpc.StreamDesc{}, 389 | Metadata: "ecommerce.proto", 390 | } 391 | -------------------------------------------------------------------------------- /catalog/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "log" 9 | "net/http" 10 | "os" 11 | "path/filepath" 12 | "strconv" 13 | "time" 14 | 15 | "google.golang.org/grpc" 16 | "google.golang.org/grpc/credentials" 17 | 18 | pb "microservices-grpc-go-python/catalog/ecommerce" 19 | ) 20 | 21 | func getDiscountConnection(host string) (*grpc.ClientConn, error) { 22 | wd, _ := os.Getwd() 23 | parentDir := filepath.Dir(wd) 24 | certFile := filepath.Join(parentDir, "keys", "cert.pem") 25 | creds, _ := credentials.NewClientTLSFromFile(certFile, "") 26 | return grpc.Dial(host, grpc.WithTransportCredentials(creds)) 27 | } 28 | 29 | func findCustomerByID(id int) (pb.Customer, error) { 30 | c1 := pb.Customer{Id: 1, FirstName: "John", LastName: "Snow"} 31 | c2 := pb.Customer{Id: 2, FirstName: "Daenerys", LastName: "Targaryen"} 32 | customers := map[int]pb.Customer{ 33 | 1: c1, 34 | 2: c2, 35 | } 36 | found, ok := customers[id] 37 | if ok { 38 | return found, nil 39 | } 40 | return found, errors.New("Customer not found.") 41 | } 42 | 43 | func getFakeProducts() []*pb.Product { 44 | p1 := pb.Product{Id: 1, Slug: "iphone-x", Description: "64GB, black and iOS 12", PriceInCents: 99999} 45 | p2 := pb.Product{Id: 2, Slug: "notebook-avell-g1511", Description: "Notebook Gamer Intel Core i7", PriceInCents: 150000} 46 | p3 := pb.Product{Id: 3, Slug: "playstation-4-slim", Description: "1TB Console", PriceInCents: 32999} 47 | return []*pb.Product{&p1, &p2, &p3} 48 | } 49 | 50 | func getProductsWithDiscountApplied(customer pb.Customer, products []*pb.Product) []*pb.Product { 51 | host := os.Getenv("DISCOUNT_SERVICE_HOST") 52 | if len(host) == 0 { 53 | host = "localhost:11443" 54 | } 55 | conn, err := getDiscountConnection(host) 56 | if err != nil { 57 | log.Fatalf("did not connect: %v", err) 58 | } 59 | defer conn.Close() 60 | 61 | c := pb.NewDiscountClient(conn) 62 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) 63 | defer cancel() 64 | 65 | productsWithDiscountApplied := make([]*pb.Product, 0) 66 | for _, product := range products { 67 | r, err := c.ApplyDiscount(ctx, &pb.DiscountRequest{Customer: &customer, Product: product}) 68 | if err == nil { 69 | productsWithDiscountApplied = append(productsWithDiscountApplied, r.GetProduct()) 70 | } else { 71 | log.Println("Failed to apply discount.", err) 72 | } 73 | } 74 | 75 | if len(productsWithDiscountApplied) > 0 { 76 | return productsWithDiscountApplied 77 | } 78 | return products 79 | } 80 | 81 | func handleGetProducts(w http.ResponseWriter, req *http.Request) { 82 | products := getFakeProducts() 83 | w.Header().Set("Content-Type", "application/json") 84 | 85 | customerID := req.Header.Get("X-USER-ID") 86 | if customerID == "" { 87 | json.NewEncoder(w).Encode(products) 88 | return 89 | } 90 | id, err := strconv.Atoi(customerID) 91 | if err != nil { 92 | http.Error(w, "Customer ID is not a number.", http.StatusBadRequest) 93 | return 94 | } 95 | 96 | customer, err := findCustomerByID(id) 97 | if err != nil { 98 | json.NewEncoder(w).Encode(products) 99 | return 100 | } 101 | 102 | productsWithDiscountApplied := getProductsWithDiscountApplied(customer, products) 103 | json.NewEncoder(w).Encode(productsWithDiscountApplied) 104 | } 105 | 106 | func main() { 107 | port := "11080" 108 | if len(os.Args) > 1 { 109 | port = os.Args[1] 110 | } 111 | 112 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 113 | fmt.Fprintf(w, "It is working.") 114 | }) 115 | http.HandleFunc("/products", handleGetProducts) 116 | 117 | fmt.Println("Server running on", port) 118 | http.ListenAndServe(":"+port, nil) 119 | } 120 | -------------------------------------------------------------------------------- /discount/ecommerce_pb2.py: -------------------------------------------------------------------------------- 1 | # Generated by the protocol buffer compiler. DO NOT EDIT! 2 | # source: ecommerce.proto 3 | 4 | import sys 5 | _b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) 6 | from google.protobuf import descriptor as _descriptor 7 | from google.protobuf import message as _message 8 | from google.protobuf import reflection as _reflection 9 | from google.protobuf import symbol_database as _symbol_database 10 | # @@protoc_insertion_point(imports) 11 | 12 | _sym_db = _symbol_database.Default() 13 | 14 | 15 | 16 | 17 | DESCRIPTOR = _descriptor.FileDescriptor( 18 | name='ecommerce.proto', 19 | package='ecommerce', 20 | syntax='proto3', 21 | serialized_options=None, 22 | serialized_pb=_b('\n\x0f\x65\x63ommerce.proto\x12\tecommerce\"=\n\x08\x43ustomer\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x12\n\nfirst_name\x18\x02 \x01(\t\x12\x11\n\tlast_name\x18\x03 \x01(\t\"\x82\x01\n\x07Product\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x0c\n\x04slug\x18\x02 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x03 \x01(\t\x12\x16\n\x0eprice_in_cents\x18\x04 \x01(\x05\x12\x30\n\x0e\x64iscount_value\x18\x05 \x01(\x0b\x32\x18.ecommerce.DiscountValue\"4\n\rDiscountValue\x12\x0b\n\x03pct\x18\x01 \x01(\x02\x12\x16\n\x0evalue_in_cents\x18\x02 \x01(\x05\"]\n\x0f\x44iscountRequest\x12%\n\x08\x63ustomer\x18\x01 \x01(\x0b\x32\x13.ecommerce.Customer\x12#\n\x07product\x18\x02 \x01(\x0b\x32\x12.ecommerce.Product\"7\n\x10\x44iscountResponse\x12#\n\x07product\x18\x01 \x01(\x0b\x32\x12.ecommerce.Product2V\n\x08\x44iscount\x12J\n\rApplyDiscount\x12\x1a.ecommerce.DiscountRequest\x1a\x1b.ecommerce.DiscountResponse\"\x00\x62\x06proto3') 23 | ) 24 | 25 | 26 | 27 | 28 | _CUSTOMER = _descriptor.Descriptor( 29 | name='Customer', 30 | full_name='ecommerce.Customer', 31 | filename=None, 32 | file=DESCRIPTOR, 33 | containing_type=None, 34 | fields=[ 35 | _descriptor.FieldDescriptor( 36 | name='id', full_name='ecommerce.Customer.id', index=0, 37 | number=1, type=5, cpp_type=1, label=1, 38 | has_default_value=False, default_value=0, 39 | message_type=None, enum_type=None, containing_type=None, 40 | is_extension=False, extension_scope=None, 41 | serialized_options=None, file=DESCRIPTOR), 42 | _descriptor.FieldDescriptor( 43 | name='first_name', full_name='ecommerce.Customer.first_name', index=1, 44 | number=2, type=9, cpp_type=9, label=1, 45 | has_default_value=False, default_value=_b("").decode('utf-8'), 46 | message_type=None, enum_type=None, containing_type=None, 47 | is_extension=False, extension_scope=None, 48 | serialized_options=None, file=DESCRIPTOR), 49 | _descriptor.FieldDescriptor( 50 | name='last_name', full_name='ecommerce.Customer.last_name', index=2, 51 | number=3, type=9, cpp_type=9, label=1, 52 | has_default_value=False, default_value=_b("").decode('utf-8'), 53 | message_type=None, enum_type=None, containing_type=None, 54 | is_extension=False, extension_scope=None, 55 | serialized_options=None, file=DESCRIPTOR), 56 | ], 57 | extensions=[ 58 | ], 59 | nested_types=[], 60 | enum_types=[ 61 | ], 62 | serialized_options=None, 63 | is_extendable=False, 64 | syntax='proto3', 65 | extension_ranges=[], 66 | oneofs=[ 67 | ], 68 | serialized_start=30, 69 | serialized_end=91, 70 | ) 71 | 72 | 73 | _PRODUCT = _descriptor.Descriptor( 74 | name='Product', 75 | full_name='ecommerce.Product', 76 | filename=None, 77 | file=DESCRIPTOR, 78 | containing_type=None, 79 | fields=[ 80 | _descriptor.FieldDescriptor( 81 | name='id', full_name='ecommerce.Product.id', index=0, 82 | number=1, type=5, cpp_type=1, label=1, 83 | has_default_value=False, default_value=0, 84 | message_type=None, enum_type=None, containing_type=None, 85 | is_extension=False, extension_scope=None, 86 | serialized_options=None, file=DESCRIPTOR), 87 | _descriptor.FieldDescriptor( 88 | name='slug', full_name='ecommerce.Product.slug', index=1, 89 | number=2, type=9, cpp_type=9, label=1, 90 | has_default_value=False, default_value=_b("").decode('utf-8'), 91 | message_type=None, enum_type=None, containing_type=None, 92 | is_extension=False, extension_scope=None, 93 | serialized_options=None, file=DESCRIPTOR), 94 | _descriptor.FieldDescriptor( 95 | name='description', full_name='ecommerce.Product.description', index=2, 96 | number=3, type=9, cpp_type=9, label=1, 97 | has_default_value=False, default_value=_b("").decode('utf-8'), 98 | message_type=None, enum_type=None, containing_type=None, 99 | is_extension=False, extension_scope=None, 100 | serialized_options=None, file=DESCRIPTOR), 101 | _descriptor.FieldDescriptor( 102 | name='price_in_cents', full_name='ecommerce.Product.price_in_cents', index=3, 103 | number=4, type=5, cpp_type=1, label=1, 104 | has_default_value=False, default_value=0, 105 | message_type=None, enum_type=None, containing_type=None, 106 | is_extension=False, extension_scope=None, 107 | serialized_options=None, file=DESCRIPTOR), 108 | _descriptor.FieldDescriptor( 109 | name='discount_value', full_name='ecommerce.Product.discount_value', index=4, 110 | number=5, type=11, cpp_type=10, label=1, 111 | has_default_value=False, default_value=None, 112 | message_type=None, enum_type=None, containing_type=None, 113 | is_extension=False, extension_scope=None, 114 | serialized_options=None, file=DESCRIPTOR), 115 | ], 116 | extensions=[ 117 | ], 118 | nested_types=[], 119 | enum_types=[ 120 | ], 121 | serialized_options=None, 122 | is_extendable=False, 123 | syntax='proto3', 124 | extension_ranges=[], 125 | oneofs=[ 126 | ], 127 | serialized_start=94, 128 | serialized_end=224, 129 | ) 130 | 131 | 132 | _DISCOUNTVALUE = _descriptor.Descriptor( 133 | name='DiscountValue', 134 | full_name='ecommerce.DiscountValue', 135 | filename=None, 136 | file=DESCRIPTOR, 137 | containing_type=None, 138 | fields=[ 139 | _descriptor.FieldDescriptor( 140 | name='pct', full_name='ecommerce.DiscountValue.pct', index=0, 141 | number=1, type=2, cpp_type=6, label=1, 142 | has_default_value=False, default_value=float(0), 143 | message_type=None, enum_type=None, containing_type=None, 144 | is_extension=False, extension_scope=None, 145 | serialized_options=None, file=DESCRIPTOR), 146 | _descriptor.FieldDescriptor( 147 | name='value_in_cents', full_name='ecommerce.DiscountValue.value_in_cents', index=1, 148 | number=2, type=5, cpp_type=1, label=1, 149 | has_default_value=False, default_value=0, 150 | message_type=None, enum_type=None, containing_type=None, 151 | is_extension=False, extension_scope=None, 152 | serialized_options=None, file=DESCRIPTOR), 153 | ], 154 | extensions=[ 155 | ], 156 | nested_types=[], 157 | enum_types=[ 158 | ], 159 | serialized_options=None, 160 | is_extendable=False, 161 | syntax='proto3', 162 | extension_ranges=[], 163 | oneofs=[ 164 | ], 165 | serialized_start=226, 166 | serialized_end=278, 167 | ) 168 | 169 | 170 | _DISCOUNTREQUEST = _descriptor.Descriptor( 171 | name='DiscountRequest', 172 | full_name='ecommerce.DiscountRequest', 173 | filename=None, 174 | file=DESCRIPTOR, 175 | containing_type=None, 176 | fields=[ 177 | _descriptor.FieldDescriptor( 178 | name='customer', full_name='ecommerce.DiscountRequest.customer', index=0, 179 | number=1, type=11, cpp_type=10, label=1, 180 | has_default_value=False, default_value=None, 181 | message_type=None, enum_type=None, containing_type=None, 182 | is_extension=False, extension_scope=None, 183 | serialized_options=None, file=DESCRIPTOR), 184 | _descriptor.FieldDescriptor( 185 | name='product', full_name='ecommerce.DiscountRequest.product', index=1, 186 | number=2, type=11, cpp_type=10, label=1, 187 | has_default_value=False, default_value=None, 188 | message_type=None, enum_type=None, containing_type=None, 189 | is_extension=False, extension_scope=None, 190 | serialized_options=None, file=DESCRIPTOR), 191 | ], 192 | extensions=[ 193 | ], 194 | nested_types=[], 195 | enum_types=[ 196 | ], 197 | serialized_options=None, 198 | is_extendable=False, 199 | syntax='proto3', 200 | extension_ranges=[], 201 | oneofs=[ 202 | ], 203 | serialized_start=280, 204 | serialized_end=373, 205 | ) 206 | 207 | 208 | _DISCOUNTRESPONSE = _descriptor.Descriptor( 209 | name='DiscountResponse', 210 | full_name='ecommerce.DiscountResponse', 211 | filename=None, 212 | file=DESCRIPTOR, 213 | containing_type=None, 214 | fields=[ 215 | _descriptor.FieldDescriptor( 216 | name='product', full_name='ecommerce.DiscountResponse.product', index=0, 217 | number=1, type=11, cpp_type=10, label=1, 218 | has_default_value=False, default_value=None, 219 | message_type=None, enum_type=None, containing_type=None, 220 | is_extension=False, extension_scope=None, 221 | serialized_options=None, file=DESCRIPTOR), 222 | ], 223 | extensions=[ 224 | ], 225 | nested_types=[], 226 | enum_types=[ 227 | ], 228 | serialized_options=None, 229 | is_extendable=False, 230 | syntax='proto3', 231 | extension_ranges=[], 232 | oneofs=[ 233 | ], 234 | serialized_start=375, 235 | serialized_end=430, 236 | ) 237 | 238 | _PRODUCT.fields_by_name['discount_value'].message_type = _DISCOUNTVALUE 239 | _DISCOUNTREQUEST.fields_by_name['customer'].message_type = _CUSTOMER 240 | _DISCOUNTREQUEST.fields_by_name['product'].message_type = _PRODUCT 241 | _DISCOUNTRESPONSE.fields_by_name['product'].message_type = _PRODUCT 242 | DESCRIPTOR.message_types_by_name['Customer'] = _CUSTOMER 243 | DESCRIPTOR.message_types_by_name['Product'] = _PRODUCT 244 | DESCRIPTOR.message_types_by_name['DiscountValue'] = _DISCOUNTVALUE 245 | DESCRIPTOR.message_types_by_name['DiscountRequest'] = _DISCOUNTREQUEST 246 | DESCRIPTOR.message_types_by_name['DiscountResponse'] = _DISCOUNTRESPONSE 247 | _sym_db.RegisterFileDescriptor(DESCRIPTOR) 248 | 249 | Customer = _reflection.GeneratedProtocolMessageType('Customer', (_message.Message,), dict( 250 | DESCRIPTOR = _CUSTOMER, 251 | __module__ = 'ecommerce_pb2' 252 | # @@protoc_insertion_point(class_scope:ecommerce.Customer) 253 | )) 254 | _sym_db.RegisterMessage(Customer) 255 | 256 | Product = _reflection.GeneratedProtocolMessageType('Product', (_message.Message,), dict( 257 | DESCRIPTOR = _PRODUCT, 258 | __module__ = 'ecommerce_pb2' 259 | # @@protoc_insertion_point(class_scope:ecommerce.Product) 260 | )) 261 | _sym_db.RegisterMessage(Product) 262 | 263 | DiscountValue = _reflection.GeneratedProtocolMessageType('DiscountValue', (_message.Message,), dict( 264 | DESCRIPTOR = _DISCOUNTVALUE, 265 | __module__ = 'ecommerce_pb2' 266 | # @@protoc_insertion_point(class_scope:ecommerce.DiscountValue) 267 | )) 268 | _sym_db.RegisterMessage(DiscountValue) 269 | 270 | DiscountRequest = _reflection.GeneratedProtocolMessageType('DiscountRequest', (_message.Message,), dict( 271 | DESCRIPTOR = _DISCOUNTREQUEST, 272 | __module__ = 'ecommerce_pb2' 273 | # @@protoc_insertion_point(class_scope:ecommerce.DiscountRequest) 274 | )) 275 | _sym_db.RegisterMessage(DiscountRequest) 276 | 277 | DiscountResponse = _reflection.GeneratedProtocolMessageType('DiscountResponse', (_message.Message,), dict( 278 | DESCRIPTOR = _DISCOUNTRESPONSE, 279 | __module__ = 'ecommerce_pb2' 280 | # @@protoc_insertion_point(class_scope:ecommerce.DiscountResponse) 281 | )) 282 | _sym_db.RegisterMessage(DiscountResponse) 283 | 284 | 285 | 286 | _DISCOUNT = _descriptor.ServiceDescriptor( 287 | name='Discount', 288 | full_name='ecommerce.Discount', 289 | file=DESCRIPTOR, 290 | index=0, 291 | serialized_options=None, 292 | serialized_start=432, 293 | serialized_end=518, 294 | methods=[ 295 | _descriptor.MethodDescriptor( 296 | name='ApplyDiscount', 297 | full_name='ecommerce.Discount.ApplyDiscount', 298 | index=0, 299 | containing_service=None, 300 | input_type=_DISCOUNTREQUEST, 301 | output_type=_DISCOUNTRESPONSE, 302 | serialized_options=None, 303 | ), 304 | ]) 305 | _sym_db.RegisterServiceDescriptor(_DISCOUNT) 306 | 307 | DESCRIPTOR.services_by_name['Discount'] = _DISCOUNT 308 | 309 | # @@protoc_insertion_point(module_scope) 310 | -------------------------------------------------------------------------------- /discount/ecommerce_pb2_grpc.py: -------------------------------------------------------------------------------- 1 | # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! 2 | import grpc 3 | 4 | import ecommerce_pb2 as ecommerce__pb2 5 | 6 | 7 | class DiscountStub(object): 8 | # missing associated documentation comment in .proto file 9 | pass 10 | 11 | def __init__(self, channel): 12 | """Constructor. 13 | 14 | Args: 15 | channel: A grpc.Channel. 16 | """ 17 | self.ApplyDiscount = channel.unary_unary( 18 | '/ecommerce.Discount/ApplyDiscount', 19 | request_serializer=ecommerce__pb2.DiscountRequest.SerializeToString, 20 | response_deserializer=ecommerce__pb2.DiscountResponse.FromString, 21 | ) 22 | 23 | 24 | class DiscountServicer(object): 25 | # missing associated documentation comment in .proto file 26 | pass 27 | 28 | def ApplyDiscount(self, request, context): 29 | # missing associated documentation comment in .proto file 30 | pass 31 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 32 | context.set_details('Method not implemented!') 33 | raise NotImplementedError('Method not implemented!') 34 | 35 | 36 | def add_DiscountServicer_to_server(servicer, server): 37 | rpc_method_handlers = { 38 | 'ApplyDiscount': grpc.unary_unary_rpc_method_handler( 39 | servicer.ApplyDiscount, 40 | request_deserializer=ecommerce__pb2.DiscountRequest.FromString, 41 | response_serializer=ecommerce__pb2.DiscountResponse.SerializeToString, 42 | ), 43 | } 44 | generic_handler = grpc.method_handlers_generic_handler( 45 | 'ecommerce.Discount', rpc_method_handlers) 46 | server.add_generic_rpc_handlers((generic_handler,)) 47 | -------------------------------------------------------------------------------- /discount/server.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import time 3 | import os 4 | import grpc 5 | import decimal 6 | import ecommerce_pb2 7 | import ecommerce_pb2_grpc 8 | from concurrent import futures 9 | 10 | 11 | class Ecommerce(ecommerce_pb2_grpc.DiscountServicer): 12 | def ApplyDiscount(self, request, content): 13 | customer = request.customer 14 | product = request.product 15 | discount = ecommerce_pb2.DiscountValue() 16 | if customer.id == 1 and product.price_in_cents > 0: 17 | percentual = decimal.Decimal(10) / 100 # save 10% 18 | price = decimal.Decimal(product.price_in_cents) / 100 19 | new_price = price - (price * percentual) 20 | value_in_cents = int(new_price * 100) 21 | discount = ecommerce_pb2.DiscountValue(pct=percentual, value_in_cents=value_in_cents) 22 | 23 | new_product = ecommerce_pb2.Product(id=product.id, 24 | slug=product.slug, 25 | description=product.description, 26 | price_in_cents=product.price_in_cents, 27 | discount_value=discount) 28 | return ecommerce_pb2.DiscountResponse(product=new_product) 29 | 30 | 31 | def get_server(host): 32 | server = grpc.server(futures.ThreadPoolExecutor(max_workers=5)) 33 | keys_dir = os.path.abspath(os.path.join('.', os.pardir, 'keys')) 34 | with open('%s/private.key' % keys_dir, 'rb') as f: 35 | private_key = f.read() 36 | with open('%s/cert.pem' % keys_dir, 'rb') as f: 37 | certificate_chain = f.read() 38 | server_credentials = grpc.ssl_server_credentials(((private_key, certificate_chain),)) 39 | server.add_secure_port(host, server_credentials) 40 | ecommerce_pb2_grpc.add_DiscountServicer_to_server(Ecommerce(), server) 41 | return server 42 | 43 | 44 | if __name__ == '__main__': 45 | port = sys.argv[1] if len(sys.argv) > 1 else 443 46 | host = '[::]:%s' % port 47 | server = get_server(host) 48 | try: 49 | server.start() 50 | print('Running Discount service on %s' % host) 51 | while True: 52 | time.sleep(1) 53 | except Exception as e: 54 | print('[error] %s' % e) 55 | server.stop(0) 56 | 57 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | discount: 4 | build: 5 | context: . 6 | dockerfile: Dockerfile.discount 7 | ports: 8 | - 11443:11443 9 | catalog: 10 | build: 11 | context: . 12 | dockerfile: Dockerfile.catalog 13 | environment: 14 | - DISCOUNT_SERVICE_HOST=discount:11443 15 | links: 16 | - discount:discount 17 | depends_on: 18 | - discount 19 | ports: 20 | - 11080:11080 21 | -------------------------------------------------------------------------------- /ecommerce.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package ecommerce; 4 | 5 | service Discount { 6 | rpc ApplyDiscount (DiscountRequest) returns (DiscountResponse) {} 7 | } 8 | 9 | message Customer { 10 | int32 id = 1; 11 | string first_name = 2; 12 | string last_name = 3; 13 | } 14 | 15 | message Product { 16 | int32 id = 1; 17 | string slug = 2; 18 | string description = 3; 19 | int32 price_in_cents = 4; 20 | DiscountValue discount_value = 5; 21 | } 22 | 23 | message DiscountValue { 24 | float pct = 1; 25 | int32 value_in_cents = 2; 26 | } 27 | 28 | 29 | message DiscountRequest { 30 | Customer customer = 1; 31 | Product product = 2; 32 | } 33 | 34 | message DiscountResponse { 35 | Product product = 1; 36 | } 37 | -------------------------------------------------------------------------------- /keys/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIE+zCCAuOgAwIBAgIJAILqTGN00kZBMA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNV 3 | BAMMCWxvY2FsaG9zdDAeFw0xODExMDUxOTQwMTNaFw0xOTExMDUxOTQwMTNaMBQx 4 | EjAQBgNVBAMMCWxvY2FsaG9zdDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC 5 | ggIBAO0MGZXzu+lrHfEezMoDZPsFhRBUKN0YJeP7xHnIK64bt6khUuiRDetV5mCJ 6 | IXGRs5vscoIlsGKZ3mjws+QB0WQELNlORFuGqLHZ4fdUASWAqoDLncePeAWVMkwj 7 | sZIQhyhDYwwbeOmk2HtpIKg4lk9rbx0giXaKEvkOJUeCuH976E4SHlonXbqsHDYP 8 | /XDVA6WZ0T9ViVUCeoKExQmxcoporwiUcJpiNRI5UmZ1liJCjBtW/6Ahd0jXYJIM 9 | mdVhUWUwiclB8lfhYw09i1sofZ9mV5HvVs7+6dzdgxSyybtk17VtPdoCSPyt80Xc 10 | Edg7AVvJMme4RXExJ96SUSA8PiSer6lojceMD/ELX3Ph0VwJxMI4yPX1WRw/s9dU 11 | qeu+GWJidAB693+yH2IgCXd+WvpPl3KW9BjZRpNWkOn95r4/quSAGjAvajOMaNpU 12 | cqersg/bh9jdxQVop/L62YLgpsvsexPnHngx4b3W9L/QSYo36mPTkcurVjy52LxD 13 | hWphTsmyqzEUGIqEiPrarNJJzOdZzKvroJHhYXdrA5lJRtMyCWp9uzIIk6D+7OZc 14 | dJU0/k6VggrGW7I8DznciEhP6OVmaM8L8dm9wvPIBHTInlzriH3gR7+KNB+J0tty 15 | 6U+GGUnhSzjqmwk7OictZX4i1qnoiAwBwIrnNJBqbBoh2nH3AgMBAAGjUDBOMB0G 16 | A1UdDgQWBBThODu7lGu8A30255KnGonhyK1yljAfBgNVHSMEGDAWgBThODu7lGu8 17 | A30255KnGonhyK1yljAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQBQ 18 | UbtxY29WRG2S1/C0/XogKhCbVbMhkXq7aPcoDYj4qqx6b+ksAk56eCvqDzmNhhcu 19 | nM9C7ppU8b8UxcBuh/PcY/AHsj2g7cC7+wNdFIFAQZCuhiwBo1E2/6x6qff6aHqY 20 | hS2crMfv9pzh8wPHQKmhCTurg6w/XPka5e/ihQZbtf6DReZTiK4fVxKW4xOBzKDn 21 | cGBvd2AI89bWxqeoBCFpr2jbKhHArnvIFQLO/W1mGlJNgANtRbm2pTmashMvpSo3 22 | UAy1046SlNe0mgeD/KKUzHPkewlQ2f3PBKIq3mmUGGqsj1p8uHBTaPSc+mFnc7LK 23 | 29JvSRu2kw4ArPMGv0r8JVzl9aKLMF4vprZVyf727HcPKiGU60++YOV/k4b3okCS 24 | 4jXKYIVJcgSGHfJOjOTgqd0YWbHT+SDSBAt6Opufs92ayXfeuOkQ2eRJQzeLJrWx 25 | 0TOdJfkoU0PdFM0BN3WMB2m5qUoiKDQwGq7HNq/KfgUBa5Z665l2eukTkTkKlT0+ 26 | 3jucqQlfoAHTSlGggb5varEquqectV35jpDrRemIISUUMrKhuPVkFhxMCe1zXsFE 27 | pae98h8b8uw+AlSoe9SPVoBC0UH/B/xGxPywt+Zzk/RPK4+nlY00mwKSe/12v5Xo 28 | DghTZgvSbYFNZH3L2yql319vYMUQnF4yDVvMQrTpPA== 29 | -----END CERTIFICATE----- 30 | -------------------------------------------------------------------------------- /keys/discount.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | 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 51 | xh1JrgwgRKcPPhl65g8CRYs8xLNaFA== 52 | -----END PRIVATE KEY----- 53 | -------------------------------------------------------------------------------- /keys/discount.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIE+TCCAuGgAwIBAgIJAPiZ+tY25tfsMA0GCSqGSIb3DQEBCwUAMBMxETAPBgNV 3 | BAMMCGRpc2NvdW50MB4XDTE4MTExMTAwNDE1OFoXDTE5MTExMTAwNDE1OFowEzER 4 | MA8GA1UEAwwIZGlzY291bnQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC 5 | AQDUug34hyC1zTA5o0efGUN+5aSGowe1A9oRFPPJWs16qGxeE/JmJReIlcJOpglD 6 | SE9bjH7/aFZ0UOWcdsDSAtLkjJkO5fyj6kO3ccTuMtOw7Hdryl5zJ3ywiX+u/VlQ 7 | J6ZZ0dXmf6UMy0dsHRlqAmBgH4Ra+Lclj0+8Azk4mHwvKvdlkGGbHuz6vk88wGQ4 8 | 3JishGWp0xKewfVUkj+9P+QD48JmgCTJhp8J1kAwGbHA+btuLPYb3LWoV4Vhr+r3 9 | Rb3ON/hl4fZYJpgho0OdeFcIoYOAjATITIT4528oXd80jWdMK4dQq68zyaWlK9ZN 10 | lpbm2wW8I4Z3J8bvtII0aEJ/gt1ssM7xuyC5loZ+rgwuEkui6OWa9VM/UtXkk8lA 11 | J7XJbzQNT/52jwKVb/WdYcQyfVo8gFUvcI9rqa+/Pl0Ba/Gcpk6JhX6TD7WOc2im 12 | h3wDeMCkhCHr57Tf+2ZyCU7klAZyQw0sQjA4x2da2I5L4A+O9yFE4B7wadBxM8JI 13 | 7WphA8fwbQuiWauyBWSH6tZ+e3ssOFzPR/h6LKB4+1gQxKjawRXH3dCMIb0ezdiG 14 | u+Ys0SC4QoPuW1AvIo0x0804yTNm6UXUt1yp19U9ULlMAUdrUjjp/Ux12rkjNosq 15 | BifjSga3p46OztlnE2GkbJvss70V5Zbxaf7v6I2x+Edq/QIDAQABo1AwTjAdBgNV 16 | HQ4EFgQUonT4Mq2jwsxrNInGzk3g3osB2kowHwYDVR0jBBgwFoAUonT4Mq2jwsxr 17 | NInGzk3g3osB2kowDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAQcOd 18 | blZisPEXuYvZhM/+75uZZiAHeDH/EHtHAPBCLXCzNZD0zAXVgMpcJFIzBHoO4h/E 19 | YL+qxW+kgJevsgwe45AI5UuRe0qRJbmZHJ4QF7dfFFsuBdnul63GinbV4PfK9RBC 20 | SwLbf/n53olbmf2imcYfh4Bn37Y1Jse2k4OmCpXChdgh0Ng+S/a6621wjwC0Vnnt 21 | PY1zm1W3wxzeCF1BsuQzN9N5/8aXBUW15R7x0ni5/TIwM56ALNfOwkvX6wwyKDwT 22 | RrDchj0I8EljsO+EH/W7BRzh4/BEd9X4S4Fjrtzjcy0Q/MxubBuPXkBOTj5p4bIc 23 | ze8HkcjbpSoJKE73PgmywlZgM3xjhvgLDsfBT2l7TXNa/gRBtLmV9BxkkiivCVJh 24 | /ugTmvp09mH+YfOjijdB6gQWQDhmpZ1/qKzaqKxyf7VewpNy+eDBPWpW7FaxiZS+ 25 | 3NRHJKjYaEKgJ5DNdy4y+wet/8kfwQ/o7fSP5tpKtiJuG8WSljZVjRA63umY2rI3 26 | xyDUzHEIq7WJQPsya9tYrWQ+xLevLvoYwW+xqEQ+5RxXjIrxyGvX+tG8wJYy+xUR 27 | roZT8R+o1lr56kAwnt7Jw/TT4KHiZBt69Kgo7TxLmdpIiptyrr5VFcmcdiejtJvD 28 | +G/YnXyCg6hGljYdmCqCrX2j4Yi8TruGsiCAE8k= 29 | -----END CERTIFICATE----- 30 | -------------------------------------------------------------------------------- /keys/private.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQDtDBmV87vpax3x 3 | HszKA2T7BYUQVCjdGCXj+8R5yCuuG7epIVLokQ3rVeZgiSFxkbOb7HKCJbBimd5o 4 | 8LPkAdFkBCzZTkRbhqix2eH3VAElgKqAy53Hj3gFlTJMI7GSEIcoQ2MMG3jppNh7 5 | aSCoOJZPa28dIIl2ihL5DiVHgrh/e+hOEh5aJ126rBw2D/1w1QOlmdE/VYlVAnqC 6 | hMUJsXKKaK8IlHCaYjUSOVJmdZYiQowbVv+gIXdI12CSDJnVYVFlMInJQfJX4WMN 7 | PYtbKH2fZleR71bO/unc3YMUssm7ZNe1bT3aAkj8rfNF3BHYOwFbyTJnuEVxMSfe 8 | klEgPD4knq+paI3HjA/xC19z4dFcCcTCOMj19VkcP7PXVKnrvhliYnQAevd/sh9i 9 | IAl3flr6T5dylvQY2UaTVpDp/ea+P6rkgBowL2ozjGjaVHKnq7IP24fY3cUFaKfy 10 | +tmC4KbL7HsT5x54MeG91vS/0EmKN+pj05HLq1Y8udi8Q4VqYU7JsqsxFBiKhIj6 11 | 2qzSScznWcyr66CR4WF3awOZSUbTMglqfbsyCJOg/uzmXHSVNP5OlYIKxluyPA85 12 | 3IhIT+jlZmjPC/HZvcLzyAR0yJ5c64h94Ee/ijQfidLbculPhhlJ4Us46psJOzon 13 | LWV+Itap6IgMAcCK5zSQamwaIdpx9wIDAQABAoICAHXoLNnPcEp8Q1pp7YcWBSZu 14 | 6m7izaibmE08L9A7Zq6ljscLkkenzvqdsYeW7hGlXWdTRunBgWiqDFy6TWA+Hz7W 15 | bNByA8JaypIcRC9Xk3Jp+2Uw4nweup5cRbZlkq+KlQ/L5Ppit9PPcPaBpgLGks1S 16 | LOSaCIXzy9gh1x0owkh3A2fBz2McbZyqeyXQ8kkrSzMVvWT5UOP95Z4CGFfn8Ycy 17 | 5s0nuJKyAp/b70aTemf0HeiDb4bg3opWxuweZG4kh+4DJKX8rWRU8YXmdOiUzVd5 18 | +ljWaaFwXjbD361t+LXDRSAzBC7qMA+vHnBDNIUPZG+ZNf8tQy04PBKTSNL15Box 19 | 29dfNGWKVG2VLF8vH0rjcI1Kyt0n0RuEgoEdGu2pYYOUzKDBaMK0WS+OWNHMXAfJ 20 | YSuGyOOh42sSSbPvGArqBjGwuLulqwzRYDCBVNa/UO12Py8VianjpYSuxOD9po4k 21 | hOlq0DHaCJN0+vhuazQZbvvUNCamjpq9LR1q/CdeCGsiFYE4M94CfJkp7Vjlzhow 22 | lDq2H1v83B4to2d+pg0Oys8dgKIykPVHppvTomVjOWJ9T5uWS3M/GIwED4RiP7Pw 23 | F/OwuaUuG4VTx10NMGi6sA6L4xhg2B+AtfyuPZwcrHTu7oFt6tAzkRe1b+U2Vxc2 24 | rgDTgowkT0ci+/E1vaRhAoIBAQD28B9aZTTVK3XWezPWBl7qJOElmYQMNu+c2TnD 25 | gLSBaciKbD3X8J8GsbH84xy8+HoLkfNELiPCfA53eFWDKXRcfwh/8Lgp6p4fAz8A 26 | z0/EYei4p9tc3dQiA7fmW6RfwIZiTlxw+gsi/MQQjTcsPWWhPdgQGQGAXvVyOY1V 27 | jYxT2AaDkJpoqVPMPzERH1eTBpd4j3fNVo9kmdOjpD8otZ1b1oBpAT0znzgikbTo 28 | pVU19Wp1pMhvt57qhN71Pzzs/QChLKti7Ot/j/OG/PwGsJDLPQZ/A1UlCTXeTtIe 29 | JDJUvsR7y/Wsi81IZL0Ow2UWTiF+2oUXi/vO3wjrjGEVmcL5AoIBAQD1vw70kw6m 30 | CiltEvVdnrZMGA85ymfSX2tO2wLYc9DH/kRl68phPHedr6fxM0OcSZLd5/ZI2/V5 31 | v9jHHzDQx5uTh97GA2S3DgGcOcldvhM/0lqAv1+Fo/0q0HUpDg57kBd+35kqdK5R 32 | HA/0wDue+iOkJEQ+G7TVNuEobgx8BWNOEP2vzDv/mNGNN14FX+iKVUnHxovyO1rN 33 | otDLpd2C9MquMg0eo5/Fla9cxfhPbn1xZz4B2TMmqkh7DedkqH1CoNS0Td+lVV1C 34 | ItPnhzjD1Wfk+x4y75k3eTj7Lt1uAg1r1w+WQSQDvUrh6Fi2dIc6ZNXlCu/2smbt 35 | RX9Pep5heChvAoIBAAWeMuhokwoigfzOMsC0xmYHTsP/ORzaBxuHaaQEApdLI8/a 36 | ZJHMHKIXWthJBndaI9Stjl5HunKLRfz71N42DDqqfTQD8vypJM3J0h4fmU37eELz 37 | Nq9nMJNRiFaKUTQIekY0SDAd0DEMlR1XSSENiIlhkc/T8c/M2UKvAoMmxEGIEaXe 38 | nVSyPYDREzmzf9eNd9a7VNtsE7kTMthvPSyc7SaQu70n1Q+emLVYoUgFsumWWsSw 39 | agr4n3nwae4kcStnGiOQk5mdkEIHsV+p08YHMFQfYE11cvNzwTD8lBUxd5+r82Zp 40 | nn49c/8oo5B5a4xVXLe5GvwNKD5tnpNVT0QhimkCggEBAMom48+PFTLDEzbVrJ/Z 41 | yk4oUIYSmXs14RkaEmoqQ2nxISTSZUW9rww5DibdK9Xps/X1NeTThEgl59ZNMyPb 42 | v4AJ+djbu8LVs79mzd3eWQlcKfTU+Gf/8WeB2Y7vMDy22I0WtHF3UFoKgpmsdJ8f 43 | V6hT6QtKUWQ/Y9KVTJHNANacJYOytvxYIrFPBXnYXntFE49SJZca+mREdgvAIsry 44 | QbQwGhjFMs4fhwUDGXOGCYz7B8gPewNoen03f8yOyZPAp5i2oq3n3fJkofpIgwqd 45 | h3yWkk4GSPyYLKZ788zlxVKbtAe/CDhHQ804C5nzm7YOcuGaMLG4KfEvBT5FOPon 46 | FOMCggEBAItTfuur/LUdYSvcKN7lN5/zcifP6UjxmzQQgX871D32+VGaGwkOLbB/ 47 | 88NRbcqwUVWVEefIufzgNO0ukYNVK0mhqaBFuY+awZKImsZjIrGKEXJOzZroQfh+ 48 | RBbokzZGjBZW/XODPDTYphFSbmXmhH0lHQxZcO7Djo9Tt+dTP71klupT+Fp3GSSm 49 | 8Y6tKswy4yj9rBo2VSToayCje+htGjwr2t3NoTaLYhLxwV+VO1Qa+b8CKzqPRpVn 50 | TryA8csWk7Y49MlT33IlBHZNMQVFY1dbhBLzfbxp2BuVLG1D7Mgmbmt5cETsovr+ 51 | T7gYePtBvSJWlMsreDGpLM2zQU2C9Is= 52 | -----END PRIVATE KEY----- 53 | --------------------------------------------------------------------------------