Skip to content

Commit b279115

Browse files
Add README. Finish controller.
1 parent cfc2663 commit b279115

File tree

15 files changed

+679
-0
lines changed

15 files changed

+679
-0
lines changed

README.md

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
# Kubernetes Controller Example
2+
3+
This is an example application that uses Kubernetes Custom Resource Definitions to keep a list of TODO actions, but stored in Kubernetes.
4+
5+
The application is a custom resource definition, namespaced.
6+
7+
The code in this repository **is not production ready**. It's merely an example repository for training purposes.
8+
9+
![Example controller UI](example-controller-ui.png)
10+
11+
## Prerequisites
12+
13+
The following apps are required:
14+
15+
* Kubernetes `kubectl`
16+
* Docker
17+
* Kind
18+
19+
The stack uses [Kind](https://kind.sigs.k8s.io/) to launch a cluster locally, then deploy the controller there. Since this controller relies on a Docker container, see below for instructions on how to send the Docker image once built to the Kubernetes Kind cluster.
20+
21+
To launch a cluster with a single node mappings port 80 and 443 to 80 and 443 on given node, use [`kind/cluster.yaml`](kind/cluster.yaml):
22+
23+
```bash
24+
kind create cluster --config kind/cluster.yaml
25+
```
26+
27+
If using a different cluster engine other than Kind, ensure you perform the appropriate modifications to the [`controller/controller.yaml`](controller/controller.yaml) file so it uses the proper Docker image. See below for more details.
28+
29+
## Application structure
30+
31+
```text
32+
.
33+
├── controller
34+
│   ├── controller.yaml
35+
│   ├── Dockerfile
36+
│   ├── go.mod
37+
│   ├── homepage.tmpl
38+
│   ├── kubernetes.go
39+
│   ├── main.go
40+
│   ├── req_logger.go
41+
│   ├── res_utils.go
42+
│   └── utils.go
43+
├── kind
44+
│   └── cluster.yaml
45+
├── kubernetes
46+
│   ├── crd-use.yaml
47+
│   └── crd.yaml
48+
└── README.md
49+
```
50+
51+
## Kubernetes & CRD
52+
53+
The [`kubernetes`](kubernetes) folder contains two files:
54+
55+
* The CRD itself, named [`crd.yaml`](kubernetes/crd.yaml) which is used to create a resource named `Todo` in Kubernetes.
56+
* The usage of given CRD, called [`crd-use.yaml`](kubernetes/crd-use.yaml). This is used to create an initial TODO object.
57+
58+
## Go Application
59+
60+
The [`controller`](controller) folder includes a Go application that reads from the Kubernetes API server any resource typed `Todo`.
61+
62+
The application **does not use the `kubernetes/client-go` to interact with the Kubernetes API**. Since the idea of this repository is to provide a baseline for training, it uses instead pure HTTP calls. It does ensure, however:
63+
64+
* That the HTTP client is configured to talk to the Kubernetes API using the CA certificate from the `ServiceAccount`.
65+
* That the token and current namespace are captured from the `ServiceAccount` details automatically mounted in the container.
66+
67+
It's possible to use a sidecar container to avoid having all this logic here: the sidecar container can run `kubectl proxy` and this app can interact then with the sidecar's host and port to perform preauthenticated requests against the API.
68+
69+
The Go application uses a very simple Vue UI which loads both the namespace the controller is running at, as well as any `Todo` in it, and returns it back to the UI using Javascript's `fetch()`.
70+
71+
### Compiling the application
72+
73+
The application uses a Docker multi-stage build to compile the application and avoid having to build it locally. You can perform simple tests by running this app in your own machine rather than in the cluster, if at the same time you use `kubectl proxy` to allow for unauthenticated local access to the Kubernetes API. Then, simply configure the environment variables so the controller knows where to connect to the Kubernetes API. In this case, those environment variables are in [`controller/.env`](controller/.env):
74+
75+
```bash
76+
KUBERNETES_API_HOST=http://localhost:8080
77+
KUBERNETES_CONTROLLER_PORT=8081
78+
```
79+
80+
Run the application with these environment variables set, and in another terminal, run:
81+
82+
```bash
83+
kubectl proxy --port 8080
84+
```
85+
86+
That way, the controller connects to your `kubectl proxy` on port `8080`, but the app itself with the UI is available in `localhost:8081`.
87+
88+
### Running the application
89+
90+
In order for the application to run in a cluster, you need:
91+
92+
* To have the Docker container either stored in a Registry or loaded into the Cluster
93+
* To allow the controller to have access to the Kubernetes API, and specifically, to the `Todo` resource in the current namespace
94+
95+
To achieve the first point while using Kind, you can build the Docker image locally, then load it into the cluster using the `kind` CLI:
96+
97+
```bash
98+
$ docker build -q -t demo-controller -f controller/Dockerfile controller/
99+
sha256:4a711a67ac8cb79f555bf736f72bf7eff3febf918895499b7f2c2093f3ca1cbe
100+
101+
$ kind load docker-image demo-controller:latest --name testing-cluster
102+
Image: "demo-controller:latest" with ID "sha256:4a711a67ac8cb79f555bf736f72bf7eff3febf918895499b7f2c2093f3ca1cbe" not yet present on node "testing-cluster-control-plane", loading...
103+
```
104+
105+
You can use the image now. If you're using Kind, make sure the `imagePullPolicy` is set to `IfNotPresent` to ensure it uses the copy of the image we just loaded rather than grabbing one from Internet.
106+
107+
In order to have a full controller, a deployment has been provided using the [`controller/controller.yaml`](controller/controller.yaml) file. This file will:
108+
109+
* Create a service account named `example-controller-service-account`
110+
* Create a Role which has access to list `todos`
111+
* Bind the Service account to the Role using a Role Binding
112+
* Finally, deploy the controller using a Deployment, which maps port `8080` as `web`
113+
114+
You can install this manifest using:
115+
116+
```bash
117+
$ kubectl apply -f controller/controller.yaml
118+
serviceaccount/example-controller-service-account created
119+
role.rbac.authorization.k8s.io/example-controller-role created
120+
rolebinding.rbac.authorization.k8s.io/example-controller-rolebinding created
121+
deployment.apps/example-controller created
122+
```
123+
124+
You can access the application running here by using Kubernetes port-forward. The example below will use `awk` to find the appropriate pod then expose it on port `1234`:
125+
126+
```bash
127+
$ kubectl get pods --show-labels | awk '/example\-controller/ { print $1 }' | xargs -I{} kubectl port-forward --address 0.0.0.0 pod/{} 1234:8080
128+
Forwarding from 0.0.0.0:1234 -> 8080
129+
```
130+
131+
Alternatively, you can build a `Service` out of it and expose it, or top it off with an `Ingress`. The sky is the limit.

controller/.env

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#!/usr/bin/env bash
2+
# shellcheck disable=SC2034
3+
4+
KUBERNETES_API_HOST=http://localhost:8080
5+
KUBERNETES_CONTROLLER_PORT=8081

controller/Dockerfile

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
FROM alpine as certs
2+
RUN apk add --no-cache ca-certificates && update-ca-certificates
3+
4+
FROM golang:1.17 as builder
5+
WORKDIR /app
6+
ADD . .
7+
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w -extldflags='-static'" -o /bin/controller
8+
9+
FROM scratch
10+
COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
11+
COPY --from=builder /bin/controller /controller
12+
EXPOSE 8080
13+
ENTRYPOINT ["/controller"]
14+

controller/controller.yaml

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
apiVersion: v1
2+
kind: ServiceAccount
3+
metadata:
4+
name: example-controller-service-account
5+
6+
---
7+
8+
apiVersion: rbac.authorization.k8s.io/v1
9+
kind: Role
10+
metadata:
11+
name: example-controller-role
12+
rules:
13+
- apiGroups:
14+
- "patrickdap.com"
15+
resources:
16+
- todos
17+
verbs:
18+
- list
19+
20+
---
21+
22+
apiVersion: rbac.authorization.k8s.io/v1
23+
kind: RoleBinding
24+
metadata:
25+
name: example-controller-rolebinding
26+
subjects:
27+
- kind: ServiceAccount
28+
name: example-controller-service-account
29+
roleRef:
30+
apiGroup: rbac.authorization.k8s.io
31+
kind: Role
32+
name: example-controller-role
33+
34+
---
35+
apiVersion: apps/v1
36+
kind: Deployment
37+
metadata:
38+
name: example-controller
39+
spec:
40+
replicas: 1
41+
selector:
42+
matchLabels:
43+
app: example-controller
44+
template:
45+
metadata:
46+
labels:
47+
app: example-controller
48+
spec:
49+
serviceAccountName: example-controller-service-account
50+
containers:
51+
- name: example-controller
52+
image: demo-controller:latest
53+
imagePullPolicy: IfNotPresent
54+
ports:
55+
- name: web
56+
containerPort: 8080

controller/go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module github.com/patrickdappollonio/crd-controller/controller
2+
3+
go 1.16

controller/homepage.tmpl

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
<!DOCTPE html>
2+
<html>
3+
<head>
4+
<title>My TODO list</title>
5+
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
6+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.3/css/bulma.min.css">
7+
<style type="text/css">
8+
nav {
9+
margin-bottom: 20px
10+
}
11+
12+
.lds-hourglass {
13+
display: inline-block;
14+
position: relative;
15+
width: 80px;
16+
height: 80px;
17+
}
18+
.lds-hourglass:after {
19+
content: " ";
20+
display: block;
21+
border-radius: 50%;
22+
width: 0;
23+
height: 0;
24+
margin: 8px;
25+
box-sizing: border-box;
26+
border: 32px solid #3e8ed0;
27+
border-color: #3e8ed0 transparent #3e8ed0 transparent;
28+
animation: lds-hourglass 1.2s infinite;
29+
}
30+
@keyframes lds-hourglass {
31+
0% {
32+
transform: rotate(0);
33+
animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);
34+
}
35+
50% {
36+
transform: rotate(900deg);
37+
animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
38+
}
39+
100% {
40+
transform: rotate(1800deg);
41+
}
42+
}
43+
</style>
44+
</head>
45+
<body>
46+
<nav class="navbar is-info" role="navigation" aria-label="main navigation">
47+
<div class="container">
48+
<div class="navbar-brand">
49+
<a class="navbar-item" href="/">
50+
<svg width="140" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" x="0" y="0" viewBox="0 0 503.1 89.5" style="enable-background:new 0 0 503.1 89.5" xml:space="preserve">
51+
<style>.st2{fill:#fff}</style>
52+
<g id="Layer_2">
53+
<g id="Layer_1-2">
54+
<path d="M82.3 21.3c-.5-1.6-1.7-2.9-3.2-3.7L48.6 3c-.8-.4-1.7-.5-2.5-.5s-1.7 0-2.5.2L13.1 17.4c-1.5.7-2.6 2-3 3.7L2.6 54c-.3 1.7.1 3.4 1.1 4.8l21.1 26.1c1.2 1.2 2.9 2 4.6 2.1H63c1.8.2 3.5-.6 4.6-2.1l21.1-26.1c1-1.4 1.4-3.1 1.2-4.8l-7.6-32.7z" style="fill:#fff;stroke:#fff;stroke-width:5;stroke-miterlimit:10"/>
55+
<path d="M82.3 21.3c-.5-1.6-1.7-2.9-3.2-3.7L48.6 3c-.8-.4-1.7-.5-2.5-.5s-1.7 0-2.5.2L13.1 17.4c-1.5.7-2.6 2-3 3.7L2.6 54c-.3 1.7.1 3.4 1.1 4.8l21.1 26.1c1.2 1.2 2.9 2 4.6 2.1H63c1.8.2 3.5-.6 4.6-2.1l21.1-26.1c1-1.4 1.4-3.1 1.2-4.8l-7.6-32.7z" style="fill:#326de6"/>
56+
<path class="st2" d="M77.6 52.7c-.1 0-.2 0-.2-.1s-.2-.1-.4-.1c-.4-.1-.8-.1-1.2-.1-.2 0-.4 0-.6-.1h-.1c-1.1-.1-2.3-.3-3.4-.6-.3-.1-.6-.4-.7-.7.1 0 0 0 0 0l-.8-.2c.4-2.9.2-5.9-.4-8.8-.7-2.9-1.9-5.7-3.5-8.2l.6-.6v-.1c0-.3.1-.7.3-.9.9-.8 1.8-1.4 2.8-2l.6-.3c.4-.2.7-.4 1.1-.6.1-.1.2-.1.3-.2s0-.1 0-.2c.9-.7 1.1-1.9.4-2.8-.3-.4-.9-.7-1.4-.7-.5 0-1 .2-1.4.5l-.1.1c-.1.1-.2.2-.3.2-.3.3-.6.6-.8.9-.1.2-.3.3-.4.4-.7.8-1.6 1.6-2.5 2.2-.2.1-.4.2-.6.2-.1 0-.3 0-.4-.1h-.1l-.8.5c-.8-.8-1.7-1.6-2.5-2.4-3.7-2.9-8.3-4.7-13-5.2l-.1-.8v.1c-.3-.2-.4-.5-.5-.8 0-1.1 0-2.2.2-3.4v-.1c0-.2.1-.4.1-.6.1-.4.1-.8.2-1.2v-.6c.1-1-.7-2-1.7-2.1-.6-.1-1.2.2-1.7.7-.4.4-.6.9-.6 1.4v.5c0 .4.1.8.2 1.2.1.2.1.4.1.6v.1c.2 1.1.2 2.2.2 3.4-.1.3-.2.6-.5.8v.2l-.1.8c-1.1.1-2.2.3-3.4.5-4.7 1-9 3.5-12.3 7l-.6-.4h-.1c-.1 0-.2.1-.4.1s-.4-.1-.6-.2c-.9-.7-1.8-1.5-2.5-2.3-.1-.2-.3-.3-.4-.4-.3-.3-.5-.6-.8-.9-.1-.1-.2-.1-.3-.2l-.1-.1c-.4-.3-.9-.5-1.4-.5-.6 0-1.1.2-1.4.7-.6.9-.4 2.1.4 2.8.1 0 .1.1.1.1s.2.2.3.2c.3.2.7.4 1.1.6l.6.3c1 .6 2 1.2 2.8 2 .2.2.4.6.3.9V33l.6.6c-.1.2-.2.3-.3.5-3.1 4.9-4.4 10.7-3.5 16.4l-.8.2c0 .1-.1.1-.1.1-.1.3-.4.5-.7.7-1.1.3-2.2.5-3.4.6-.2 0-.4 0-.6.1-.4 0-.8.1-1.2.1-.1 0-.2.1-.4.1-.1 0-.1 0-.2.1-1.1.2-1.8 1.2-1.6 2.3.2.9 1.1 1.5 2 1.4.2 0 .3 0 .5-.1.1 0 .1 0 .1-.1s.3 0 .4 0c.4-.1.8-.3 1.1-.4.2-.1.4-.2.6-.2h.1c1.1-.4 2.1-.7 3.3-.9h.1c.3 0 .6.1.8.3.1 0 .1.1.1.1l.9-.1c1.5 4.6 4.3 8.7 8.2 11.7.9.7 1.7 1.3 2.7 1.8l-.5.7c0 .1.1.1.1.1.2.3.2.7.1 1-.4 1-1 2-1.6 2.9v.1c-.1.2-.2.3-.4.5s-.4.6-.7 1c-.1.1-.1.2-.2.3 0 0 0 .1-.1.1-.5 1-.1 2.2.8 2.7.2.1.5.2.7.2.8 0 1.5-.5 1.9-1.2 0 0 0-.1.1-.1 0-.1.1-.2.2-.3.1-.4.3-.7.4-1.1l.2-.6c.3-1.1.8-2.1 1.3-3.1.2-.3.5-.5.8-.6.1 0 .1 0 .1-.1l.4-.8c2.8 1.1 5.7 1.6 8.7 1.6 1.8 0 3.6-.2 5.4-.7 1.1-.2 2.2-.6 3.2-.9l.4.7c.1 0 .1 0 .1.1.3.1.6.3.8.6.5 1 1 2 1.3 3.1v.1l.2.6c.1.4.2.8.4 1.1.1.1.1.2.2.3 0 0 0 .1.1.1.4.7 1.1 1.2 1.9 1.2.3 0 .5-.1.8-.2.4-.2.8-.6.9-1.1.1-.5.1-1-.1-1.5 0-.1-.1-.1-.1-.1 0-.1-.1-.2-.2-.3-.2-.4-.4-.7-.7-1-.1-.2-.2-.3-.4-.5V73c-.7-.9-1.2-1.9-1.6-2.9-.1-.3-.1-.7.1-1 0-.1.1-.1.1-.1l-.3-.8c5.1-3.1 9-7.9 10.8-13.6l.8.1c.1 0 .1-.1.1-.1.2-.2.5-.3.8-.3h.1c1.1.2 2.2.5 3.2.9h.1c.2.1.4.2.6.2.4.2.7.4 1.1.5.1 0 .2.1.4.1.1 0 .1 0 .2.1.2.1.3.1.5.1.9 0 1.7-.6 2-1.4-.1-1.1-.9-1.9-1.8-2.1zm-28.9-3.1L46 50.9l-2.7-1.3-.7-2.9 1.9-2.4h3l1.9 2.4-.7 2.9zM65 43.1c.5 2.1.6 4.2.4 6.3l-9.5-2.7c-.9-.2-1.4-1.1-1.2-2 .1-.3.2-.5.4-.7l7.5-6.8c1.1 1.8 1.9 3.8 2.4 5.9zm-5.4-9.6-8.2 5.8c-.7.4-1.7.3-2.2-.4-.2-.2-.3-.4-.3-.7l-.6-10.1c4.4.5 8.3 2.4 11.3 5.4zm-18.1-5.1 2-.4-.5 10c0 .9-.8 1.6-1.7 1.6-.3 0-.5-.1-.8-.2l-8.3-5.9c2.6-2.5 5.8-4.3 9.3-5.1zm-12.2 8.8 7.4 6.6c.7.6.8 1.6.2 2.3-.2.3-.4.4-.8.5l-9.7 2.8c-.3-4.2.7-8.5 2.9-12.2zm-1.7 16.9 9.9-1.7c.8 0 1.6.5 1.7 1.3.1.3.1.7-.1 1l-3.8 9.2c-3.5-2.3-6.3-5.8-7.7-9.8zm22.7 12.4c-1.4.3-2.8.5-4.3.5-2.1 0-4.3-.4-6.3-1l4.9-8.9c.5-.6 1.3-.8 2-.4.3.2.5.4.8.7l4.8 8.7c-.6.1-1.2.2-1.9.4zm12.2-8.7c-1.5 2.4-3.6 4.5-6 6l-3.9-9.4c-.2-.8.2-1.6.9-1.9.3-.1.6-.2.9-.2l10 1.7c-.5 1.4-1.1 2.7-1.9 3.8z"/>
57+
<g id="layer1">
58+
<g id="text4373">
59+
<path id="path2985" class="st2" d="M128.1 48.4c1.1-1.2 2.1-2.4 3.3-3.6 1.1-1.3 2.2-2.5 3.3-3.7 1.1-1.3 2.1-2.4 3-3.5s1.8-2.1 2.5-2.9H153c-2.6 2.9-5.1 5.8-7.5 8.5-2.5 2.7-5.1 5.5-8 8.3 1.6 1.5 3.1 3 4.5 4.7 1.5 1.8 3 3.6 4.5 5.6 1.4 1.9 2.8 3.9 4 5.8 1.2 1.9 2.2 3.7 3 5.3h-12.4c-.8-1.3-1.7-2.6-2.7-4.1s-2.1-3-3.1-4.6c-1.1-1.5-2.3-3-3.6-4.4-1.1-1.3-2.3-2.5-3.6-3.5V73h-10.8V18.2l10.8-1.7v31.9"/>
60+
<path id="path2987" class="st2" d="M191.1 71.4c-2.3.6-4.7 1.1-7.1 1.4-3 .5-6.1.7-9.1.7-2.8.1-5.5-.4-8.1-1.3-2-.8-3.7-2-5.1-3.6-1.3-1.7-2.2-3.6-2.7-5.7-.6-2.3-.8-4.8-.8-7.2V34.6H169v19.9c0 3.5.5 6 1.4 7.5s2.6 2.3 5.1 2.3c.8 0 1.6 0 2.5-.1s1.6-.1 2.3-.3V34.6h10.8v36.8"/>
61+
<path id="path2989" class="st2" d="M225.7 53.4c0-7-2.6-10.4-7.7-10.4-1.1 0-2.2.1-3.3.4-.9.2-1.8.6-2.6 1.1v19.6c.5.1 1.2.2 2 .3.8.1 1.7.1 2.7.1 2.6.2 5.1-.9 6.7-3 1.5-2.4 2.3-5.2 2.2-8.1m11 .4c0 2.8-.4 5.6-1.4 8.3-.8 2.4-2.1 4.5-3.8 6.3-1.8 1.8-3.9 3.2-6.2 4.1-2.7 1-5.5 1.4-8.4 1.4-1.3 0-2.7-.1-4.1-.2-1.4-.1-2.8-.3-4.2-.4-1.3-.2-2.6-.4-3.9-.7-1.3-.2-2.4-.5-3.3-.9V18.2l10.8-1.7v19c1.2-.5 2.5-.9 3.8-1.2 1.4-.3 2.8-.4 4.2-.4 2.5 0 4.9.4 7.2 1.5 2 .9 3.8 2.3 5.2 4 1.5 1.9 2.6 4 3.2 6.3.6 2.5.9 5.3.9 8.1"/>
62+
<path id="path2991" class="st2" d="M243.2 54c-.1-3 .5-6 1.5-8.8.9-2.4 2.3-4.5 4.1-6.4 1.7-1.7 3.6-3 5.8-3.8 2.2-.9 4.5-1.3 6.8-1.3 5.4 0 9.7 1.7 12.8 5 3.1 3.3 4.7 8.2 4.7 14.5 0 .6 0 1.3-.1 2.1s-.1 1.4-.1 2h-24.5c.2 2.1 1.3 4.1 3.1 5.3 2.2 1.4 4.8 2.1 7.4 2 1.9 0 3.9-.2 5.8-.5 1.6-.3 3.2-.8 4.7-1.4l1.5 8.8c-.7.4-1.5.6-2.3.9-1.1.3-2.2.6-3.3.7-1.2.2-2.4.4-3.8.6-1.3.1-2.7.2-4.1.2-3.1.1-6.1-.4-9-1.5-2.4-.9-4.5-2.3-6.3-4.1-1.7-1.8-2.9-4-3.7-6.3-.6-2.7-1-5.3-1-8m25.4-4.1c0-.9-.2-1.8-.5-2.7-.2-.9-.7-1.6-1.2-2.3-.6-.7-1.3-1.3-2.1-1.7-1-.5-2-.7-3.1-.7s-2.1.2-3.1.7c-.8.4-1.6.9-2.2 1.6-.6.7-1.1 1.5-1.4 2.4-.3.9-.5 1.8-.6 2.7h14.2"/>
63+
<path id="path2993" class="st2" d="M310.3 44.2c-1-.2-2.1-.5-3.4-.7-1.4-.3-2.8-.4-4.2-.4-.8 0-1.6.1-2.5.2-.7.1-1.4.2-2.1.4v29.1h-10.8V36.6c2.2-.8 4.5-1.4 6.8-1.9 2.9-.7 5.9-1 8.8-.9.7 0 1.4.1 2.1.1.8 0 1.6.1 2.5.3.8.1 1.6.2 2.5.4.7.1 1.4.3 2.1.6l-1.8 9"/>
64+
<path id="path2995" class="st2" d="M317.9 35.9c2.3-.6 4.7-1.1 7.1-1.5 3-.5 6.1-.7 9.1-.7 2.7-.1 5.4.4 8 1.3 2 .7 3.8 1.9 5.1 3.5 1.3 1.6 2.2 3.5 2.7 5.5.6 2.3.8 4.7.8 7.1v21.5H340V52.5c0-3.5-.5-5.9-1.4-7.4-.9-1.4-2.6-2.2-5.1-2.2-.8 0-1.6 0-2.5.1-.9 0-1.6.1-2.3.2v29.4h-10.8V35.9"/>
65+
<path id="path2997" class="st2" d="M358.9 54c-.1-3 .5-6 1.5-8.8.9-2.4 2.3-4.5 4.1-6.4 1.7-1.7 3.6-3 5.8-3.8 2.2-.9 4.5-1.3 6.8-1.3 5.4 0 9.7 1.7 12.8 5 3.1 3.3 4.7 8.2 4.7 14.6 0 .6 0 1.3-.1 2.1s-.1 1.4-.1 2h-24.5c.2 2.1 1.3 4.1 3.1 5.3 2.2 1.4 4.8 2.1 7.4 2 1.9 0 3.9-.2 5.8-.5 1.6-.3 3.2-.8 4.8-1.5l1.5 8.8c-.7.4-1.5.6-2.3.9-1.1.3-2.2.6-3.3.7-1.2.2-2.4.4-3.8.6-1.3.1-2.7.2-4.1.2-3.1.1-6.1-.4-9-1.5-2.4-.9-4.5-2.3-6.3-4.1-1.7-1.8-2.9-4-3.7-6.3-.8-2.7-1.2-5.4-1.1-8m25.3-4.1c0-.9-.2-1.8-.5-2.7-.2-.9-.7-1.6-1.2-2.3-.6-.7-1.3-1.3-2.1-1.7-1-.5-2-.7-3.1-.7s-2.1.2-3.1.7c-.8.4-1.5.9-2.1 1.6-.6.7-1.1 1.5-1.4 2.4-.3.9-.5 1.8-.6 2.7h14.1"/>
66+
<path id="path2999" class="st2" d="m402.6 25.1 10.8-1.7v11.2h13v9h-13V57c-.1 1.9.3 3.8 1.2 5.4.8 1.3 2.4 2 4.9 2 1.2 0 2.4-.1 3.5-.3 1.2-.2 2.3-.5 3.4-.9l1.5 8.4c-1.4.6-2.9 1-4.4 1.4-1.9.4-3.9.6-5.9.6-2.5.1-5-.3-7.3-1.2-1.8-.7-3.4-1.9-4.6-3.3-1.2-1.5-2-3.3-2.5-5.2-.5-2.2-.7-4.5-.6-6.7V25.1"/>
67+
<path id="path3001" class="st2" d="M431.9 54c-.1-3 .5-6 1.5-8.8.9-2.4 2.3-4.5 4.1-6.4 1.7-1.7 3.6-3 5.8-3.8 2.2-.9 4.5-1.3 6.8-1.3 5.4 0 9.7 1.7 12.8 5s4.7 8.2 4.7 14.6c0 .6 0 1.3-.1 2.1 0 .8-.1 1.4-.1 2H443c.2 2.1 1.3 4.1 3.1 5.3 2.2 1.4 4.8 2.1 7.4 2 1.9 0 3.9-.2 5.8-.5 1.6-.3 3.2-.8 4.7-1.5l1.5 8.8c-.7.4-1.5.6-2.3.9-1.1.3-2.2.6-3.3.7-1.2.2-2.4.4-3.8.6-1.3.1-2.7.2-4.1.2-3.1.1-6.1-.4-9-1.5-2.4-.9-4.5-2.3-6.3-4.1-1.7-1.8-2.9-4-3.7-6.3-.7-2.7-1.1-5.4-1.1-8m25.3-4.1c0-.9-.2-1.8-.5-2.7-.2-.9-.7-1.6-1.2-2.3-.6-.7-1.3-1.3-2.1-1.7-1-.5-2-.7-3.1-.7s-2.1.2-3.1.7c-.8.4-1.6.9-2.2 1.6-.6.7-1.1 1.5-1.4 2.4-.3.9-.5 1.8-.6 2.7h14.2"/>
68+
<path id="path3003" class="st2" d="M487 65c1.4.1 2.9-.1 4.2-.6.8-.4 1.3-1.3 1.2-2.2-.1-1-.7-1.9-1.6-2.2-1.5-.9-3.2-1.7-4.9-2.2-1.7-.6-3.2-1.3-4.6-2-1.3-.6-2.4-1.4-3.5-2.4-1-1-1.7-2.1-2.2-3.4-.6-1.5-.8-3.1-.8-4.7-.1-3.3 1.4-6.5 4-8.5 2.7-2.1 6.3-3.1 10.9-3.1 2.2 0 4.5.2 6.7.7 1.7.3 3.4.7 5.1 1.3l-1.9 8.5c-1.4-.5-2.7-.8-4.1-1.2-1.6-.4-3.3-.5-4.9-.5-3.4 0-5.1.9-5.1 2.8 0 .4.1.8.2 1.2.2.4.5.7.9 1 .4.3 1 .6 1.7 1s1.7.8 2.9 1.2c2 .7 4 1.6 5.8 2.6 1.4.7 2.6 1.6 3.6 2.8.9.9 1.5 2 1.9 3.3.4 1.4.6 2.8.6 4.2.2 3.4-1.4 6.7-4.3 8.6-2.8 1.9-6.8 2.9-12 2.9-2.9.1-5.7-.2-8.5-.9-1.6-.4-3.1-.9-4.6-1.4l1.8-8.8c1.8.7 3.7 1.3 5.6 1.7 2 .1 3.9.3 5.9.3"/>
69+
</g>
70+
</g>
71+
</g>
72+
</g>
73+
</svg>
74+
</a>
75+
</div>
76+
</div>
77+
</nav>
78+
79+
<div class="container">
80+
<div class="box content" id="app">
81+
<div v-if="namespace !== ''">
82+
<h3>My TODO items for namespace: {{ namespace }}</h3>
83+
<ul>
84+
<li v-for="item in items" :key="item.title">
85+
{{ item }}
86+
</li>
87+
<li v-if="items.length === 0"><em>No items found</em></li>
88+
</ul>
89+
</div>
90+
91+
<div v-else style="text-align: center">
92+
<div class="lds-hourglass"></div>
93+
</div>
94+
</div>
95+
</div>
96+
97+
<script>
98+
const endpoint = "/api/todos"
99+
const app = new Vue({
100+
el: '#app',
101+
102+
data: {
103+
namespace: '',
104+
items: []
105+
},
106+
107+
methods: {
108+
fetchItems() {
109+
fetch(endpoint)
110+
.then(response => response.json())
111+
.then(data => {
112+
this.namespace = data.namespace;
113+
this.items = data.items;
114+
})
115+
}
116+
},
117+
118+
mounted() {
119+
this.fetchItems();
120+
}
121+
})
122+
</script>
123+
</body>
124+
</html>

0 commit comments

Comments
 (0)