Add 2023 day 84 What is an API

Signed-off-by: Alistair Hey <alistair@heyal.co.uk>
This commit is contained in:
Alistair Hey
2023-03-24 14:46:31 +00:00
parent 8657e7f413
commit 4ff8c158a9
13 changed files with 570 additions and 1 deletions

View File

@ -0,0 +1,44 @@
# Getting started
This repo expects you to have a working kubernetes cluster already setup and
available with kubectl
We expect you already have a kubernetes cluster setup and available with kubectl and helm.
I like using (Civo)[https://www.civo.com/] for this as it is easy to setup and run clusters
The code is available in this folder to build/push your own images if you wish - there are no instructions for this.
## Start the Database
```shell
kubectl apply -f database/mysql.yaml
```
## deploy the day1 - sync
```shell
kubectl apply -f synchronous/k8s.yaml
```
Check your logs
```shell
kubectl logs deploy/generator
kubectl logs deploy/requestor
```
## deploy nats
helm repo add nats https://nats-io.github.io/k8s/helm/charts/
helm install my-nats nats/nats
## deploy day 2 - async
```shell
kubectl apply -f async/k8s.yaml
```
Check your logs
```shell
kubectl logs deploy/generator
kubectl logs deploy/requestor
```

View File

@ -0,0 +1,77 @@
apiVersion: v1
kind: Service
metadata:
name: mysql
spec:
ports:
- port: 3306
selector:
app: mysql
clusterIP: None
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: mysql
spec:
selector:
matchLabels:
app: mysql
strategy:
type: Recreate
template:
metadata:
labels:
app: mysql
spec:
containers:
- image: mysql:5.6
name: mysql
env:
# Use secret in real usage
- name: MYSQL_ROOT_PASSWORD
value: password
- name: MYSQL_DATABASE
value: example
- name: MYSQL_USER
value: example
- name: MYSQL_PASSWORD
value: password
ports:
- containerPort: 3306
name: mysql
volumeMounts:
- name: mysql-persistent-storage
mountPath: /var/lib/mysql
volumes:
- name: mysql-persistent-storage
persistentVolumeClaim:
claimName: mysql-pv-claim
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: mysql-pv-volume
labels:
type: local
spec:
storageClassName: manual
capacity:
storage: 20Gi
accessModes:
- ReadWriteOnce
hostPath:
path: "/mnt/data"
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mysql-pv-claim
spec:
storageClassName: manual
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
# https://kubernetes.io/docs/tasks/run-application/run-single-instance-stateful-application/

View File

@ -0,0 +1,17 @@
# Set the base image to use
FROM golang:1.17-alpine
# Set the working directory inside the container
WORKDIR /app
# Copy the source code into the container
COPY . .
# Build the Go application
RUN go build -o main .
# Expose the port that the application will run on
EXPOSE 8080
# Define the command that will run when the container starts
CMD ["/app/main"]

View File

@ -0,0 +1,5 @@
module main
go 1.20
require github.com/go-sql-driver/mysql v1.7.0

View File

@ -0,0 +1,2 @@
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=

View File

@ -0,0 +1,139 @@
package main
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
"math/rand"
"net/http"
"time"
)
func generateAndStoreString() (string, error) {
// Connect to the database
db, err := sql.Open("mysql", "root:password@tcp(mysql:3306)/mysql")
if err != nil {
return "", err
}
defer db.Close()
// Generate a random string
// Define a string of characters to use
characters := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
// Generate a random string of length 10
randomString := make([]byte, 64)
for i := range randomString {
randomString[i] = characters[rand.Intn(len(characters))]
}
// Insert the random number into the database
_, err = db.Exec("INSERT INTO generator(random_string) VALUES(?)", string(randomString))
if err != nil {
return "", err
}
fmt.Printf("Random string %s has been inserted into the database\n", string(randomString))
return string(randomString), nil
}
func main() {
// Create a new HTTP server
server := &http.Server{
Addr: ":8080",
}
err := createGeneratordb()
if err != nil {
panic(err.Error())
}
ticker := time.NewTicker(60 * time.Second)
quit := make(chan struct{})
go func() {
for {
select {
case <-ticker.C:
checkStringReceived()
case <-quit:
ticker.Stop()
return
}
}
}()
// Handle requests to /generate
http.HandleFunc("/new", func(w http.ResponseWriter, r *http.Request) {
// Generate a random number
randomString, err := generateAndStoreString()
if err != nil {
http.Error(w, "unable to generate and save random string", http.StatusInternalServerError)
return
}
print(fmt.Sprintf("random string: %s", randomString))
w.Write([]byte(randomString))
})
// Start the server
fmt.Println("Listening on port 8080")
err = server.ListenAndServe()
if err != nil {
panic(err.Error())
}
}
func createGeneratordb() error {
db, err := sql.Open("mysql", "root:password@tcp(mysql:3306)/mysql")
if err != nil {
return err
}
defer db.Close()
// try to create a table for us
_, err = db.Exec("CREATE TABLE IF NOT EXISTS generator(random_string VARCHAR(100), seen BOOLEAN)")
return err
}
func checkStringReceived() {
// get a list of strings from database that dont have the "seen" bool set top true
// loop over them and make a call to the requestor's 'check' endpoint and if we get a 200 then set seen to true
// Connect to the database
db, err := sql.Open("mysql", "root:password@tcp(mysql:3306)/mysql")
if err != nil {
print(err)
}
defer db.Close()
// Insert the random number into the database
results, err := db.Query("SELECT random_string FROM generator WHERE seen IS NOT true")
if err != nil {
print(err)
}
// loop over results
for results.Next() {
var randomString string
err = results.Scan(&randomString)
if err != nil {
print(err)
}
// make a call to the requestor's 'check' endpoint
// if we get a 200 then set seen to true
r, err := http.Get("http://requestor-service:8080/check/" + randomString)
if err != nil {
print(err)
}
if r.StatusCode == 200 {
_, err = db.Exec("UPDATE generator SET seen = true WHERE random_string = ?", randomString)
if err != nil {
print(err)
}
} else {
fmt.Println(fmt.Sprintf("Random string has not been received by the requestor: %s", randomString))
}
}
}

View File

@ -0,0 +1,69 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: requestor
spec:
replicas: 1
selector:
matchLabels:
app: requestor
template:
metadata:
labels:
app: requestor
spec:
containers:
- name: requestor
image: heyal/requestor:sync
imagePullPolicy: Always
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: requestor-service
spec:
selector:
app: requestor
ports:
- name: http
protocol: TCP
port: 8080
targetPort: 8080
type: ClusterIP
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: generator
spec:
replicas: 1
selector:
matchLabels:
app: generator
template:
metadata:
labels:
app: generator
spec:
containers:
- name: generator
image: heyal/generator:sync
imagePullPolicy: Always
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: generator-service
spec:
selector:
app: generator
ports:
- name: http
protocol: TCP
port: 8080
targetPort: 8080
type: ClusterIP

View File

@ -0,0 +1,17 @@
# Set the base image to use
FROM golang:1.17-alpine
# Set the working directory inside the container
WORKDIR /app
# Copy the source code into the container
COPY . .
# Build the Go application
RUN go build -o main .
# Expose the port that the application will run on
EXPOSE 8080
# Define the command that will run when the container starts
CMD ["/app/main"]

View File

@ -0,0 +1,5 @@
module main
go 1.20
require github.com/go-sql-driver/mysql v1.7.0

View File

@ -0,0 +1,2 @@
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=

View File

@ -0,0 +1,134 @@
package main
import (
"database/sql"
"errors"
"fmt"
_ "github.com/go-sql-driver/mysql"
"io"
"net/http"
"strings"
"time"
)
func storeString(input string) error {
// Connect to the database
db, err := sql.Open("mysql", "root:password@tcp(mysql:3306)/mysql")
defer db.Close()
// Insert the random number into the database
_, err = db.Exec("INSERT INTO requestor(random_string) VALUES(?)", input)
if err != nil {
return err
}
fmt.Printf("Random string %s has been inserted into the database\n", input)
return nil
}
func getStringFromDB(input string) error {
// see if the string exists in the db, if so return nil
// if not, return an error
db, err := sql.Open("mysql", "root:password@tcp(mysql:3306)/mysql")
defer db.Close()
result, err := db.Query("SELECT * FROM requestor WHERE random_string = ?", input)
if err != nil {
return err
}
for result.Next() {
var randomString string
err = result.Scan(&randomString)
if err != nil {
return err
}
if randomString == input {
return nil
}
}
return errors.New("string not found")
}
func getStringFromGenerator() {
// make a request to the generator
// save sthe string to db
r, err := http.Get("http://generator-service:8080/new")
if err != nil {
fmt.Println(err)
return
}
body, err := io.ReadAll(r.Body)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(fmt.Sprintf("body from generator: %s", string(body)))
storeString(string(body))
}
func main() {
// setup a goroutine loop calling the generator every minute, saving the result in the DB
ticker := time.NewTicker(60 * time.Second)
quit := make(chan struct{})
go func() {
for {
select {
case <-ticker.C:
getStringFromGenerator()
case <-quit:
ticker.Stop()
return
}
}
}()
// Create a new HTTP server
server := &http.Server{
Addr: ":8080",
}
err := createRequestordb()
if err != nil {
panic(err.Error())
}
// Handle requests to /generate
http.HandleFunc("/check/", func(w http.ResponseWriter, r *http.Request) {
// get the value after check from the url
id := strings.TrimPrefix(r.URL.Path, "/check/")
// check if it exists in the db
err := getStringFromDB(id)
if err != nil {
http.Error(w, "string not found", http.StatusNotFound)
return
}
fmt.Fprintf(w, "string found", http.StatusOK)
})
// Start the server
fmt.Println("Listening on port 8080")
err = server.ListenAndServe()
if err != nil {
panic(err.Error())
}
}
func createRequestordb() error {
db, err := sql.Open("mysql", "root:password@tcp(mysql:3306)/mysql")
if err != nil {
return err
}
defer db.Close()
// try to create a table for us
_, err = db.Exec("CREATE TABLE IF NOT EXISTS requestor(random_string VARCHAR(100))")
return err
}