Skip to content

Commit

Permalink
Merge pull request #12 from kolayne-IU-assignments/lab12
Browse files Browse the repository at this point in the history
Lab12
  • Loading branch information
kolayne authored May 17, 2024
2 parents 511e8b1 + 5c7aabd commit 0f5d07a
Show file tree
Hide file tree
Showing 19 changed files with 227 additions and 18 deletions.
3 changes: 3 additions & 0 deletions app_go/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,6 @@
go.work

# End of https://www.toptal.com/developers/gitignore/api/go


/persistent
4 changes: 4 additions & 0 deletions app_go/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
FROM golang:1.22.0-alpine3.19 as builder

RUN ["mkdir", "/empty-dir"]

WORKDIR /usr/src/app/

COPY go.mod *.go /usr/src/app/
Expand All @@ -16,6 +18,8 @@ COPY --from=0 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.

EXPOSE 5000

VOLUME /persistent
COPY --from=builder /usr/src/app/catfact_webapp /
USER 2004:2004
COPY --from=builder --chown=2004:2004 /empty-dir /persistent
ENTRYPOINT ["/catfact_webapp"]
46 changes: 43 additions & 3 deletions app_go/main.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
package main

import (
"sync/atomic"
"encoding/binary"
"errors"
"path/filepath"
"fmt"
"log"
"net/http"
"os"
"time"

"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/prometheus/client_golang/prometheus/promhttp"
)

func index(w http.ResponseWriter, r *http.Request) {
const visitsFile = "persistent/visits.bin"
var visits atomic.Uint64

func indexHandler(w http.ResponseWriter, r *http.Request) {
fact, err := catFact()
if err == nil {
w.WriteHeader(http.StatusOK)
Expand All @@ -22,6 +30,10 @@ func index(w http.ResponseWriter, r *http.Request) {
}
}

func visitsHandler(w http.ResponseWriter, r *http.Request) {
_, _ = fmt.Fprintf(w, "%d", visits.Load())
}


var (
reqCnt = promauto.NewCounter(prometheus.CounterOpts{
Expand All @@ -48,14 +60,42 @@ func noteTimeMiddleware(next http.Handler) http.Handler {
})
}

func countVisitsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
visits.Add(1) // Atomic increment, no race condition here, so counter
// is always correct
buf := make([]byte, binary.MaxVarintLen64)
binary.LittleEndian.PutUint64(buf, visits.Load())
// Race condition in the order in which threads will write the file out,
// so the file may not be correct, but as the same number of bytes is
// always written out, the counter in the file remains a valid number.
os.WriteFile(visitsFile, buf, 0644)
next.ServeHTTP(w, r)
})
}


func init() {
_ = os.MkdirAll(filepath.Dir(visitsFile), 0755)
buf, err := os.ReadFile(visitsFile)
if err == nil {
visits.Store(binary.LittleEndian.Uint64(buf))
} else if errors.Is(err, os.ErrNotExist) {
visits.Store(0)
} else {
panic(err)
}
}


func main() {
businessLogic := http.NewServeMux()
businessLogic.Handle("/", asHandler(index))
businessLogic.Handle("/", asHandler(indexHandler))
businessLogic.Handle("/visits", asHandler(visitsHandler))
// Note: keeping /metrics under middleware too for consistency with app_py
businessLogic.Handle("/metrics", promhttp.Handler())

wrapped := noteTimeMiddleware(businessLogic)
wrapped := noteTimeMiddleware(countVisitsMiddleware(businessLogic))

hostPort := "0.0.0.0:5000"
_, _ = fmt.Println("Listening on http://" + hostPort)
Expand Down
2 changes: 1 addition & 1 deletion app_go/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
func TestFactLoads(t *testing.T) {
w := httptest.NewRecorder()

index(w, nil)
indexHandler(w, nil)
resp := w.Result()

if resp.StatusCode != http.StatusOK {
Expand Down
3 changes: 3 additions & 0 deletions app_python/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -174,3 +174,6 @@ poetry.toml
pyrightconfig.json

# End of https://www.toptal.com/developers/gitignore/api/python


/persistent
2 changes: 2 additions & 0 deletions app_python/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ COPY requirements.txt /app/
COPY moscow_time/ /app/moscow_time
# Note: keeping the project files owned by root so
# the web server has less privileges over them
RUN ["mkdir", "--mode", "777", "/app/persistent"]
VOLUME /app/persistent
USER flask:flask
RUN ["pip", "install", "--user", "-r", "requirements.txt"]
CMD ["python", "-m", "moscow_time"]
3 changes: 3 additions & 0 deletions app_python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ docker run --rm -d -p 5000 kolay0ne/app_py

Replace `kolay0ne/app_py` with your image/tag name if you built it manually.

One may want to mount a volume or a bind-mount at `/app/persistent`, which acts
as a persistent storage for the visits counter of the web app.

## Unit Tests

To run unit tests:
Expand Down
24 changes: 23 additions & 1 deletion app_python/moscow_time/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import datetime
from os import makedirs
from time import monotonic

from flask import Flask, request, Response
from flask import Flask, request, Response, send_from_directory
import requests
import prometheus_client

from .cache import cache_for
from .visits import increment_on_call


app = Flask(__name__)
Expand Down Expand Up @@ -42,12 +44,32 @@ def get_time():
return dt.time()


VISITS_FILENAME = 'persistent/visits.bin'

# Create it on start
try:
basename_at = VISITS_FILENAME.rindex('/')
except ValueError:
pass
else:
makedirs(VISITS_FILENAME[:basename_at], exist_ok=True)
open(VISITS_FILENAME, 'a+').close() # The `a+` mode ensures we have write perm


@app.route('/')
@increment_on_call(VISITS_FILENAME)
def index():
time = get_time()
return f"In MSK it's {time.hour}:{time.minute}:{time.second}. " \
"Have you brushed your teeth today yet?"

@app.route('/metrics')
@increment_on_call(VISITS_FILENAME)
def prometheus_metrics():
return Response(prometheus_client.generate_latest(), mimetype='text/plain')

@app.route('/visits')
@increment_on_call(VISITS_FILENAME)
def visits():
with open(VISITS_FILENAME, 'rb') as f:
return str(int.from_bytes(f.read(), byteorder='little'))
28 changes: 28 additions & 0 deletions app_python/moscow_time/visits.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import fcntl
from functools import wraps
from threading import Lock
from typing import Callable


def increment(filename: str) -> None:
with open(filename, 'rb+') as f:
try:
fcntl.flock(f.fileno(), fcntl.LOCK_EX)
cur = int.from_bytes(f.read(), byteorder='little')
f.seek(0)
cur += 1
f.write(cur.to_bytes(byteorder='little', length=(cur.bit_length() // 8 + 1)))
f.truncate() # Not necessary, as larger numbers take more bytes
finally:
# Note: will be unlocked anyway when the file is closed
fcntl.flock(f.fileno(), fcntl.LOCK_UN)


def increment_on_call(filename: str) -> Callable[[Callable, ...], Callable]:
def decorator(func: Callable) -> Callable:
@wraps(func)
def with_increment(*args, **kwargs):
increment(filename)
return func(*args, **kwargs)
return with_increment
return decorator
81 changes: 81 additions & 0 deletions k8s/12.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Lab 12

## ConfigMap file

```sh
$ helm install app-py . -f secrets://./env-secrets.yaml
NAME: app-py
LAST DEPLOYED: Mon Apr 22 16:55:16 2024
NAMESPACE: default
STATUS: deployed
REVISION: 1
NOTES:
1. Get the application URL by running these commands:
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
You can watch the status of by running 'kubectl get --namespace default svc -w app-py'
export SERVICE_IP=$(kubectl get svc --namespace default app-py --template "{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}")
echo http://$SERVICE_IP:5000
$ kubectl wait deployment/app-py --for condition=available
deployment.apps/app-py condition met
$ kubectl get po
NAME READY STATUS RESTARTS AGE
app-py-7d7d86657c-df667 1/1 Running 0 30s
app-py-7d7d86657c-hq8nw 1/1 Running 0 30s
app-py-7d7d86657c-jxgrj 1/1 Running 0 30s
app-py-7d7d86657c-twd5l 1/1 Running 0 30s
$ kubectl exec app-py-7d7d86657c-twd5l -- cat /persistent/config.json
{"mole": ["hamsters"], "hamster": ["moles"]}
$
```

## ConfigMap env

```sh
$ helm install app-go .
NAME: app-go
LAST DEPLOYED: Mon Apr 22 19:26:23 2024
NAMESPACE: default
STATUS: deployed
REVISION: 1
NOTES:
1. Get the application URL by running these commands:
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
You can watch the status of by running 'kubectl get --namespace default svc -w app-go-app-py'
export SERVICE_IP=$(kubectl get svc --namespace default app-go-app-py --template "{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}")
echo http://$SERVICE_IP:5000
$ kubectl get po
NAME READY STATUS RESTARTS AGE
app-go-app-py-59f4c5c69d-8fljp 1/1 Running 0 22s
app-go-app-py-59f4c5c69d-bv496 1/1 Running 0 22s
app-go-app-py-59f4c5c69d-jlz6q 1/1 Running 0 22s
app-go-app-py-59f4c5c69d-t2nnn 1/1 Running 0 22s
$ kubectl debug --image=gcc -it app-go-app-py-59f4c5c69d-8fljp --target=app-py
Targeting container "app-py". If you don't see processes from this container it may be because the container runtime doesn't support this feature.
Defaulting debug container name to debugger-864lv.
If you don't see a command prompt, try pressing enter.
root@app-go-app-py-59f4c5c69d-8fljp:/# echo 'main(){setreuid(geteuid(),geteuid());execl("/bin/sh","sh",0);}' > a.c
root@app-go-app-py-59f4c5c69d-8fljp:/# gcc -o a a.c
a.c:1:1: warning: return type defaults to 'int' [-Wimplicit-int]
1 | main(){setreuid(geteuid(),geteuid());execl("/bin/sh","sh",0);}
| ^~~~
a.c: In function 'main':
a.c:1:8: warning: implicit declaration of function 'setreuid' [-Wimplicit-function-declaration]
1 | main(){setreuid(geteuid(),geteuid());execl("/bin/sh","sh",0);}
| ^~~~~~~~
a.c:1:17: warning: implicit declaration of function 'geteuid' [-Wimplicit-function-declaration]
1 | main(){setreuid(geteuid(),geteuid());execl("/bin/sh","sh",0);}
| ^~~~~~~
a.c:1:38: warning: implicit declaration of function 'execl' [-Wimplicit-function-declaration]
1 | main(){setreuid(geteuid(),geteuid());execl("/bin/sh","sh",0);}
| ^~~~~
a.c:1:38: warning: incompatible implicit declaration of built-in function 'execl' [-Wbuiltin-declaration-mismatch]
root@app-go-app-py-59f4c5c69d-8fljp:/# chown 2004:2004 a
root@app-go-app-py-59f4c5c69d-8fljp:/# chmod u+s a
root@app-go-app-py-59f4c5c69d-8fljp:/# exec ./a
$ cat /proc/1/environ | sed 's/\x0/\n/g' | grep seal
seal=2001
monk_seal=century
$ exit
Session ended, the ephemeral container will not be restarted but may be reattached using 'kubectl attach app-go-app-py-59f4c5c69d-8fljp -c debugger-864lv -i -t' if it is still running
$
```
Binary file added k8s/app-go/charts/label-lib-0.1.0.tgz
Binary file not shown.
2 changes: 2 additions & 0 deletions k8s/app-go/files/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
seal: "2001"
monk_seal: "century"
6 changes: 6 additions & 0 deletions k8s/app-go/templates/config-map.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
apiVersion: "v1"
kind: ConfigMap
metadata:
name: {{.Release.Name}}-config-map
data:
{{ .Files.Get "files/config.yaml" | indent 2 }}
3 changes: 3 additions & 0 deletions k8s/app-go/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ spec:
{{- end }}
env:
{{ include "app-go.environ" . | nindent 12 }}
envFrom:
- configMapRef:
name: {{.Release.Name}}-config-map
{{- with .Values.volumes }}
volumes:
{{- toYaml . | nindent 8 }}
Expand Down
2 changes: 1 addition & 1 deletion k8s/app-go/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ image:
repository: kolay0ne/app_go
pullPolicy: IfNotPresent
# Overrides the image tag whose default is the chart appVersion.
tag: "lab8"
tag: "lab12"

serviceAccount:
# Specifies whether a service account should be created
Expand Down
1 change: 1 addition & 0 deletions k8s/app-py/files/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"mole": ["hamsters"], "hamster": ["moles"]}
6 changes: 6 additions & 0 deletions k8s/app-py/templates/config_map.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-config-map
data:
config.json: {{ .Files.Get "files/config.json" | quote }}
19 changes: 9 additions & 10 deletions k8s/app-py/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ image:
repository: kolay0ne/app_py
pullPolicy: IfNotPresent
# Overrides the image tag whose default is the chart appVersion.
tag: "lab8"
tag: "lab12"

serviceAccount:
# Specifies whether a service account should be created
Expand Down Expand Up @@ -84,17 +84,16 @@ autoscaling:
# targetMemoryUtilizationPercentage: 80

# Additional volumes on the output Deployment definition.
volumes: []
# - name: foo
# secret:
# secretName: mysecret
# optional: false
volumes:
- name: config-map
configMap:
name: app-py-config-map

# Additional volumeMounts on the output Deployment definition.
volumeMounts: []
# - name: foo
# mountPath: "/etc/foo"
# readOnly: true
volumeMounts:
- name: config-map
mountPath: "/persistent"
readOnly: true

nodeSelector: {}

Expand Down
Loading

0 comments on commit 0f5d07a

Please sign in to comment.