Skip to content

Commit 0f5d07a

Browse files
authored
Merge pull request #12 from kolayne-IU-assignments/lab12
Lab12
2 parents 511e8b1 + 5c7aabd commit 0f5d07a

File tree

19 files changed

+227
-18
lines changed

19 files changed

+227
-18
lines changed

app_go/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,6 @@
2525
go.work
2626

2727
# End of https://www.toptal.com/developers/gitignore/api/go
28+
29+
30+
/persistent

app_go/Dockerfile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
FROM golang:1.22.0-alpine3.19 as builder
22

3+
RUN ["mkdir", "/empty-dir"]
4+
35
WORKDIR /usr/src/app/
46

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

1719
EXPOSE 5000
1820

21+
VOLUME /persistent
1922
COPY --from=builder /usr/src/app/catfact_webapp /
2023
USER 2004:2004
24+
COPY --from=builder --chown=2004:2004 /empty-dir /persistent
2125
ENTRYPOINT ["/catfact_webapp"]

app_go/main.go

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,25 @@
11
package main
22

33
import (
4+
"sync/atomic"
5+
"encoding/binary"
6+
"errors"
7+
"path/filepath"
48
"fmt"
59
"log"
610
"net/http"
11+
"os"
712
"time"
813

914
"github.com/prometheus/client_golang/prometheus"
1015
"github.com/prometheus/client_golang/prometheus/promauto"
1116
"github.com/prometheus/client_golang/prometheus/promhttp"
1217
)
1318

14-
func index(w http.ResponseWriter, r *http.Request) {
19+
const visitsFile = "persistent/visits.bin"
20+
var visits atomic.Uint64
21+
22+
func indexHandler(w http.ResponseWriter, r *http.Request) {
1523
fact, err := catFact()
1624
if err == nil {
1725
w.WriteHeader(http.StatusOK)
@@ -22,6 +30,10 @@ func index(w http.ResponseWriter, r *http.Request) {
2230
}
2331
}
2432

33+
func visitsHandler(w http.ResponseWriter, r *http.Request) {
34+
_, _ = fmt.Fprintf(w, "%d", visits.Load())
35+
}
36+
2537

2638
var (
2739
reqCnt = promauto.NewCounter(prometheus.CounterOpts{
@@ -48,14 +60,42 @@ func noteTimeMiddleware(next http.Handler) http.Handler {
4860
})
4961
}
5062

63+
func countVisitsMiddleware(next http.Handler) http.Handler {
64+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
65+
visits.Add(1) // Atomic increment, no race condition here, so counter
66+
// is always correct
67+
buf := make([]byte, binary.MaxVarintLen64)
68+
binary.LittleEndian.PutUint64(buf, visits.Load())
69+
// Race condition in the order in which threads will write the file out,
70+
// so the file may not be correct, but as the same number of bytes is
71+
// always written out, the counter in the file remains a valid number.
72+
os.WriteFile(visitsFile, buf, 0644)
73+
next.ServeHTTP(w, r)
74+
})
75+
}
76+
77+
78+
func init() {
79+
_ = os.MkdirAll(filepath.Dir(visitsFile), 0755)
80+
buf, err := os.ReadFile(visitsFile)
81+
if err == nil {
82+
visits.Store(binary.LittleEndian.Uint64(buf))
83+
} else if errors.Is(err, os.ErrNotExist) {
84+
visits.Store(0)
85+
} else {
86+
panic(err)
87+
}
88+
}
89+
5190

5291
func main() {
5392
businessLogic := http.NewServeMux()
54-
businessLogic.Handle("/", asHandler(index))
93+
businessLogic.Handle("/", asHandler(indexHandler))
94+
businessLogic.Handle("/visits", asHandler(visitsHandler))
5595
// Note: keeping /metrics under middleware too for consistency with app_py
5696
businessLogic.Handle("/metrics", promhttp.Handler())
5797

58-
wrapped := noteTimeMiddleware(businessLogic)
98+
wrapped := noteTimeMiddleware(countVisitsMiddleware(businessLogic))
5999

60100
hostPort := "0.0.0.0:5000"
61101
_, _ = fmt.Println("Listening on http://" + hostPort)

app_go/main_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import (
1313
func TestFactLoads(t *testing.T) {
1414
w := httptest.NewRecorder()
1515

16-
index(w, nil)
16+
indexHandler(w, nil)
1717
resp := w.Result()
1818

1919
if resp.StatusCode != http.StatusOK {

app_python/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,3 +174,6 @@ poetry.toml
174174
pyrightconfig.json
175175

176176
# End of https://www.toptal.com/developers/gitignore/api/python
177+
178+
179+
/persistent

app_python/Dockerfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ COPY requirements.txt /app/
77
COPY moscow_time/ /app/moscow_time
88
# Note: keeping the project files owned by root so
99
# the web server has less privileges over them
10+
RUN ["mkdir", "--mode", "777", "/app/persistent"]
11+
VOLUME /app/persistent
1012
USER flask:flask
1113
RUN ["pip", "install", "--user", "-r", "requirements.txt"]
1214
CMD ["python", "-m", "moscow_time"]

app_python/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ docker run --rm -d -p 5000 kolay0ne/app_py
5454

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

57+
One may want to mount a volume or a bind-mount at `/app/persistent`, which acts
58+
as a persistent storage for the visits counter of the web app.
59+
5760
## Unit Tests
5861

5962
To run unit tests:

app_python/moscow_time/__init__.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import datetime
2+
from os import makedirs
23
from time import monotonic
34

4-
from flask import Flask, request, Response
5+
from flask import Flask, request, Response, send_from_directory
56
import requests
67
import prometheus_client
78

89
from .cache import cache_for
10+
from .visits import increment_on_call
911

1012

1113
app = Flask(__name__)
@@ -42,12 +44,32 @@ def get_time():
4244
return dt.time()
4345

4446

47+
VISITS_FILENAME = 'persistent/visits.bin'
48+
49+
# Create it on start
50+
try:
51+
basename_at = VISITS_FILENAME.rindex('/')
52+
except ValueError:
53+
pass
54+
else:
55+
makedirs(VISITS_FILENAME[:basename_at], exist_ok=True)
56+
open(VISITS_FILENAME, 'a+').close() # The `a+` mode ensures we have write perm
57+
58+
4559
@app.route('/')
60+
@increment_on_call(VISITS_FILENAME)
4661
def index():
4762
time = get_time()
4863
return f"In MSK it's {time.hour}:{time.minute}:{time.second}. " \
4964
"Have you brushed your teeth today yet?"
5065

5166
@app.route('/metrics')
67+
@increment_on_call(VISITS_FILENAME)
5268
def prometheus_metrics():
5369
return Response(prometheus_client.generate_latest(), mimetype='text/plain')
70+
71+
@app.route('/visits')
72+
@increment_on_call(VISITS_FILENAME)
73+
def visits():
74+
with open(VISITS_FILENAME, 'rb') as f:
75+
return str(int.from_bytes(f.read(), byteorder='little'))

app_python/moscow_time/visits.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import fcntl
2+
from functools import wraps
3+
from threading import Lock
4+
from typing import Callable
5+
6+
7+
def increment(filename: str) -> None:
8+
with open(filename, 'rb+') as f:
9+
try:
10+
fcntl.flock(f.fileno(), fcntl.LOCK_EX)
11+
cur = int.from_bytes(f.read(), byteorder='little')
12+
f.seek(0)
13+
cur += 1
14+
f.write(cur.to_bytes(byteorder='little', length=(cur.bit_length() // 8 + 1)))
15+
f.truncate() # Not necessary, as larger numbers take more bytes
16+
finally:
17+
# Note: will be unlocked anyway when the file is closed
18+
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
19+
20+
21+
def increment_on_call(filename: str) -> Callable[[Callable, ...], Callable]:
22+
def decorator(func: Callable) -> Callable:
23+
@wraps(func)
24+
def with_increment(*args, **kwargs):
25+
increment(filename)
26+
return func(*args, **kwargs)
27+
return with_increment
28+
return decorator

k8s/12.md

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# Lab 12
2+
3+
## ConfigMap file
4+
5+
```sh
6+
$ helm install app-py . -f secrets://./env-secrets.yaml
7+
NAME: app-py
8+
LAST DEPLOYED: Mon Apr 22 16:55:16 2024
9+
NAMESPACE: default
10+
STATUS: deployed
11+
REVISION: 1
12+
NOTES:
13+
1. Get the application URL by running these commands:
14+
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
15+
You can watch the status of by running 'kubectl get --namespace default svc -w app-py'
16+
export SERVICE_IP=$(kubectl get svc --namespace default app-py --template "{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}")
17+
echo http://$SERVICE_IP:5000
18+
$ kubectl wait deployment/app-py --for condition=available
19+
deployment.apps/app-py condition met
20+
$ kubectl get po
21+
NAME READY STATUS RESTARTS AGE
22+
app-py-7d7d86657c-df667 1/1 Running 0 30s
23+
app-py-7d7d86657c-hq8nw 1/1 Running 0 30s
24+
app-py-7d7d86657c-jxgrj 1/1 Running 0 30s
25+
app-py-7d7d86657c-twd5l 1/1 Running 0 30s
26+
$ kubectl exec app-py-7d7d86657c-twd5l -- cat /persistent/config.json
27+
{"mole": ["hamsters"], "hamster": ["moles"]}
28+
$
29+
```
30+
31+
## ConfigMap env
32+
33+
```sh
34+
$ helm install app-go .
35+
NAME: app-go
36+
LAST DEPLOYED: Mon Apr 22 19:26:23 2024
37+
NAMESPACE: default
38+
STATUS: deployed
39+
REVISION: 1
40+
NOTES:
41+
1. Get the application URL by running these commands:
42+
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
43+
You can watch the status of by running 'kubectl get --namespace default svc -w app-go-app-py'
44+
export SERVICE_IP=$(kubectl get svc --namespace default app-go-app-py --template "{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}")
45+
echo http://$SERVICE_IP:5000
46+
$ kubectl get po
47+
NAME READY STATUS RESTARTS AGE
48+
app-go-app-py-59f4c5c69d-8fljp 1/1 Running 0 22s
49+
app-go-app-py-59f4c5c69d-bv496 1/1 Running 0 22s
50+
app-go-app-py-59f4c5c69d-jlz6q 1/1 Running 0 22s
51+
app-go-app-py-59f4c5c69d-t2nnn 1/1 Running 0 22s
52+
$ kubectl debug --image=gcc -it app-go-app-py-59f4c5c69d-8fljp --target=app-py
53+
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.
54+
Defaulting debug container name to debugger-864lv.
55+
If you don't see a command prompt, try pressing enter.
56+
root@app-go-app-py-59f4c5c69d-8fljp:/# echo 'main(){setreuid(geteuid(),geteuid());execl("/bin/sh","sh",0);}' > a.c
57+
root@app-go-app-py-59f4c5c69d-8fljp:/# gcc -o a a.c
58+
a.c:1:1: warning: return type defaults to 'int' [-Wimplicit-int]
59+
1 | main(){setreuid(geteuid(),geteuid());execl("/bin/sh","sh",0);}
60+
| ^~~~
61+
a.c: In function 'main':
62+
a.c:1:8: warning: implicit declaration of function 'setreuid' [-Wimplicit-function-declaration]
63+
1 | main(){setreuid(geteuid(),geteuid());execl("/bin/sh","sh",0);}
64+
| ^~~~~~~~
65+
a.c:1:17: warning: implicit declaration of function 'geteuid' [-Wimplicit-function-declaration]
66+
1 | main(){setreuid(geteuid(),geteuid());execl("/bin/sh","sh",0);}
67+
| ^~~~~~~
68+
a.c:1:38: warning: implicit declaration of function 'execl' [-Wimplicit-function-declaration]
69+
1 | main(){setreuid(geteuid(),geteuid());execl("/bin/sh","sh",0);}
70+
| ^~~~~
71+
a.c:1:38: warning: incompatible implicit declaration of built-in function 'execl' [-Wbuiltin-declaration-mismatch]
72+
root@app-go-app-py-59f4c5c69d-8fljp:/# chown 2004:2004 a
73+
root@app-go-app-py-59f4c5c69d-8fljp:/# chmod u+s a
74+
root@app-go-app-py-59f4c5c69d-8fljp:/# exec ./a
75+
$ cat /proc/1/environ | sed 's/\x0/\n/g' | grep seal
76+
seal=2001
77+
monk_seal=century
78+
$ exit
79+
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
80+
$
81+
```

k8s/app-go/charts/label-lib-0.1.0.tgz

338 Bytes
Binary file not shown.

k8s/app-go/files/config.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
seal: "2001"
2+
monk_seal: "century"

k8s/app-go/templates/config-map.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
apiVersion: "v1"
2+
kind: ConfigMap
3+
metadata:
4+
name: {{.Release.Name}}-config-map
5+
data:
6+
{{ .Files.Get "files/config.yaml" | indent 2 }}

k8s/app-go/templates/deployment.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ spec:
5252
{{- end }}
5353
env:
5454
{{ include "app-go.environ" . | nindent 12 }}
55+
envFrom:
56+
- configMapRef:
57+
name: {{.Release.Name}}-config-map
5558
{{- with .Values.volumes }}
5659
volumes:
5760
{{- toYaml . | nindent 8 }}

k8s/app-go/values.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ image:
44
repository: kolay0ne/app_go
55
pullPolicy: IfNotPresent
66
# Overrides the image tag whose default is the chart appVersion.
7-
tag: "lab8"
7+
tag: "lab12"
88

99
serviceAccount:
1010
# Specifies whether a service account should be created

k8s/app-py/files/config.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"mole": ["hamsters"], "hamster": ["moles"]}

k8s/app-py/templates/config_map.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
apiVersion: v1
2+
kind: ConfigMap
3+
metadata:
4+
name: {{ .Release.Name }}-config-map
5+
data:
6+
config.json: {{ .Files.Get "files/config.json" | quote }}

k8s/app-py/values.yaml

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ image:
44
repository: kolay0ne/app_py
55
pullPolicy: IfNotPresent
66
# Overrides the image tag whose default is the chart appVersion.
7-
tag: "lab8"
7+
tag: "lab12"
88

99
serviceAccount:
1010
# Specifies whether a service account should be created
@@ -84,17 +84,16 @@ autoscaling:
8484
# targetMemoryUtilizationPercentage: 80
8585

8686
# Additional volumes on the output Deployment definition.
87-
volumes: []
88-
# - name: foo
89-
# secret:
90-
# secretName: mysecret
91-
# optional: false
87+
volumes:
88+
- name: config-map
89+
configMap:
90+
name: app-py-config-map
9291

9392
# Additional volumeMounts on the output Deployment definition.
94-
volumeMounts: []
95-
# - name: foo
96-
# mountPath: "/etc/foo"
97-
# readOnly: true
93+
volumeMounts:
94+
- name: config-map
95+
mountPath: "/persistent"
96+
readOnly: true
9897

9998
nodeSelector: {}
10099

0 commit comments

Comments
 (0)