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 +
+ +
+ + +## 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 || '
- {{text}}Copy
+ {{ text }}
+
+
+
+
+ ✔︎
@@ -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 run the
This means that changes you make to the code locally are
@@ -63,23 +64,26 @@
- 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 and you can run
+
+
+ You can also use the Garden CLI directly and run
If you click the vote buttons belows you should see the corresponding service logs.
Garden treats tests as a first-class citizen. To run the entire
- test suite for this project, simply run:
-
+
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
- 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
- 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:
-