diff --git a/README.md b/README.md index c935513..15add8c 100644 --- a/README.md +++ b/README.md @@ -66,16 +66,7 @@ minikube start --cpus=4 --memory 4096 --disk-size 32g 4. Run `kubectl get pods` to verify the Pods are ready and running. -5. Access the web frontend through your browser - -- **Minikube** requires you to run a command to access the frontend service: -```shell -minikube service frontend-external -``` - -- **Docker For Desktop** should automatically provide the frontend at - http://localhost:80 - +5. Access the web frontend via port forwarding. ### Cleanup diff --git a/helm-manifests/deploy.sh b/helm-manifests/deploy.sh new file mode 100755 index 0000000..3e1a03c --- /dev/null +++ b/helm-manifests/deploy.sh @@ -0,0 +1,7 @@ +#!/bin/bash +set -e + +pushd . +cd "$(dirname "$0")" +helm install mariadb bitnami/mariadb --version=11.5.0 -f ./values.yaml +popd \ No newline at end of file diff --git a/helm-manifests/values.yaml b/helm-manifests/values.yaml new file mode 100644 index 0000000..1600031 --- /dev/null +++ b/helm-manifests/values.yaml @@ -0,0 +1,35 @@ +initdbScripts: + backup.sql: | + CREATE TABLE `products` ( + `id` VARCHAR(1024), + `name` VARCHAR(1024), + `description` VARCHAR(1024), + `picture` VARCHAR(1024), + `price_usd` JSON, + `categories` JSON + ); + + INSERT INTO `products` VALUES + ('16BEE20109','Honeybee Plush','Adorable plush toy that wants to be snuggled like any other fuzzy.','/static/img/products/bee-plush.jpg','{ "currency_code": "USD", "units": 30, "nanos": 780000000 }','["toy"]'), + ('999SLO4D20','20 sided die','The ultimate observability quest tool.','/static/img/products/d20.jpg','{ "currency_code": "USD", "units": 15, "nanos": 200000000 }','["game"]'), + ('OLJCESPC7Z','Vintage Typewriter','This typewriter looks good in your living room.','/static/img/products/typewriter.jpg','{ "currency_code": "USD", "units": 67, "nanos": 990000000 }','["vintage"]'), + ('66VCHSJNUP','Vintage Camera Lens','You won''t have a camera to use it and it probably doesn''t work anyway.','/static/img/products/camera-lens.jpg','{ "currency_code": "USD", "units": 12, "nanos": 490000000 }','["photography","vintage"]'), + ('1YMWWN1N4O','Home Barista Kit','Always wanted to brew coffee with Chemex and Aeropress at home?','/static/img/products/barista-kit.jpg','{ "currency_code": "USD", "units": 124, "nanos": 0 }','["cookware"]'), + ('DG9ZAG9RCG','Toshok has a Branch','An original ''Toshok has a Branch for that'' t-shirt.','/static/img/products/toshok-branch.jpg','{ "currency_code": "USD", "units": 34, "nanos": 980000000 }','["game"]'), + ('L9ECAV7KIM','Terrarium','This terrarium will looks great in your white painted living room.','/static/img/products/terrarium.jpg','{ "currency_code": "USD", "units": 36, "nanos": 450000000 }','["gardening"]'), + ('2ZYFJ3GM2N','Film Camera','This camera looks like it''s a film camera, but it''s actually digital.','/static/img/products/film-camera.jpg','{ "currency_code": "USD", "units": 2245, "nanos": 0 }','["photography","vintage"]'), + ('0PUK6V6EV0','Vintage Record Player','It still works.','/static/img/products/record-player.jpg','{ "currency_code": "USD", "units": 65, "nanos": 500000000 }','["music","vintage"]'), + ('LS4PSXUNUM','Metal Camping Mug','You probably don''t go camping that often but this is better than plastic cups.','/static/img/products/camp-mug.jpg','{ "currency_code": "USD", "units": 24, "nanos": 330000000 }','["cookware"]'), + ('9SIQT8TOJO','City Bike','This single gear bike probably cannot climb the hills of San Francisco.','/static/img/products/city-bike.jpg','{ "currency_code": "USD", "units": 789, "nanos": 500000000 }','["cycling"]'), + ('6E92ZMYYFZ','Air Plant','Have you ever wondered whether air plants need water? Buy one and figure out.','/static/img/products/air-plant.jpg','{ "currency_code": "USD", "units": 12, "nanos": 300000000 }','["gardening"]'); + +primary: + persistence: + enabled: false + +serviceAccount: + create: false + +auth: + database: productcatalogservice + rootPassword: "root" \ No newline at end of file diff --git a/kubernetes-manifests/adservice.yaml b/kubernetes-manifests/adservice.yaml index db8b1c2..185fc22 100644 --- a/kubernetes-manifests/adservice.yaml +++ b/kubernetes-manifests/adservice.yaml @@ -17,7 +17,7 @@ spec: terminationGracePeriodSeconds: 5 containers: - name: server - image: signadot/adservice:latest + image: adservice:latest ports: - containerPort: 9555 env: diff --git a/kubernetes-manifests/cartservice.yaml b/kubernetes-manifests/cartservice.yaml index 3c2afc3..c4fce93 100644 --- a/kubernetes-manifests/cartservice.yaml +++ b/kubernetes-manifests/cartservice.yaml @@ -17,7 +17,7 @@ spec: terminationGracePeriodSeconds: 5 containers: - name: server - image: signadot/cartservice:latest + image: cartservice:latest ports: - containerPort: 7070 env: diff --git a/kubernetes-manifests/checkoutservice.yaml b/kubernetes-manifests/checkoutservice.yaml index 44712a1..e702569 100644 --- a/kubernetes-manifests/checkoutservice.yaml +++ b/kubernetes-manifests/checkoutservice.yaml @@ -16,7 +16,7 @@ spec: serviceAccountName: default containers: - name: server - image: signadot/checkoutservice:latest + image: checkoutservice:latest ports: - containerPort: 5050 # readinessProbe: diff --git a/kubernetes-manifests/currencyservice.yaml b/kubernetes-manifests/currencyservice.yaml index 10cfdf4..ba0d18d 100644 --- a/kubernetes-manifests/currencyservice.yaml +++ b/kubernetes-manifests/currencyservice.yaml @@ -17,7 +17,7 @@ spec: terminationGracePeriodSeconds: 5 containers: - name: server - image: signadot/currencyservice:latest + image: currencyservice:latest ports: - name: grpc containerPort: 7000 diff --git a/kubernetes-manifests/emailservice.yaml b/kubernetes-manifests/emailservice.yaml index b0446a7..97a9f46 100644 --- a/kubernetes-manifests/emailservice.yaml +++ b/kubernetes-manifests/emailservice.yaml @@ -17,7 +17,7 @@ spec: terminationGracePeriodSeconds: 5 containers: - name: server - image: signadot/emailservice:latest + image: emailservice:latest ports: - containerPort: 8080 env: diff --git a/kubernetes-manifests/frontend.yaml b/kubernetes-manifests/frontend.yaml index ba1ea39..89297ff 100644 --- a/kubernetes-manifests/frontend.yaml +++ b/kubernetes-manifests/frontend.yaml @@ -16,7 +16,7 @@ spec: serviceAccountName: default containers: - name: server - image: signadot/frontend:latest + image: frontend:latest ports: - containerPort: 8080 # readinessProbe: diff --git a/kubernetes-manifests/loadgenerator.yaml b/kubernetes-manifests/loadgenerator.yaml index 5cf2c6d..626b7e0 100644 --- a/kubernetes-manifests/loadgenerator.yaml +++ b/kubernetes-manifests/loadgenerator.yaml @@ -17,7 +17,7 @@ spec: restartPolicy: Always containers: - name: main - image: signadot/loadgenerator:latest + image: loadgenerator:latest env: - name: FRONTEND_ADDR value: http://frontend diff --git a/kubernetes-manifests/paymentservice.yaml b/kubernetes-manifests/paymentservice.yaml index f12135e..bff99dd 100644 --- a/kubernetes-manifests/paymentservice.yaml +++ b/kubernetes-manifests/paymentservice.yaml @@ -17,7 +17,7 @@ spec: terminationGracePeriodSeconds: 5 containers: - name: server - image: signadot/paymentservice:latest + image: paymentservice:latest ports: - containerPort: 50051 env: diff --git a/kubernetes-manifests/productcatalogservice.yaml b/kubernetes-manifests/productcatalogservice.yaml index d7c99a3..32ab6de 100644 --- a/kubernetes-manifests/productcatalogservice.yaml +++ b/kubernetes-manifests/productcatalogservice.yaml @@ -17,10 +17,14 @@ spec: terminationGracePeriodSeconds: 5 containers: - name: server - image: signadot/productcatalogservice:latest + image: productcatalogservice:latest ports: - containerPort: 3550 env: + - name: DB_HOST + value: "mariadb.shop.svc" + - name: DB_PORT + value: "3306" - name: PORT value: "3550" - name: OTEL_EXPORTER_OTLP_ENDPOINT diff --git a/kubernetes-manifests/recommendationservice.yaml b/kubernetes-manifests/recommendationservice.yaml index 6995433..fbbbd33 100644 --- a/kubernetes-manifests/recommendationservice.yaml +++ b/kubernetes-manifests/recommendationservice.yaml @@ -17,7 +17,7 @@ spec: terminationGracePeriodSeconds: 5 containers: - name: server - image: signadot/recommendationservice:latest + image: recommendationservice:latest ports: - containerPort: 8080 # readinessProbe: diff --git a/kubernetes-manifests/shippingservice.yaml b/kubernetes-manifests/shippingservice.yaml index 5746315..aabb843 100644 --- a/kubernetes-manifests/shippingservice.yaml +++ b/kubernetes-manifests/shippingservice.yaml @@ -16,7 +16,7 @@ spec: serviceAccountName: default containers: - name: server - image: signadot/shippingservice:latest + image: shippingservice:latest ports: - containerPort: 50051 env: diff --git a/plugins/productcatalog-mariadb/README.md b/plugins/productcatalog-mariadb/README.md new file mode 100644 index 0000000..b4fe922 --- /dev/null +++ b/plugins/productcatalog-mariadb/README.md @@ -0,0 +1,66 @@ +# ProductCatalog MariaDB Plugin + +This is a resource plugin that provisions a temporary mariadb server for use within a sandbox associated +with the Product Catalog service. + +## Installing the Plugin + +Before installing the plugin, create the required service account and RBAC permissions: + +```sh +kubectl -n signadot create -f ./k8s/mariadb-init.yaml +``` + +Using the `signadot` CLI, register the plugin in Signadot Control Plane: + +```sh +signadot resourceplugin apply -f ./plugin.yaml +``` + +## Using the Plugin + +When creating a Signadot Sandbox, you can request a temporary MariaDB instance +with a specified database name from this plugin by specifying the plugin name +`mariadb` and passing the following parameters. + +Parameter | Description | Example +--------- | ----------- | ------- +`dbname` | The name of the empty database to create | `testdb` + +After the resource is provisioned, the following output keys will be available +for use by forked workloads in the sandbox: + +Output Key | Description | Example +---------- | ----------- | ------- +`provision.host` | The hostname of the database | `testdb-k5ncuujcjllj2.my-namespace.svc` +`provision.port` | The port of the database | `3306` +`provision.root-password` | The password for mariadb root access | `xxj87hd` + +[`example-sandbox.yaml`](./example-sandbox.yaml) is an example of a sandbox that uses this plugin. +To run it, you will need to install the [`example-baseline`](./../example-baseline/) application +in your cluster, and use `signadot` CLI to create the sandbox (replacing `` with your +cluster name, and `` with the namespace where `example-baseline` was deployed): + +```sh +signadot sandbox apply -f ./example-sandbox.yaml --set cluster= --set namespace= +``` + +Now, in the [Signadot Dashboard](https://app.signadot.com/sandboxes), you can follow the status of your sandbox, +and once ready, you will be able to access the preview endpoint, where you will see the added env vars: +`DB_HOST`, `DB_PORT` and `DB_ROOT_PASSWORD`. + + +## Removing the Plugin + +Make sure all sandboxes that used the chart are deleted, so that the plugin gets +a chance to deprovision anything that was provisioned, and then use `signadot` CLI to uninstall the plugin: + +```sh +signadot resourceplugin delete -f ./plugin.yaml +``` + +Finally delete the service account and RBAC permissions: + +```sh +kubectl -n signadot delete -f ./k8s/mariadb-init.yaml +``` \ No newline at end of file diff --git a/plugins/productcatalog-mariadb/k8s/mariadb-init.yaml b/plugins/productcatalog-mariadb/k8s/mariadb-init.yaml new file mode 100644 index 0000000..698e54d --- /dev/null +++ b/plugins/productcatalog-mariadb/k8s/mariadb-init.yaml @@ -0,0 +1,35 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + namespace: signadot + name: sd-productcatalog-mariadb +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + namespace: signadot + name: sd-productcatalog-mariadb +rules: +- apiGroups: [""] + resources: ["pods"] + verbs: ["get", "list", "watch"] +- apiGroups: [""] + resources: ["configmaps", "secrets", "services"] + verbs: ["get", "list", "watch", "create", "delete"] +- apiGroups: ["apps"] + resources: ["statefulsets"] + verbs: ["get", "list", "watch", "create", "delete"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + namespace: signadot + name: sd-productcatalog-mariadb +subjects: +- kind: ServiceAccount + namespace: signadot + name: sd-productcatalog-mariadb +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: sd-productcatalog-mariadb \ No newline at end of file diff --git a/plugins/productcatalog-mariadb/plugin.yaml b/plugins/productcatalog-mariadb/plugin.yaml new file mode 100644 index 0000000..1ff66b1 --- /dev/null +++ b/plugins/productcatalog-mariadb/plugin.yaml @@ -0,0 +1,34 @@ +name: productcatalog-mariadb +spec: + description: | + Provision a MariaDB instance for productcatalog + Sandbox should provide input 'values' for the values.yaml. + + runner: + image: dtzar/helm-kubectl + namespace: signadot + podTemplateOverlay: | + spec: + serviceAccountName: sd-productcatalog-mariadb + + create: + - name: provision + inputs: + - name: values + valueFromSandbox: true + as: + env: VALUES + + outputs: + - name: host + valueFromPath: /tmp/host + - name: port + valueFromPath: /tmp/port + - name: root-password + valueFromPath: /tmp/root-password + + script: "@{embed: ./plugin/provision.sh}" + + delete: + - name: deprovision + script: "@{embed: ./plugin/deprovision.sh}" \ No newline at end of file diff --git a/plugins/productcatalog-mariadb/plugin/deprovision.sh b/plugins/productcatalog-mariadb/plugin/deprovision.sh new file mode 100644 index 0000000..8dafe1f --- /dev/null +++ b/plugins/productcatalog-mariadb/plugin/deprovision.sh @@ -0,0 +1,12 @@ +#!/bin/bash +# exit when any command fails +set -e + +echo "Sandbox id: ${SIGNADOT_SANDBOX_ID}" +echo "Resource name: ${SIGNADOT_RESOURCE_NAME}" + +# Undeploy the temporary DB for this Sandbox. +export NAMESPACE=signadot +RELEASE_NAME="signadot-${SIGNADOT_RESOURCE_NAME,,}-${SIGNADOT_SANDBOX_ID}" +echo "Deleting Helm release: ${RELEASE_NAME}" +helm -n ${NAMESPACE} uninstall "${RELEASE_NAME}" --wait --timeout 5m0s \ No newline at end of file diff --git a/plugins/productcatalog-mariadb/plugin/provision.sh b/plugins/productcatalog-mariadb/plugin/provision.sh new file mode 100644 index 0000000..d410b15 --- /dev/null +++ b/plugins/productcatalog-mariadb/plugin/provision.sh @@ -0,0 +1,35 @@ +#!/bin/bash +# exit when any command fails +set -e + +echo "Provisioning a MariaDB server" +echo "Sandbox id: ${SIGNADOT_SANDBOX_ID}" +echo "Resource name: ${SIGNADOT_RESOURCE_NAME}" +echo "values: ${VALUES}" + +# Install bitnami helm chart repo +helm repo add bitnami https://charts.bitnami.com/bitnami + +# values.yaml +echo "${VALUES}" > ./values.json +yq -P '.' ./values.json --output-format=yaml > ./values.yaml +cat ./values.yaml + +# Deploy a temporary DB for this Sandbox +export NAMESPACE=signadot +RELEASE_NAME="signadot-${SIGNADOT_RESOURCE_NAME,,}-${SIGNADOT_SANDBOX_ID}" +echo "Installing Helm release: ${RELEASE_NAME}" +helm -n ${NAMESPACE} install "${RELEASE_NAME}" bitnami/mariadb \ +--version 11.5.0 --wait --timeout 5m0s \ +--set fullnameOverride="${RELEASE_NAME}-mariadb" \ +-f ./values.yaml + +# Get the generated password, based on instructions from the Helm chart. +MYSQL_ROOT_PASSWORD=$(kubectl -n ${NAMESPACE} get secret "${RELEASE_NAME}-mariadb" -o jsonpath="{.data.mariadb-root-password}" | base64 -d) + +# Populate the outputs +DBHOST="${RELEASE_NAME}-mariadb.${NAMESPACE}.svc" + +echo -n "${DBHOST}" > /tmp/host +echo -n 3306 > /tmp/port +echo -n "${MYSQL_ROOT_PASSWORD}" > /tmp/root-password \ No newline at end of file diff --git a/sandboxes/sandbox-with-mariadb-resource.yaml b/sandboxes/sandbox-with-mariadb-resource.yaml new file mode 100644 index 0000000..c3f0543 --- /dev/null +++ b/sandboxes/sandbox-with-mariadb-resource.yaml @@ -0,0 +1,53 @@ +name: 'sandbox-productcatalogsvc' +spec: + ttl: + duration: 10w + labels: + team: productcatalogservice + owner: foxish + description: sandbox env to test a new productcatalogservice + cluster: demo + resources: + - name: mariadb + plugin: productcatalog-mariadb + params: + values: | + { + "primary": { + "persistence": { + "enabled": false + } + }, + "serviceAccount": { + "create": false + }, + "auth": { + "database": "productcatalogservice", + "rootPassword": "root" + }, + "initdbScripts": { + "backup.sql": "CREATE TABLE `products` (\n `id` VARCHAR(1024),\n `name` VARCHAR(1024),\n `description` VARCHAR(1024),\n `picture` VARCHAR(1024),\n `price_usd` JSON,\n `categories` JSON\n);\n\nINSERT INTO `products` VALUES\n('16BEE20109','Honeybee Plush','Adorable plush toy that wants to be snuggled like any other fuzzy.','/static/img/products/bee-plush.jpg','{ \"currency_code\": \"USD\", \"units\": 35, \"nanos\": 780000000 }','[\"toy\"]');\n" + } + } + forks: + - forkOf: + kind: Deployment + name: productcatalogservice + namespace: shop + customizations: + env: + - name: DB_HOST + valueFrom: + resource: + name: mariadb + outputKey: provision.host + - name: DB_PORT + valueFrom: + resource: + name: mariadb + outputKey: provision.port + endpoints: + - name: hotrod-fe + host: frontend.shop.svc + port: 80 + protocol: http \ No newline at end of file diff --git a/sandboxes/sandbox-with-sqs-resource.yaml b/sandboxes/sandbox-with-sqs-resource.yaml deleted file mode 100644 index af82189..0000000 --- a/sandboxes/sandbox-with-sqs-resource.yaml +++ /dev/null @@ -1,38 +0,0 @@ -name: 'feat-tracking-id-frontend' -spec: - ttl: - duration: 2d - tags: - team: frontend - owner: foxish - description: sandbox env to test a new checkout service change - cluster: demo - resources: - - name: mysqs - plugin: sd-amazon-sqs - params: - region: us-west-2 - forks: - - forkOf: - kind: Deployment - name: frontend - namespace: shop - customizations: - images: - - image: '@{image}' - env: - - name: QUEUE_NAME - valueFrom: - resource: - name: mysqs - outputKey: queue-name - - name: QUEUE_URL - valueFrom: - resource: - name: mysqs - outputKey: queue-url - endpoints: - - name: hotrod-fe - host: frontend.shop.svc - port: 80 - protocol: http diff --git a/src/productcatalogservice/Dockerfile b/src/productcatalogservice/Dockerfile index 6e50e53..adf7cd2 100644 --- a/src/productcatalogservice/Dockerfile +++ b/src/productcatalogservice/Dockerfile @@ -15,7 +15,6 @@ RUN GRPC_HEALTH_PROBE_VERSION=v0.3.6 && \ chmod +x /bin/grpc_health_probe WORKDIR /productcatalogservice COPY --from=builder /productcatalogservice ./server -COPY products.json . EXPOSE 3550 ENTRYPOINT ["/productcatalogservice/server"] diff --git a/src/productcatalogservice/go.mod b/src/productcatalogservice/go.mod index 6f63b24..ed44fc0 100644 --- a/src/productcatalogservice/go.mod +++ b/src/productcatalogservice/go.mod @@ -3,6 +3,7 @@ module github.com/honeycombio/microservices-demo/src/productcatalogservice go 1.17 require ( + github.com/go-sql-driver/mysql v1.7.0 github.com/golang/protobuf v1.5.2 github.com/google/go-cmp v0.5.7 github.com/sirupsen/logrus v1.4.2 @@ -22,6 +23,7 @@ require ( github.com/go-logr/logr v1.2.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect + github.com/jmoiron/sqlx v1.3.5 // indirect github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.4.0 // indirect go.opentelemetry.io/proto/otlp v0.12.0 // indirect diff --git a/src/productcatalogservice/go.sum b/src/productcatalogservice/go.sum index f85af90..2223e8b 100644 --- a/src/productcatalogservice/go.sum +++ b/src/productcatalogservice/go.sum @@ -56,6 +56,9 @@ github.com/go-logr/logr v1.2.2 h1:ahHml/yUpnlb96Rp8HCvtYVPY8ZYpxq3g7UYchIYwbs= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -106,6 +109,8 @@ github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFb github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= +github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= @@ -115,6 +120,8 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxv github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= diff --git a/src/productcatalogservice/products.json b/src/productcatalogservice/products.json deleted file mode 100644 index 0d9ec3b..0000000 --- a/src/productcatalogservice/products.json +++ /dev/null @@ -1,146 +0,0 @@ -{ - "products": [ - { - "id": "16BEE20109", - "name": "Honeybee Plush", - "description": "Adorable plush toy that wants to be snuggled like any other fuzzy.", - "picture": "/static/img/products/bee-plush.jpg", - "priceUsd": { - "currencyCode": "USD", - "units": 29, - "nanos": 790000000 - }, - "categories": ["toy"] - }, - { - "id": "999SLO4D20", - "name": "20 sided die", - "description": "The ultimate observability quest tool.", - "picture": "/static/img/products/d20.jpg", - "priceUsd": { - "currencyCode": "USD", - "units": 15, - "nanos": 200000000 - }, - "categories": ["game"] - }, - { - "id": "OLJCESPC7Z", - "name": "Vintage Typewriter", - "description": "This typewriter looks good in your living room.", - "picture": "/static/img/products/typewriter.jpg", - "priceUsd": { - "currencyCode": "USD", - "units": 67, - "nanos": 990000000 - }, - "categories": ["vintage"] - }, - { - "id": "66VCHSJNUP", - "name": "Vintage Camera Lens", - "description": "You won't have a camera to use it and it probably doesn't work anyway.", - "picture": "/static/img/products/camera-lens.jpg", - "priceUsd": { - "currencyCode": "USD", - "units": 12, - "nanos": 490000000 - }, - "categories": ["photography", "vintage"] - }, - { - "id": "1YMWWN1N4O", - "name": "Home Barista Kit", - "description": "Always wanted to brew coffee with Chemex and Aeropress at home?", - "picture": "/static/img/products/barista-kit.jpg", - "priceUsd": { - "currencyCode": "USD", - "units": 124 - }, - "categories": ["cookware"] - }, - { - "id": "DG9ZAG9RCG", - "name": "Toshok has a Branch", - "description": "An original 'Toshok has a Branch for that' t-shirt.", - "picture": "/static/img/products/toshok-branch.jpg", - "priceUsd": { - "currencyCode": "USD", - "units": 34, - "nanos": 980000000 - }, - "categories": ["game"] - }, - { - "id": "L9ECAV7KIM", - "name": "Terrarium", - "description": "This terrarium will looks great in your white painted living room.", - "picture": "/static/img/products/terrarium.jpg", - "priceUsd": { - "currencyCode": "USD", - "units": 36, - "nanos": 450000000 - }, - "categories": ["gardening"] - }, - { - "id": "2ZYFJ3GM2N", - "name": "Film Camera", - "description": "This camera looks like it's a film camera, but it's actually digital.", - "picture": "/static/img/products/film-camera.jpg", - "priceUsd": { - "currencyCode": "USD", - "units": 2245 - }, - "categories": ["photography", "vintage"] - }, - { - "id": "0PUK6V6EV0", - "name": "Vintage Record Player", - "description": "It still works.", - "picture": "/static/img/products/record-player.jpg", - "priceUsd": { - "currencyCode": "USD", - "units": 65, - "nanos": 500000000 - }, - "categories": ["music", "vintage"] - }, - { - "id": "LS4PSXUNUM", - "name": "Metal Camping Mug", - "description": "You probably don't go camping that often but this is better than plastic cups.", - "picture": "/static/img/products/camp-mug.jpg", - "priceUsd": { - "currencyCode": "USD", - "units": 24, - "nanos": 330000000 - }, - "categories": ["cookware"] - }, - { - "id": "9SIQT8TOJO", - "name": "City Bike", - "description": "This single gear bike probably cannot climb the hills of San Francisco.", - "picture": "/static/img/products/city-bike.jpg", - "priceUsd": { - "currencyCode": "USD", - "units": 789, - "nanos": 500000000 - }, - "categories": ["cycling"] - }, - { - "id": "6E92ZMYYFZ", - "name": "Air Plant", - "description": "Have you ever wondered whether air plants need water? Buy one and figure out.", - "picture": "/static/img/products/air-plant.jpg", - "priceUsd": { - "currencyCode": "USD", - "units": 12, - "nanos": 300000000 - }, - "categories": ["gardening"] - } - ] -} \ No newline at end of file diff --git a/src/productcatalogservice/server.go b/src/productcatalogservice/server.go index e996820..8c35d5a 100644 --- a/src/productcatalogservice/server.go +++ b/src/productcatalogservice/server.go @@ -1,10 +1,19 @@ package main import ( - "bytes" "context" + "encoding/json" "flag" "fmt" + _ "github.com/go-sql-driver/mysql" + "github.com/jmoiron/sqlx" + "math/rand" + "net" + "os" + "os/signal" + "syscall" + "time" + "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/exporters/otlp/otlptrace" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" @@ -13,20 +22,10 @@ import ( sdktrace "go.opentelemetry.io/otel/sdk/trace" semconv "go.opentelemetry.io/otel/semconv/v1.7.0" "go.opentelemetry.io/otel/trace" - "io/ioutil" - "math/rand" - "net" - "os" - "os/signal" - "strings" - "sync" - "syscall" - "time" pb "github.com/honeycombio/microservices-demo/src/productcatalogservice/demo/msdemo" healthpb "google.golang.org/grpc/health/grpc_health_v1" - "github.com/golang/protobuf/jsonpb" "github.com/sirupsen/logrus" "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" @@ -37,10 +36,10 @@ import ( ) var ( - cat pb.ListProductsResponse - catalogMutex *sync.Mutex - log *logrus.Logger + cat pb.ListProductsResponse + log *logrus.Logger + db *sqlx.DB port = "3550" reloadCatalog bool @@ -57,11 +56,6 @@ func init() { TimestampFormat: time.RFC3339Nano, } log.Out = os.Stdout - catalogMutex = &sync.Mutex{} - err := readCatalogFile(&cat) - if err != nil { - log.Warnf("could not parse product catalog") - } } func initOtelTracing(ctx context.Context, log logrus.FieldLogger) *sdktrace.TracerProvider { @@ -111,6 +105,21 @@ func main() { flag.Parse() + var err error + + db, err = sqlx.Open("mysql", + fmt.Sprintf("root:root@(%s:%s)/productcatalogservice", + os.Getenv("DB_HOST"), os.Getenv("DB_PORT"))) + if err != nil { + panic(err) + } + defer db.Close() + + // Connect and check the server version + var version string + db.QueryRow("SELECT VERSION()").Scan(&version) + fmt.Println("Connected to:", version) + sigs := make(chan os.Signal, 1) signal.Notify(sigs, syscall.SIGUSR1, syscall.SIGUSR2) go func() { @@ -148,7 +157,6 @@ func run(port string) string { ) svc := &productCatalog{} - pb.RegisterProductCatalogServiceServer(srv, svc) healthpb.RegisterHealthServer(srv, svc) go func() { @@ -159,35 +167,6 @@ func run(port string) string { type productCatalog struct{} -func readCatalogFile(catalog *pb.ListProductsResponse) error { - catalogMutex.Lock() - defer catalogMutex.Unlock() - catalogJSON, err := ioutil.ReadFile("products.json") - if err != nil { - log.Fatalf("failed to open product catalog json file: %v", err) - return err - } - if err := jsonpb.Unmarshal(bytes.NewReader(catalogJSON), catalog); err != nil { - log.Warnf("failed to parse the catalog JSON: %v", err) - return err - } - log.Info("successfully parsed product catalog json") - - sleepRandom(50) - - return nil -} - -func parseCatalog() []*pb.Product { - if reloadCatalog || len(cat.Products) == 0 { - err := readCatalogFile(&cat) - if err != nil { - return []*pb.Product{} - } - } - return cat.Products -} - func getRandomWaitTime(max int, buckets int) float32 { num := float32(0) val := float32(max / buckets) @@ -202,16 +181,6 @@ func sleepRandom(max int) { time.Sleep((time.Duration(rnd)) * time.Millisecond) } -func mockDatabaseCall(ctx context.Context, maxTime int, name, query string) { - tracer := otel.GetTracerProvider().Tracer("") - ctx, span := tracer.Start(ctx, name) - span.SetAttributes(attribute.String("db.statement", query), - attribute.String("db.name", "productcatalog")) - defer span.End() - - sleepRandom(maxTime) -} - func (p *productCatalog) Check(_ context.Context, _ *healthpb.HealthCheckRequest) (*healthpb.HealthCheckResponse, error) { return &healthpb.HealthCheckResponse{Status: healthpb.HealthCheckResponse_SERVING}, nil } @@ -221,39 +190,77 @@ func (p *productCatalog) Watch(_ *healthpb.HealthCheckRequest, _ healthpb.Health } func (p *productCatalog) ListProducts(ctx context.Context, _ *pb.Empty) (*pb.ListProductsResponse, error) { - mockDatabaseCall(ctx, 40, "SELECT productcatalog.products", "SELECT * FROM products") - return &pb.ListProductsResponse{Products: parseCatalog()}, nil + var products []*pb.Product + rows, err := db.QueryxContext(ctx, "SELECT * FROM products") + if err != nil { + log.Fatalln(err) + } + for rows.Next() { + products = append(products, p.mustTransformRows(rows)) + } + return &pb.ListProductsResponse{Products: products}, nil } func (p *productCatalog) GetProduct(ctx context.Context, req *pb.GetProductRequest) (*pb.Product, error) { span := trace.SpanFromContext(ctx) span.SetAttributes(attribute.String("app.product_id", req.GetId())) - //mockDatabaseCall(ctx, 30, "SELECT productcatalog.products", "SELECT * FROM products WHERE product_id = ?") - sleepRandom(30) - - var found *pb.Product - for i := 0; i < len(parseCatalog()); i++ { - if req.Id == parseCatalog()[i].Id { - found = parseCatalog()[i] - } + rows, err := db.QueryxContext(ctx, "SELECT * FROM products WHERE id = ?", req.GetId()) + if err != nil { + log.Fatalln(err) } - if found == nil { + + if rows.Next() { + return p.mustTransformRows(rows), nil + } else { return nil, status.Errorf(codes.NotFound, "no product with ID %s", req.Id) } - return found, nil } func (p *productCatalog) SearchProducts(ctx context.Context, req *pb.SearchProductsRequest) (*pb.SearchProductsResponse, error) { - mockDatabaseCall(ctx, 50, "SELECT productcatalog.products", "SELECT * FROM products WHERE name LIKE ? OR description LIKE ?") - - // Interpret query as a substring match in name or description. var ps []*pb.Product - for _, p := range parseCatalog() { - if strings.Contains(strings.ToLower(p.Name), strings.ToLower(req.Query)) || - strings.Contains(strings.ToLower(p.Description), strings.ToLower(req.Query)) { - ps = append(ps, p) - } + rows, err := db.QueryxContext(ctx, "SELECT * FROM products WHERE name LIKE ? OR description LIKE ?", req.Query, req.Query) + if err != nil { + log.Fatalln(err) + } + for rows.Next() { + ps = append(ps, p.mustTransformRows(rows)) } return &pb.SearchProductsResponse{Results: ps}, nil } + +func (p *productCatalog) mustTransformRows(rows *sqlx.Rows) *pb.Product { + var product struct { + Id string `db:"id,omitempty"` + Name string `db:"name,omitempty"` + Description string `db:"description,omitempty"` + Picture string `db:"picture,omitempty"` + PriceUsd []byte `db:"price_usd,omitempty"` + Categories []byte `db:"categories,omitempty"` + } + + err := rows.StructScan(&product) + if err != nil { + log.Fatalln(err) + } + + var result pb.Product + var money pb.Money + + result.Id = product.Id + result.Name = product.Name + result.Description = product.Description + result.Picture = product.Picture + + var categories []string + if err := json.Unmarshal(product.PriceUsd, &money); err != nil { + log.Fatalln(err) + } + if err := json.Unmarshal(product.Categories, &categories); err != nil { + log.Fatalln(err) + } + + result.PriceUsd = &money + result.Categories = categories + return &result +}