diff --git a/README.md b/README.md index 3452b20..9543914 100644 --- a/README.md +++ b/README.md @@ -1 +1,68 @@ -Please see Garden's official [Quickstart Guide](https://docs.garden.io/basics/quickstart) for the up-to-date instructions on how to use this repository. \ No newline at end of file +

+ + + + Garden + +

+
+ Website +   •   + Docs +   •   + Examples +   •   + Blog +   •   + Discord +
+ +## Welcome to Garden's Quickstart Example 👋 + +This repository contains the Garden Quickstart example. Please see our [Quickstart Guide](https://docs.garden.io/basics/quickstart) for step-by-step instructions on how to deploy this project. If you see any issues or bugs, kindly report them +to the [main Garden repo](https://github.com/garden-io/garden/issues/new). + +![Deploying the quickstart example](https://github.com/garden-io/quickstart-example/assets/5373776/5bde4656-0c6f-4ace-ad17-7f5feb4d9c23) + +## About the Project + +This project is a voting application that's meant to resemble a typical (if simplified) microservice architecture that runs on Kubernetes. +The goal is to demonstrate how you can use Garden to build, develop, and test applications like this. + +The project also doubles as an interactive guide that walks you through some common Garden commands and workflows. We encourage you to give it a spin! + +It's a good reference for how to _configure_ a Garden project but please don't take the application source code too seriously, +it's of mixed quality :) + +### Garden Plugins + +In this example we use the `ephmeral-kubernetes` plugin to deploy the project to a zero-config, ephemeral, Garden managed cluster that's spun up on-demand. +It's the quickest way to get started with Garden and this is the "quickstart" example after all. + +If you'd rather deploy it to your own cluster, you can update the values in +the [`project.garden.yml`](https://github.com/garden-io/quickstart-example/blob/main/project.garden.yml) file. +To learn more about our different K8s plugins, check out [our documentation](https://docs.garden.io/kubernetes-plugins/about). + +### Garden Actions + +The project has the following micro services: + +- `vote`—a frontend Vue application +- `api`—a Python server that receives votes from the `vote` frontend and pushes them to a message queue +- `redis`—a Redis deployment that's used as a message queue +- `worker`—a Java worker service that reads votes from the queue and pushes them to a database +- `db`—a Postgres database for storing the votes +- `result`—a Node.js websocket server that reads messages from the database and sends back to the `vote` client + +These services are built, deployed, and tested with [Garden actions](https://docs.garden.io/overview/core-concepts#action). + +Specifically, the `vote`, `api`, and `result` services all have their own Kubernetes manifests so we use the `container` Build action to build them +and the `kubernetes` Deploy action to deploy them. + +The `redis` and `db` services are "off the shelf" Helm charts that are deployed via the `helm` Deploy action. + +Finally the `worker` service is built and deployed via the `container` Build and Deploy actions respectively. Garden will generate the Kubernetes +manifests for you when using the `container` Deploy action which is useful for getting started quickly if you don't have those already— +but in general we recommend using your existing charts or manifests with Garden. + +You can learn more about the Kubernetes action types [in our docs](https://docs.garden.io/kubernetes-plugins/action-types). diff --git a/api/app.py b/api/app.py index b17fcbb..67987e2 100644 --- a/api/app.py +++ b/api/app.py @@ -22,7 +22,7 @@ def get_redis(): if not hasattr(g, 'redis'): - g.redis = Redis(host="redis", db=0, socket_timeout=5) + g.redis = Redis(host="redis-master", db=0, socket_timeout=5) return g.redis @app.route("/health", methods=['GET']) @@ -46,7 +46,7 @@ def vote(): redis = get_redis() vote = request.form['vote'] data = json.dumps({'voter_id': voter_id, 'vote': vote}) - print("received vote request for '%s' from voter id: '%s'" % (vote, voter_id)) + print("Received vote request for '%s', pushing to Redis queue with ID '%s'" % (vote, voter_id)) sys.stdout.flush() redis.rpush('votes', data) @@ -56,7 +56,7 @@ def vote(): mimetype='application/json' ) else: - print("received invalid request") + print("Received invalid request") sys.stdout.flush() return app.response_class( response=json.dumps({}), diff --git a/api/garden.yml b/api/garden.yml index 372dfca..0689083 100644 --- a/api/garden.yml +++ b/api/garden.yml @@ -1,44 +1,46 @@ ---- kind: Build +name: api type: container -name: api-build -description: The backend build for the voting UI +description: Build the vote API --- kind: Deploy -type: container name: api -build: api-build -description: The backend deploy for the voting UI +type: kubernetes +description: Deploy the vote API +dependencies: [build.api, deploy.redis] + +variables: + hostname: api.${var.baseHostname || providers.ephemeral-kubernetes.outputs.default-hostname} + spec: - args: [python, app.py] + # Variables such as container image are set via Garden template strings + # in the manifests themselves. Variables can also be set in the Garden config to ensure the manifests + # remain valid K8s manifests. Learn more at: https://docs.garden.io/kubernetes-plugins/action-types/kubernetes + files: [./manifests/*] + + # This tells Garden what "target" to use for logs, code syncing and more + defaultTarget: + kind: Deployment + name: api + sync: - args: ["/bin/sh", "-c", "ls /app/app.py | entr -n -r python /app/app.py"] paths: - - target: /app + - sourcePath: . + containerPath: /app mode: "one-way-replica" exclude: [.venv] - ports: - - name: http - protocol: TCP - containerPort: 8080 - servicePort: 80 - ingresses: - - path: /api - port: http - hostname: "api.${var.base-hostname || providers.ephemeral-kubernetes.outputs.default-hostname}" - healthCheck: - httpGet: - path: /health - port: http -dependencies: - - deploy.redis + overrides: + # Use entr to restart server on file changes in sync mode + - args: ["/bin/sh", "-c", "ls /app/app.py | entr -n -r python /app/app.py"] --- kind: Test -type: container name: unit -description: Unit test for backend API -build: api-build +type: container +description: Unit test the vote API +dependencies: [build.api] + spec: + image: ${actions.build.api.outputs.deploymentImageId} args: ["echo", "ok"] diff --git a/api/manifests/deployment.yaml b/api/manifests/deployment.yaml new file mode 100644 index 0000000..2a910de --- /dev/null +++ b/api/manifests/deployment.yaml @@ -0,0 +1,57 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: api +spec: + replicas: 1 + selector: + matchLabels: + app: api + template: + metadata: + labels: + app: api + spec: + containers: + - args: + - python + - app.py + env: + image: ${actions.build.api.outputs.deploymentImageId} + imagePullPolicy: IfNotPresent + livenessProbe: + failureThreshold: 3 + httpGet: + path: /health + port: 8080 + scheme: HTTP + initialDelaySeconds: 90 + periodSeconds: 5 + successThreshold: 1 + timeoutSeconds: 3 + name: api + ports: + - containerPort: 8080 + name: http + protocol: TCP + readinessProbe: + failureThreshold: 90 + httpGet: + path: /health + port: 8080 + scheme: HTTP + initialDelaySeconds: 2 + periodSeconds: 1 + successThreshold: 2 + timeoutSeconds: 3 + resources: + limits: + memory: 300Mi + requests: + cpu: 10m + memory: 90Mi + securityContext: + allowPrivilegeEscalation: false + imagePullSecrets: + - name: ${var.imagePullSecretName || ''} + restartPolicy: Always diff --git a/api/manifests/ingress.yaml b/api/manifests/ingress.yaml new file mode 100644 index 0000000..b146dc8 --- /dev/null +++ b/api/manifests/ingress.yaml @@ -0,0 +1,17 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: api +spec: + ingressClassName: nginx + rules: + - host: ${var.hostname} + http: + paths: + - backend: + service: + name: api + port: + number: 80 + path: / + pathType: Prefix diff --git a/api/manifests/service.yaml b/api/manifests/service.yaml new file mode 100644 index 0000000..bcc9a6d --- /dev/null +++ b/api/manifests/service.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: api +spec: + ports: + - name: http + port: 80 + protocol: TCP + targetPort: 8080 + selector: + app: api diff --git a/postgres/garden.yml b/postgres/garden.yml index ed1d853..f0913ad 100644 --- a/postgres/garden.yml +++ b/postgres/garden.yml @@ -1,50 +1,61 @@ kind: Deploy -description: Postgres container for storing voting results -type: container -name: postgres +name: db +type: helm +description: Deploy a Postgres database for storing voting results + spec: - image: postgres:11.7-alpine - volumes: - - name: data - containerPath: /db-data - ports: - - name: postgres - containerPort: 5432 - env: - POSTGRES_DATABASE: ${var.postgres-database} - POSTGRES_USERNAME: ${var.postgres-username} - POSTGRES_PASSWORD: ${var.postgres-password} - healthCheck: - command: [psql, -w, -U, "${var.postgres-username}", -d, "${var.postgres-database}", -c, "SELECT 1"] + chart: + name: postgresql + repo: https://charts.bitnami.com/bitnami + version: 12.6.6 + values: + # This is a more digestable name than the default one in the template + fullnameOverride: postgres + auth: + username: ${var.postgresUsername} + database: ${var.postgresDatabase} + postgresPassword: ${var.postgresPassword} + # Avoid some late startup flakiness + primary: + readinessProbe: + successThreshold: 3 # Raised from a default of 1 + persistence: + enabled: false + --- kind: Run name: db-init -type: container -dependencies: [deploy.postgres] +type: kubernetes-exec + +dependencies: + - deploy.db + spec: - image: postgres:11.7-alpine - command: [/bin/sh, -c] - # The postgres health check appears to go through before the server accepts remote connections, so we need to - # sleep for a while. - # https://github.com/CrunchyData/crunchy-containers/issues/653 - args: + resource: + kind: "StatefulSet" + name: "postgres" + command: [ - "sleep 15 && psql -w -U ${var.postgres-username} --host=postgres --port=5432 -d ${var.postgres-database} -c 'CREATE TABLE IF NOT EXISTS votes (id VARCHAR(255) NOT NULL UNIQUE, vote VARCHAR(255) NOT NULL, created_at timestamp default NULL)'", + "bin/sh", + "-c", + "sleep 15 && PGPASSWORD=${var.postgresPassword} psql -w -U ${var.postgresUsername} --host=postgres --port=5432 -d ${var.postgresDatabase} -c 'CREATE TABLE IF NOT EXISTS votes (id VARCHAR(255) NOT NULL UNIQUE, vote VARCHAR(255) NOT NULL, created_at timestamp default NULL)'", ] - env: - PGDATABASE: ${var.postgres-database} - PGUSER: ${var.postgres-username} - PGPASSWORD: ${var.postgres-password} + --- kind: Run name: db-clear -type: container -dependencies: [deploy.postgres] +type: kubernetes-exec + +dependencies: + - deploy.db + spec: - image: postgres:11.7-alpine - command: [/bin/sh, -c] - args: ["psql -w -U ${var.postgres-username} --host=postgres --port=5432 -d ${var.postgres-database} -c 'TRUNCATE votes'"] - env: - PGDATABASE: ${var.postgres-database} - PGUSER: ${var.postgres-username} - PGPASSWORD: ${var.postgres-password} + resource: + kind: "StatefulSet" + name: "postgres" + command: + [ + "bin/sh", + "-c", + "PGPASSWORD=${var.postgresPassword} psql -w -U ${var.postgresUsername} --host postgres --port=5432 -d ${var.postgresDatabase} -c 'TRUNCATE votes'", + ] diff --git a/project.garden.yml b/project.garden.yml index afd99d1..0add686 100644 --- a/project.garden.yml +++ b/project.garden.yml @@ -5,46 +5,46 @@ defaultEnvironment: ephemeral dotIgnoreFile: .gitignore variables: - postgres-username: postgres - postgres-database: postgres - postgres-password: postgres + postgresUsername: postgres + postgresDatabase: postgres + postgresPassword: postgres # Replace underscores as Kubernetes namespaces do not allow them. - user-namespace: vote-demo-quickstart-${kebabCase(local.username)} + userNamespace: vote-demo-quickstart-${kebabCase(local.username)} environments: - name: local - defaultNamespace: ${var.user-namespace} + defaultNamespace: ${var.userNamespace} variables: - base-hostname: local.demo.garden + baseHostname: local.demo.garden - name: remote - defaultNamespace: ${var.user-namespace} + defaultNamespace: ${var.userNamespace} variables: - base-hostname: "" + baseHostname: "" + imagePullSecretName: "" - name: ephemeral providers: - name: local-kubernetes environments: [local] namespace: ${environment.namespace} - defaultHostname: ${var.base-hostname} + defaultHostname: ${var.baseHostname} - name: ephemeral-kubernetes environments: [ephemeral] - # You can use Garden with remote Kubernetes clusters as well. In fact, that's where it shines! - # Please see our docs on using the (remote) Kubernetes plugin to learn how to configure - # the values below. + # You can use Garden with your own remote Kubernetes clusters as well. + # Please see our docs to learn how to configure the values below: https://docs.garden.io/kubernetes-plugins/remote-k8s - name: kubernetes environments: [remote] context: "" ingressClass: "nginx" buildMode: cluster-buildkit imagePullSecrets: - - name: "" + - name: ${var.imagePullSecretName} namespace: default deploymentRegistry: hostname: "" namespace: "" namespace: ${environment.namespace} - defaultHostname: ${var.base-hostname} + defaultHostname: ${var.baseHostname} diff --git a/redis/garden.yml b/redis/garden.yml index a4756b7..656a203 100644 --- a/redis/garden.yml +++ b/redis/garden.yml @@ -1,10 +1,17 @@ kind: Deploy -type: container name: redis -description: Redis service for queueing votes before they are aggregated +type: helm +description: Deploy a Redis service for queueing votes before they are aggregated + spec: - image: redis:alpine - ports: - - name: redis - protocol: TCP - containerPort: 6379 + chart: + name: redis + repo: https://charts.bitnami.com/bitnami + version: "17.13.2" + values: + auth: + enabled: false + master: + persistence: + enabled: false + architecture: standalone diff --git a/result/garden.yml b/result/garden.yml index 64ae1a8..39f5d42 100644 --- a/result/garden.yml +++ b/result/garden.yml @@ -1,38 +1,41 @@ kind: Build -type: container name: result +type: container +description: Build the result websocket server --- kind: Deploy -description: Deploy results service -type: container name: result -build: result -dependencies: - - run.db-init +type: kubernetes +description: Deploy the result websocket server +dependencies: [build.result, run.db-init] + spec: - replicas: 1 - args: [nodemon, server.js] + # Variables such as container image are set via Garden template strings + # in the manifests themselves. Variables can also be set in the Garden config to ensure the manifests + # remain valid K8s manifests. Learn more at: https://docs.garden.io/kubernetes-plugins/action-types/kubernetes + files: [./manifests/*] + + # This tells Garden what "target" to use for logs, code syncing and more + defaultTarget: + kind: Deployment + name: result + sync: paths: - - target: /app + - sourcePath: . + containerPath: /app exclude: [node_modules] - ports: - - name: ui - protocol: TCP - containerPort: 8080 - servicePort: 80 - env: - PGDATABASE: ${var.postgres-database} - PGUSER: ${var.postgres-username} - PGPASSWORD: ${var.postgres-password} + overrides: + - args: [nodemon, server.js] --- kind: Test -name: e2e -description: Test results service +name: result-integ type: container -build: result -dependencies: [run.db-init] +description: Test results handler +dependencies: [build.result, run.db-init] + spec: + image: ${actions.build.result.outputs.deploymentImageId} args: [echo, ok] diff --git a/result/manifests/deployment.yaml b/result/manifests/deployment.yaml new file mode 100644 index 0000000..f9f31a6 --- /dev/null +++ b/result/manifests/deployment.yaml @@ -0,0 +1,59 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: result +spec: + replicas: 1 + selector: + matchLabels: + app: result + template: + metadata: + labels: + app: result + spec: + containers: + - name: result + image: ${actions.build.result.outputs.deploymentImageId} + imagePullPolicy: IfNotPresent + env: + - name: PGDATABASE + value: ${var.postgresDatabase} + - name: PGUSER + value: ${var.postgresUsername} + - name: PGPASSWORD + value: ${var.postgresPassword} + livenessProbe: + failureThreshold: 3 + httpGet: + path: /health + port: 8080 + scheme: HTTP + initialDelaySeconds: 90 + periodSeconds: 5 + successThreshold: 1 + timeoutSeconds: 3 + ports: + - containerPort: 8080 + name: http + protocol: TCP + readinessProbe: + failureThreshold: 90 + httpGet: + path: /health + port: 8080 + scheme: HTTP + initialDelaySeconds: 2 + periodSeconds: 1 + successThreshold: 2 + timeoutSeconds: 3 + resources: + limits: + memory: 300Mi + requests: + cpu: 10m + memory: 90Mi + securityContext: + allowPrivilegeEscalation: false + imagePullSecrets: + - name: ${var.imagePullSecretName || ''} diff --git a/result/manifests/service.yaml b/result/manifests/service.yaml new file mode 100644 index 0000000..6ba6ea9 --- /dev/null +++ b/result/manifests/service.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: result +spec: + ports: + - name: http + port: 80 + protocol: TCP + targetPort: 8080 + selector: + app: result diff --git a/result/server.js b/result/server.js index 613348b..288e79e 100644 --- a/result/server.js +++ b/result/server.js @@ -1,15 +1,13 @@ -var express = require('express'), - async = require('async'), - path = require("path") - pg = require("pg"), - cookieParser = require('cookie-parser'), - bodyParser = require('body-parser'), - methodOverride = require('method-override'), - app = express(), - server = require('http').Server(app), - io = require('socket.io')(server); - -var port = process.env.PORT || 4000; +const express = require('express') +const async = require('async') +const pg = require("pg") +const cookieParser = require('cookie-parser') +const methodOverride = require('method-override') +const app = express() +const server = require('http').Server(app) +const io = require('socket.io')(server) + +const port = process.env.PORT || 8080 const pgPool = new pg.Pool({ database: process.env.PGDATABASE, @@ -58,7 +56,7 @@ function getVotes(client) { } else { const votes = JSON.stringify(collectVotesFromResult(result)); if (votes !== cachedVotes) { - console.log("Got updated votes", votes); + console.log(`Got updated votes from DB: ${votes}, sending to client`); cachedVotes = votes } io.sockets.emit("scores", votes); @@ -69,7 +67,7 @@ function getVotes(client) { } function collectVotesFromResult(result) { - var votes = { a: 0, b: 0 }; + const votes = { a: 0, b: 0 }; result.rows.forEach(function (row) { votes[row.vote] = parseInt(row.count); @@ -79,7 +77,6 @@ function collectVotesFromResult(result) { } app.use(cookieParser()); -app.use(bodyParser()); app.use(methodOverride('X-HTTP-Method-Override')); app.use(function (req, res, next) { res.header("Access-Control-Allow-Origin", "*"); @@ -88,17 +85,10 @@ app.use(function (req, res, next) { next(); }); -app.use(express.static('views')); -// app.use(express.static(__dirname + '/result/views')); - -// app.use('/result', express.static(__dirname + '/views')); - -// app.get('/result', function (req, res) { -app.get('/', function (req, res) { - res.sendFile(path.resolve(__dirname + '/views/index.html')); +app.get('/health', function (_req, res) { + res.sendStatus(200); }); server.listen(port, function () { - var port = server.address().port; console.log('App running on port ' + port); }); diff --git a/start.sh b/start.sh index 306932f..a65a152 100644 --- a/start.sh +++ b/start.sh @@ -35,15 +35,15 @@ sleep 5 sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config # Do not install NGINX ingress controller -sed -i 's/\(providers:\)/\1\n - name: local-kubernetes\n environments: [local]\n namespace: ${environment.namespace}\n defaultHostname: ${var.base-hostname}\n setupIngressController: null/' project.garden.yml +sed -i 's/\(providers:\)/\1\n - name: local-kubernetes\n environments: [local]\n namespace: ${environment.namespace}\n defaultHostname: ${var.baseHostname}\n setupIngressController: null/' project.garden.yml # Update the garden.yml file for the vote container sed -i 's/servicePort: 80/nodePort: 30000/' vote/garden.yml -sed -i 's/vote.${var.base-hostname}/http:\/\/localhost:30000/' vote/garden.yml +sed -i 's/vote.${var.baseHostname}/http:\/\/localhost:30000/' vote/garden.yml sed -i 's/hostname:/linkUrl:/' vote/garden.yml # Remove ingress blocks from result and api containers -sed -i '/ingresses:/, /hostname: result.\${var.base-hostname}/d' api/garden.yml result/garden.yml +sed -i '/ingresses:/, /hostname: result.\${var.baseHostname}/d' api/garden.yml result/garden.yml # Exit with a success code exit 0 diff --git a/vote/garden.yml b/vote/garden.yml index 95b4f71..91e3847 100644 --- a/vote/garden.yml +++ b/vote/garden.yml @@ -1,47 +1,50 @@ kind: Build name: vote type: container -include: [.] +description: Build the vote UI --- kind: Deploy -type: container name: vote -build: vote -description: The voting UI +type: kubernetes +description: Deploy the vote UI +dependencies: + - build.vote + - deploy.api + - deploy.result + - deploy.worker + +variables: + hostname: "vote.${var.baseHostname || providers.ephemeral-kubernetes.outputs.default-hostname}" + spec: + # Variables such as container image are set via Garden template strings + # in the manifests themselves. Variables can also be set in the Garden config to ensure the manifests + # remain valid K8s manifests. Learn more at: https://docs.garden.io/kubernetes-plugins/action-types/kubernetes + files: [./manifests/*] + + # This tells Garden what "target" to use for logs, code syncing and more + defaultTarget: + kind: Deployment + name: vote + sync: paths: - - target: /app/src - source: src + - sourcePath: src + containerPath: /app/src mode: one-way-replica exclude: [node_modules] - ports: - - name: http - containerPort: 8080 - servicePort: 80 - healthCheck: - httpGet: - path: / - port: http - ingresses: - - path: / - port: http - hostname: "vote.${var.base-hostname || providers.ephemeral-kubernetes.outputs.default-hostname}" - env: - HOSTNAME: "vote.${var.base-hostname || providers.ephemeral-kubernetes.outputs.default-hostname}" - VITE_USERNAME: ${local.username} -dependencies: - - deploy.api - - deploy.result - - deploy.worker + overrides: + - args: [npm, run, dev] --- kind: Test -type: container name: unit-vote -build: vote +type: container +dependencies: [build.vote] + spec: + image: ${actions.build.vote.outputs.deploymentImageId} args: [npm, run, test:unit] # E2E Runner configs @@ -50,18 +53,12 @@ kind: Build name: e2e-runner type: container ---- -kind: Deploy -type: container -name: e2e-runner -dependencies: [deploy.vote] -build: vote - --- kind: Test -type: container name: e2e-vote -build: e2e-runner -dependencies: [deploy.vote] +type: container +dependencies: [build.e2e-runner, deploy.vote] + spec: + image: ${actions.build.e2e-runner.outputs.deploymentImageId} args: [npm, run, test:e2e] diff --git a/vote/manifests/deployment.yaml b/vote/manifests/deployment.yaml new file mode 100644 index 0000000..060d571 --- /dev/null +++ b/vote/manifests/deployment.yaml @@ -0,0 +1,57 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: vote +spec: + replicas: 1 + selector: + matchLabels: + app: vote + template: + metadata: + labels: + app: vote + spec: + containers: + - name: vote + image: ${actions.build.vote.outputs.deploymentImageId} + imagePullPolicy: IfNotPresent + env: + - name: HOSTNAME + value: ${var.hostname} + - name: VITE_USERNAME + value: ${local.username} + livenessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 8080 + scheme: HTTP + initialDelaySeconds: 90 + periodSeconds: 5 + successThreshold: 1 + timeoutSeconds: 3 + ports: + - containerPort: 8080 + name: http + protocol: TCP + readinessProbe: + failureThreshold: 90 + httpGet: + path: / + port: 8080 + scheme: HTTP + initialDelaySeconds: 2 + periodSeconds: 1 + successThreshold: 2 + timeoutSeconds: 3 + resources: + limits: + memory: 300Mi + requests: + cpu: 10m + memory: 90Mi + securityContext: + allowPrivilegeEscalation: false + imagePullSecrets: + - name: ${var.imagePullSecretName || ''} diff --git a/vote/manifests/ingress.yaml b/vote/manifests/ingress.yaml new file mode 100644 index 0000000..6a89536 --- /dev/null +++ b/vote/manifests/ingress.yaml @@ -0,0 +1,17 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: vote +spec: + ingressClassName: nginx + rules: + - host: ${var.hostname} + http: + paths: + - backend: + service: + name: vote + port: + number: 80 + path: / + pathType: Prefix diff --git a/vote/manifests/service.yaml b/vote/manifests/service.yaml new file mode 100644 index 0000000..f58d6f0 --- /dev/null +++ b/vote/manifests/service.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: vote +spec: + ports: + - name: http + port: 80 + protocol: TCP + targetPort: 8080 + selector: + app: vote diff --git a/vote/package.json b/vote/package.json index db36713..d2ce336 100644 --- a/vote/package.json +++ b/vote/package.json @@ -3,7 +3,7 @@ "version": "0.0.1", "private": true, "scripts": { - "dev": "vite", + "dev": "npm run serve", "build": "vite build", "serve": "vite dev --host $GARDEN_VARIABLES_BASE_HOSTNAME --port 8080", "test:unit": "vitest --run tests/unit", diff --git a/vote/src/components/Code.vue b/vote/src/components/Code.vue index db1d002..0b91436 100644 --- a/vote/src/components/Code.vue +++ b/vote/src/components/Code.vue @@ -1,6 +1,33 @@ @@ -10,59 +37,74 @@ export default { name: 'CodeBlock', data: () => ({ + showCopySuccess: false }), props: { text: String, type: String, + showCopyBtn: Boolean }, methods: { copyText() { // Can't use navigator.clipboard if served over http - const tmpEl = document.createElement('textarea'); - tmpEl.value = this.text; - tmpEl.style.top = '0'; - tmpEl.style.left = '0'; - tmpEl.style.position = 'fixed'; + const tmpEl = document.createElement('textarea') + tmpEl.value = this.text + tmpEl.style.top = '0' + tmpEl.style.left = '0' + tmpEl.style.position = 'fixed' - document.body.appendChild(tmpEl); - tmpEl.select(); - document.execCommand('copy'); - document.body.removeChild(tmpEl); - }, - }, + document.body.appendChild(tmpEl) + tmpEl.select() + document.execCommand('copy') + document.body.removeChild(tmpEl) -}; + this.showCopySuccess = true + setTimeout(() => { + this.showCopySuccess = false + }, 2000) + } + } +} diff --git a/vote/src/components/Guide.vue b/vote/src/components/Guide.vue index d275328..e69b244 100644 --- a/vote/src/components/Guide.vue +++ b/vote/src/components/Guide.vue @@ -29,7 +29,7 @@ export default { name: 'GuideBlock', data: () => { - const pageNames = ['welcome', 'hotReloading1', 'hotReloading2', 'logs', 'tests', 'tasks', 'exec', 'end']; + const pageNames = ['welcome', 'hotReloading1', 'hotReloading2', 'logs', 'tests', 'run-actions', 'exec', 'end']; const currentPageIdx = parseInt(window.localStorage.getItem(LOCAL_STORAGE_ITEM_PAGE), 10) || 0; return { hidden: false, diff --git a/vote/src/components/GuidePages.vue b/vote/src/components/GuidePages.vue index 5eaf51f..bb9909c 100644 --- a/vote/src/components/GuidePages.vue +++ b/vote/src/components/GuidePages.vue @@ -11,11 +11,12 @@ It contains a handful of services, a message queue, and a database.

- When you ran followed by the - command, Garden built the entire + When you ran the command, + Garden built the entire project, deployed the services, - ran a task to intialize the database, and turned on - hot reloading (i.e. live code sync). + ran an action to intialize the database, and turned on + hot reloading (i.e. live code sync). You can view the + results in the Garden web dashboard.

Try voting by clicking the buttons below. @@ -24,7 +25,7 @@

Hot reloading I

- When you run the command, hot reloading is enabled by default. + If you haven't already, you can enable hot reloading by running from the dev console.

This means that changes you make to the code locally are @@ -63,23 +64,26 @@

Logs

- To stream logs from all the micro services in this project, simply - run the following from the interactive Garden dev console: - - There are multiple different options for the logs command, you can run - to learn more. Stop following logs by turning off the log - monitors with . + To stream logs from all the micro services in this project you can run the command from the dev console. +
+
+ There are multiple different options for the logs command and you can run + to learn more. To stop following logs in the dev console you can run . +
+
+ You can also use the Garden CLI directly and run , e.g. if you want to stream logs in a separate window.

If you click the vote buttons belows you should see the corresponding service logs.

-

Tests

+

Test Actions

Garden treats tests as a first-class citizen. To run the entire - test suite for this project, simply run: - + test suite for this project you can run from the dev console. +
+
Garden also has a powerful caching mechanism built-in. Try running the test again without making any changes to the code.
@@ -88,22 +92,22 @@

(Note that running the tests may re-deploy services and thereby disable hot reloading. You - can re-enable it by running again.) + can re-enable it by running again.)

-
-

Tasks

+
+

Run Actions

- Tasks are another Garden primitive that are useful for various set-up operations. + Run actions are useful for various set-up operations.

- For example, when you first ran , a task for initialising and seeding + For example, when you first ran , a Run action for initialising and seeding the database was executed.

- You can also run individual tasks directly. To reset the database to its original state, + You can also run individual Run actions directly. To reset the database to its original state, run: - + Notice how the votes are reset to zero?

@@ -114,7 +118,7 @@

To shell into the API server, using a separate terminal (not the dev console), you can run: - + You can exit from the process by typing and hitting enter.

@@ -131,12 +135,12 @@ deploy to production.

- For next steps, we suggest heading to our docs to learn how you can start using Garden for - your own projects. And to join our Discord community at... + For next steps, we suggest heading to our docs to learn how you can start using Garden for + your own projects. If you have any questions, we happily encourage you to join our Discord commmunity.

And if you really want to amplify your Garden experience and get expert help in getting - started, check out our Garden Cloud offering for teams and enterprises. + started, check out our Garden Enterprise offering.

diff --git a/vote/vite.config.js b/vote/vite.config.js index 432269a..8449ad9 100644 --- a/vote/vite.config.js +++ b/vote/vite.config.js @@ -5,9 +5,7 @@ import vue from '@vitejs/plugin-vue' // https://vitejs.dev/config/ export default defineConfig({ - plugins: [ - vue(), - ], + plugins: [vue()], resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) @@ -20,6 +18,17 @@ export default defineConfig({ changeOrigin: true, secure: false, logLevel: 'debug', + configure: (proxy, _options) => { + proxy.on('error', (err, _req, _res) => { + console.log('API proxy error', err) + }) + proxy.on('proxyReq', (proxyReq, req, _res) => { + console.log(`Sending ${proxyReq.method} request to API`) + }) + proxy.on('proxyRes', (proxyRes, _req, _res) => { + console.log(`Received API response with status: ${proxyRes.statusCode}`) + }) + } }, '^/socket.io': { target: 'http://result', @@ -27,7 +36,7 @@ export default defineConfig({ secure: false, ws: true, logLevel: 'debug', - }, - }, + } + } } }) diff --git a/worker/garden.yml b/worker/garden.yml index 14259b9..e8399ba 100644 --- a/worker/garden.yml +++ b/worker/garden.yml @@ -1,19 +1,23 @@ ---- kind: Build name: worker type: container +description: Build the worker image --- kind: Deploy -description: The worker that collects votes and stores results in a postgres table +description: Deploy the worker that collects votes and stores results in a postgres table +# Here we're using the container type which means Garden will generate the manifests. +# We generally recommend using the 'kubernetes' or 'helm' types if you already have manifests +# or charts at hand but the 'container' type can be a good way to get started quickly if you don't. +# You can learn more about the Kubernetes action types here: https://docs.garden.io/kubernetes-plugins/action-types type: container name: worker build: worker spec: env: - PGDATABASE: ${var.postgres-database} - PGUSER: ${var.postgres-username} - PGPASSWORD: ${var.postgres-password} + PGDATABASE: ${var.postgresDatabase} + PGUSER: ${var.postgresUsername} + PGPASSWORD: ${var.postgresPassword} dependencies: - deploy.redis - run.db-init diff --git a/worker/pom.xml b/worker/pom.xml index 1fea098..0c65fc6 100644 --- a/worker/pom.xml +++ b/worker/pom.xml @@ -26,7 +26,7 @@ org.postgresql postgresql - 9.4-1200-jdbc41 + 42.2.14 diff --git a/worker/src/main/java/worker/Worker.java b/worker/src/main/java/worker/Worker.java index 1e55bfa..4832dba 100644 --- a/worker/src/main/java/worker/Worker.java +++ b/worker/src/main/java/worker/Worker.java @@ -9,7 +9,7 @@ class Worker { public static void main(String[] args) { try { - Jedis redis = connectToRedis("redis"); + Jedis redis = connectToRedis("redis-master"); Connection dbConn = connectToDB("postgres"); System.err.println("Watching vote queue"); @@ -31,7 +31,7 @@ static void getVoteFromQueue(Connection dbConn, Jedis redis) throws SQLException String voterID = voteData.getString("voter_id"); String vote = voteData.getString("vote"); - System.err.printf("Processing vote for '%s' by '%s'\n", vote, voterID); + System.err.printf("Processing vote with ID '%s' and pushing to DB\n", voterID); updateVote(dbConn, voterID, vote); } catch (Exception e) { System.err.printf("Error when processing vote from queue");