commit 126041847e7fd58de70a73b46d835d3526a8e71c Author: lhahn Date: Sat Aug 19 23:37:46 2023 +0200 Git initial commit diff --git a/Dockerfile b/Dockerfile new file mode 100755 index 0000000..e0570d4 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM python + +WORKDIR /app + +COPY . . + +RUN pip install -r requirements.txt + +EXPOSE 8000 + +ENTRYPOINT python app.py \ No newline at end of file diff --git a/README.md b/README.md new file mode 100755 index 0000000..0fb67e9 --- /dev/null +++ b/README.md @@ -0,0 +1,54 @@ +# K8sPy + +A simple test how to get a py flask app run on kubernetes. + + +--- + + +## 1. For Docker + +### Instructions + +1. docker build -t flaskapp . + +2. docker images -a + +3. docker run -d -p 80:8000 flaskapp + +-> Open Browser: http://localhost + +Endpoints: + - /start-thread/ + - /start-thread + - /stop-thread/ + - /stop-thread + - /content/ + - /content + + +--- + + +## 2. For Kubernetes +Here this is just testing with minikube!!!! +(Adjust the imagePullPolicy to Never when using Minikube) + +This will create a cluster with 5 nodes and 10 replica pods; +On average(!) 2 pods per node, but does not have to be! + +0. $ minikube start --nodes=5 + +1. $ minikube nodes list + +2. $ minikube image load flaskapp + +3. $ minikube kubectl -- apply -f deployment.yaml -f service.yaml + +4. $ minikube kubectl -- get pods + +5. $ minikube kubectl -- get pods -o json + +6. $ minikube service list -> open URL in Browser and have fun! :) + +7. $ minikube delete \ No newline at end of file diff --git a/app.py b/app.py new file mode 100755 index 0000000..5a6ddef --- /dev/null +++ b/app.py @@ -0,0 +1,146 @@ +from flask import Flask, request, abort +from tthread import RandomThread +from threading import Lock +import os + + +app = Flask(__name__) + +thread_pool = {} +thread_id_max = 0 +sleep_default = 0.5 + +mutex = Lock() + +POD_NAME_VAR = os.environ.get("POD_NAME_VAR") +NODE_NAME_VAR = os.environ.get("NODE_NAME_VAR") +info = f" on Pod '{POD_NAME_VAR}' on Node '{NODE_NAME_VAR}'" if POD_NAME_VAR is not None and NODE_NAME_VAR is not None else "" + +@app.route("/") +def root_main(): + welcome = f"Heyho{info} 😜" + thread_status = [ + f"{thread_id}: active? {thread.is_active()}" + for thread_id, thread in thread_pool.items() + ] + response = ( + '
'.join(status for status in thread_status) + if thread_status + else + "< no active thread>" + ) + return ( + f"

Thread Status

{welcome}

Current thread status:
" + f"{response}" + ) + + + + + +@app.route("/start-thread/") +def start_thread_id(threadid): + threadid = str(threadid) + if threadid in thread_pool and thread_pool[threadid].is_active(): + abort(409, "Thread already started.") + sleep_time = int(request.args.get('sleep-time') or sleep_default) + _start_thread(threadid, sleep_time) + return f"Starting thread with id '{threadid}'{info}." + +@app.route("/start-thread") +def start_thread(): + global thread_id_max + sleep_time = request.args.get('sleep-time') or sleep_default + threadid = request.args.get('thread-id') + if threadid is None: + mutex.acquire() + threadid = str(thread_id_max) + thread_pool[threadid] = None + thread_id_max += 1 + mutex.release() + if threadid in thread_pool and thread_pool[threadid] is not None and thread_pool[threadid].is_active(): + abort(409, "Thread already started.") + _start_thread(threadid, sleep_time) + return f"Starting thread with ID '{threadid}'{info}." + + + + + + +@app.route("/stop-thread/") +def stop_thread_id(threadid): + if threadid not in thread_pool: + abort(404, "Thread ID is unknown.") + elif thread_pool[threadid] is None or not thread_pool[threadid].is_active(): + abort(404, "Thread is not active.") + _stop_thread(threadid) + return f"Stopping thread with id '{threadid}'{info}." + +@app.route("/stop-thread") +def stop_threads(): + active_threads = [ + thread + for thread in thread_pool.values() + if thread is not None and thread.is_active() + ] + if not active_threads: + abort(404,"No active thread!") + for thread in active_threads: + _stop_thread(thread.id()) + id_list = [f"Thread '{thread.id()}'" for thread in active_threads] + response = f"Stopping threads{info}:
{'
'.join(id_list)}" + return response + + + + + +@app.route("/content/") +def get_content_id(threadid): + if threadid not in thread_pool: + abort(404, "Thread ID is unknown.") + elif thread_pool[threadid] is None or not thread_pool[threadid].is_active(): + abort(404, "Thread is not active.") + return f"

Thread '{threadid}'{info}


Content: '{thread_pool[threadid].get_content()}'" + +@app.route("/content") +def get_content(): + active_threads = [ + thread + for thread in thread_pool.values() + if thread is not None and thread.is_active() + ] + if not active_threads: + abort(404,"No active thread!") + content = [ + f"Thread '{thread.id()}': {thread.get_content()}" + for thread in active_threads + ] + response = ( + "

Thread Content{info}


" + f"{'
'.join(content)}" + ) + return response + + + + + +def _start_thread(threadid, sleep_time): + if not threadid in thread_pool or thread_pool[threadid] is None: + thread_pool[threadid] = RandomThread(threadid, sleep_time) + thread_pool[threadid].start() + +def _stop_thread(threadid, join=False): + if not join: + thread_pool[threadid].stop() + else: + thread_pool[threadid].stopjoin() + + + + +if __name__ == "__main__": + #DO NOT RUN IN PROD!!!!!!!!!!!!!! + app.run(host="0.0.0.0", port=8000, debug=True) \ No newline at end of file diff --git a/deployment.yaml b/deployment.yaml new file mode 100755 index 0000000..fd167b8 --- /dev/null +++ b/deployment.yaml @@ -0,0 +1,30 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: flask-app + labels: + app: flask-app +spec: + replicas: 10 + selector: + matchLabels: + app: flask-app + template: + metadata: + labels: + app: flask-app + name: flask-app + spec: + containers: + - name: flask-app + image: flaskapp + imagePullPolicy: Never + env: + - name: NODE_NAME_VAR + valueFrom: + fieldRef: + fieldPath: spec.nodeName + - name: POD_NAME_VAR + valueFrom: + fieldRef: + fieldPath: metadata.name \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100755 index 0000000..8ab6294 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +flask \ No newline at end of file diff --git a/service.yaml b/service.yaml new file mode 100755 index 0000000..f835896 --- /dev/null +++ b/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + app: flask-app + name: flask-app +spec: + type: NodePort + ports: + - port: 80 + protocol: TCP + targetPort: 8000 + nodePort: 30080 + selector: + app: flask-app diff --git a/tthread.py b/tthread.py new file mode 100755 index 0000000..f52b48d --- /dev/null +++ b/tthread.py @@ -0,0 +1,53 @@ +from random import randint +from threading import Thread, Event +from time import sleep + +class RandomThread: + def executor(event, dynamic_content, sleep_time): + while not event.is_set(): + dynamic_content["random"] = randint(0,10000) + sleep(sleep_time) + + def __init__(self, my_id, sleep_time=0.5): + self._my_id = my_id + self._event = Event() + self._content = {"random":None} + self._sleep_time = sleep_time + + def start(self): + self._event.clear() + self._thread = Thread( + target=RandomThread.executor, + args=( + self._event, + self._content, + self._sleep_time + ) + ) + self._thread.start() + + def stop(self): + self._event.set() + + def stopjoin(self): + self.stop() + self._thread.join() + + def id(self): + return self._my_id + + + def get_content(self): + return self._content['random'] + + def is_active(self): + return not self._event.is_set() + +if __name__ == "__main__": + sleep_time = 0.25 + tthread = RandomThread(0x32a, sleep_time*2) + tthread.start() + for i in range(20): + print(f"{i+1}: tthread has content {tthread.get_content()}") + sleep(sleep_time) + tthread.stopjoin() \ No newline at end of file