first
Build and Publish Docker Image / Build and Push Docker Image (push) Failing after 34s
Details
Build and Publish Docker Image / Build and Push Docker Image (push) Failing after 34s
Details
This commit is contained in:
commit
2d1f6a21b7
|
|
@ -0,0 +1,5 @@
|
|||
__pycache__
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
.git
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
name: Build and Publish Docker Image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
name: Build and Push Docker Image
|
||||
runs-on: my_custom_label
|
||||
container: node:22
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Prepare Env for Docker
|
||||
run: |
|
||||
whoami
|
||||
which docker || true
|
||||
apt update
|
||||
cat /etc/os-release
|
||||
apt install -y ca-certificates curl gnupg
|
||||
mkdir -p /etc/apt/keyrings
|
||||
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
|
||||
echo \
|
||||
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
|
||||
focal stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||
apt update
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
||||
|
||||
- name: Get Version from version.ini
|
||||
id: get_version
|
||||
run: |
|
||||
# Extract version value from version.ini
|
||||
VERSION=$(awk -F'=[ \t]*' '/^version[ \t]*=/ {gsub(/[ \t"'\''\r]/, "", $2); print $2}' version.ini)
|
||||
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build Dockerfile
|
||||
run: |
|
||||
export BUILDIMGTAG="`cat Dockerfile | tail -n1 | sed 's/^.*\///g'`"
|
||||
docker build -t $BUILDIMGTAG:${{ env.VERSION }} .
|
||||
docker images
|
||||
|
||||
- name: Tag and Push (Harbor)
|
||||
run: |
|
||||
export BUILDIMGTAG="`cat Dockerfile | tail -n1 | sed 's/^.*\///g'`"
|
||||
export FINALBUILDTAG="`cat Dockerfile | tail -n1 | sed 's/^#//g'`"
|
||||
docker tag $BUILDIMGTAG:${{ env.VERSION }} $FINALBUILDTAG:${{ env.VERSION }}
|
||||
docker images
|
||||
echo $CR_PAT | docker login harbor.freshbrewed.science -u $CR_USER --password-stdin
|
||||
docker push $FINALBUILDTAG:${{ env.VERSION }}
|
||||
env: # Or as an environment variable
|
||||
CR_PAT: ${{ secrets.CR_PAT }}
|
||||
CR_USER: ${{ secrets.CR_USER }}
|
||||
|
||||
- name: Tag and Push (Dockerhub)
|
||||
run: |
|
||||
export BUILDIMGTAG="`cat Dockerfile | tail -n1 | sed 's/^.*\///g'`"
|
||||
docker tag $BUILDIMGTAG:${{ env.VERSION }} $DHUSER/$BUILDIMGTAG:${{ env.VERSION }}
|
||||
docker images
|
||||
echo $DHPAT | docker login -u $DHUSER --password-stdin
|
||||
docker push $DHUSER/$BUILDIMGTAG:${{ env.VERSION }}
|
||||
env: # Or as an environment variable
|
||||
DHPAT: ${{ secrets.DHPAT }}
|
||||
DHUSER: ${{ secrets.DHUSER }}
|
||||
|
||||
- name: Prepare Env for Helm OCI Push (Helm v3 reference only)
|
||||
run: |
|
||||
if [ ! -f /tmp/linux-amd64/helm ]; then
|
||||
apt update
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y unzip curl || true
|
||||
wget https://get.helm.sh/helm-v3.14.0-linux-amd64.tar.gz -O /tmp/helm.tar.gz || true
|
||||
cd /tmp
|
||||
tar xzvf helm.tar.gz
|
||||
chmod +x linux-amd64/helm
|
||||
fi
|
||||
|
||||
- name: Package Helm Chart
|
||||
run: |
|
||||
set -x
|
||||
/tmp/linux-amd64/helm package ./helm-chart
|
||||
export HLMPKG=`ls -tr *.tgz | tail -n1 | tr -d '\n'`
|
||||
/tmp/linux-amd64/helm registry login harbor.freshbrewed.science -u $CR_USER -p $CR_PAT
|
||||
/tmp/linux-amd64/helm push ./$HLMPKG oci://harbor.freshbrewed.science/chartrepo/botwebwars
|
||||
env: # Or as an environment variable
|
||||
CR_PAT: ${{ secrets.CR_PAT }}
|
||||
CR_USER: ${{ secrets.CR_USER }}
|
||||
|
|
@ -0,0 +1,191 @@
|
|||
# Created by https://www.toptal.com/developers/gitignore/api/python,linux
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=python,linux
|
||||
|
||||
### Linux ###
|
||||
*~
|
||||
|
||||
# temporary files which can be created if a process still has a handle open of a deleted file
|
||||
.fuse_hidden*
|
||||
|
||||
# KDE directory preferences
|
||||
.directory
|
||||
|
||||
# Linux trash folder which might appear on any partition or disk
|
||||
.Trash-*
|
||||
|
||||
# .nfs files are created when an open file is removed but is still being accessed
|
||||
.nfs*
|
||||
|
||||
### Python ###
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/#use-with-ide
|
||||
.pdm.toml
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
### Python Patch ###
|
||||
# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
|
||||
poetry.toml
|
||||
|
||||
# ruff
|
||||
.ruff_cache/
|
||||
|
||||
# LSP config files
|
||||
pyrightconfig.json
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/python,linux
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
FROM cgr.dev/chainguard/python:latest-dev AS builder
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir --user -r requirements.txt
|
||||
|
||||
FROM cgr.dev/chainguard/python:latest
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /home/nonroot/.local /home/nonroot/.local
|
||||
COPY . .
|
||||
|
||||
ENV PATH="/home/nonroot/.local/bin:$PATH"
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
CMD ["app.py"]
|
||||
|
|
@ -0,0 +1,390 @@
|
|||
import os
|
||||
import configparser
|
||||
import requests
|
||||
from flask import Flask, render_template_string, jsonify
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
OLLAMA_HOST = os.environ.get("OLLAMA_HOST", "http://harbor.freshbrewed.science:11444")
|
||||
OLLAMA_MODEL = os.environ.get("OLLAMA_MODEL", "gemma4:e4b")
|
||||
PROMPT = os.environ.get("PROMPT", "tell me a dad joke about programming")
|
||||
PORT = int(os.environ.get("PORT", "80"))
|
||||
|
||||
def get_app_version():
|
||||
config = configparser.ConfigParser()
|
||||
version_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), "version.ini")
|
||||
if os.path.exists(version_file):
|
||||
try:
|
||||
config.read(version_file)
|
||||
return config.get("metadata", "version", fallback="unknown")
|
||||
except Exception:
|
||||
return "unknown"
|
||||
return "unknown"
|
||||
|
||||
HTML_TEMPLATE = """<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Programming Dad Jokes</title>
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
/* Vibrant animated gradient with bright red, yellow, and blue */
|
||||
background: linear-gradient(-45deg, #ff0844 0%, #ff4b2b 20%, #ffdd00 45%, #00d2ff 75%, #0052d4 100%);
|
||||
background-size: 400% 400%;
|
||||
animation: gradientShift 14s ease infinite;
|
||||
position: relative;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* Ambient glowing gradient orbs for depth */
|
||||
body::before,
|
||||
body::after {
|
||||
content: "";
|
||||
position: fixed;
|
||||
border-radius: 50%;
|
||||
filter: blur(80px);
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
opacity: 0.65;
|
||||
animation: floatOrbs 10s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
body::before {
|
||||
width: 380px;
|
||||
height: 380px;
|
||||
background: radial-gradient(circle, #ff0055 0%, #ffaa00 100%);
|
||||
top: -80px;
|
||||
left: -80px;
|
||||
}
|
||||
|
||||
body::after {
|
||||
width: 420px;
|
||||
height: 420px;
|
||||
background: radial-gradient(circle, #00d2ff 0%, #0044ff 100%);
|
||||
bottom: -100px;
|
||||
right: -100px;
|
||||
animation-delay: -5s;
|
||||
}
|
||||
|
||||
@keyframes gradientShift {
|
||||
0% { background-position: 0% 50%; }
|
||||
50% { background-position: 100% 50%; }
|
||||
100% { background-position: 0% 50%; }
|
||||
}
|
||||
|
||||
@keyframes floatOrbs {
|
||||
0% { transform: translate(0, 0) scale(1); }
|
||||
100% { transform: translate(40px, 40px) scale(1.1); }
|
||||
}
|
||||
|
||||
.container {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
max-width: 680px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border-radius: 28px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.85);
|
||||
box-shadow: 0 25px 60px rgba(0, 0, 0, 0.22),
|
||||
0 0 35px rgba(255, 255, 255, 0.35);
|
||||
padding: 44px 38px;
|
||||
text-align: center;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 16px;
|
||||
border-radius: 999px;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: #1e293b;
|
||||
background: linear-gradient(90deg, rgba(255, 75, 43, 0.15), rgba(255, 221, 0, 0.25), rgba(0, 210, 255, 0.2));
|
||||
border: 1px solid rgba(255, 75, 43, 0.3);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.badge-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #10b981;
|
||||
box-shadow: 0 0 8px #10b981;
|
||||
}
|
||||
|
||||
.badge-divider {
|
||||
opacity: 0.4;
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.2rem;
|
||||
font-weight: 800;
|
||||
color: #0f172a;
|
||||
letter-spacing: -0.02em;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 1rem;
|
||||
color: #64748b;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.joke-box {
|
||||
background: #f8fafc;
|
||||
border-radius: 18px;
|
||||
padding: 26px 24px;
|
||||
margin-bottom: 30px;
|
||||
position: relative;
|
||||
border: 1px solid #e2e8f0;
|
||||
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.02);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.joke-box::before {
|
||||
content: '"';
|
||||
position: absolute;
|
||||
top: -12px;
|
||||
left: 18px;
|
||||
font-size: 3.5rem;
|
||||
line-height: 1;
|
||||
font-family: Georgia, serif;
|
||||
color: #ff0844;
|
||||
opacity: 0.3;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.joke-text {
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.7;
|
||||
color: #1e293b;
|
||||
font-weight: 500;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
min-height: 80px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 14px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
appearance: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
padding: 14px 28px;
|
||||
border-radius: 14px;
|
||||
color: #ffffff;
|
||||
background: linear-gradient(135deg, #ff0844 0%, #ff8c00 40%, #0099ff 100%);
|
||||
background-size: 200% 200%;
|
||||
box-shadow: 0 10px 25px rgba(255, 8, 68, 0.35);
|
||||
transition: all 0.25s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.btn:hover:not(:disabled) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 14px 30px rgba(0, 153, 255, 0.45);
|
||||
background-position: 100% 100%;
|
||||
}
|
||||
|
||||
.btn:active:not(:disabled) {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.65;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
display: none;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.35);
|
||||
border-top-color: #ffffff;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.meta {
|
||||
font-size: 0.82rem;
|
||||
color: #94a3b8;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
border-top: 1px solid #f1f5f9;
|
||||
padding-top: 18px;
|
||||
}
|
||||
|
||||
.meta span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.meta code {
|
||||
background: #f1f5f9;
|
||||
padding: 2px 6px;
|
||||
border-radius: 6px;
|
||||
font-size: 0.78rem;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.card {
|
||||
padding: 30px 20px;
|
||||
}
|
||||
h1 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
.joke-text {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<main class="card">
|
||||
<div class="badge">
|
||||
<span class="badge-dot"></span>
|
||||
<span>Ollama • Gemma 4</span>
|
||||
<span class="badge-divider">•</span>
|
||||
<span>v{{ version }}</span>
|
||||
</div>
|
||||
<h1>Programming Dad Joke</h1>
|
||||
<p class="subtitle">Freshly compiled humor from our AI model</p>
|
||||
|
||||
<div class="joke-box">
|
||||
<div id="joke" class="joke-text">{{ joke }}</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button id="refreshBtn" class="btn" onclick="fetchNewJoke()">
|
||||
<span class="spinner" id="spinner"></span>
|
||||
<span id="btnText">Tell Me Another Joke! 🚀</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="meta">
|
||||
<span>Host: <code>{{ host }}</code></span>
|
||||
<span>Model: <code>{{ model }}</code></span>
|
||||
<span>Prompt: <code>"{{ prompt }}"</code></span>
|
||||
<span>App Version: <code>v{{ version }}</code></span>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function fetchNewJoke() {
|
||||
const btn = document.getElementById("refreshBtn");
|
||||
const btnText = document.getElementById("btnText");
|
||||
const spinner = document.getElementById("spinner");
|
||||
const jokeEl = document.getElementById("joke");
|
||||
|
||||
btn.disabled = true;
|
||||
spinner.style.display = "inline-block";
|
||||
btnText.textContent = "Compiling humor...";
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/joke");
|
||||
if (!response.ok) {
|
||||
throw new Error("HTTP error " + response.status);
|
||||
}
|
||||
const data = await response.json();
|
||||
jokeEl.textContent = data.joke || "No joke returned.";
|
||||
} catch (err) {
|
||||
console.error("Failed to load joke:", err);
|
||||
jokeEl.textContent = "Oops! Could not fetch a new joke right now. Please try again.";
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
spinner.style.display = "none";
|
||||
btnText.textContent = "Tell Me Another Joke! 🚀";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
def request_joke():
|
||||
url = f"{OLLAMA_HOST.rstrip('/')}/api/generate"
|
||||
payload = {
|
||||
"model": OLLAMA_MODEL,
|
||||
"prompt": PROMPT,
|
||||
"stream": False
|
||||
}
|
||||
try:
|
||||
resp = requests.post(url, json=payload, timeout=60)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
return data.get("response", "No response received from Ollama.")
|
||||
except Exception as e:
|
||||
return f"Error contacting Ollama ({OLLAMA_HOST}): {e}"
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
joke = request_joke()
|
||||
version = get_app_version()
|
||||
return render_template_string(
|
||||
HTML_TEMPLATE,
|
||||
joke=joke,
|
||||
host=OLLAMA_HOST,
|
||||
model=OLLAMA_MODEL,
|
||||
prompt=PROMPT,
|
||||
version=version
|
||||
)
|
||||
|
||||
@app.route("/api/joke")
|
||||
def api_joke():
|
||||
joke = request_joke()
|
||||
version = get_app_version()
|
||||
return jsonify({
|
||||
"joke": joke,
|
||||
"version": version
|
||||
})
|
||||
|
||||
if __name__ == "__main__":
|
||||
version = get_app_version()
|
||||
print(f"Starting server v{version} on port {PORT} with model '{OLLAMA_MODEL}' at {OLLAMA_HOST}...")
|
||||
app.run(host="0.0.0.0", port=PORT)
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
.DS_Store
|
||||
*.tgz
|
||||
.git/
|
||||
.gitignore
|
||||
.dockerignore
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
apiVersion: v2
|
||||
name: dadjoke-app
|
||||
description: A basic Helm chart for the Programming Dad Joke Flask application
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "0.1"
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "dadjoke-app.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified app name.
|
||||
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||
If release name contains chart name it will be used as a full name.
|
||||
*/}}
|
||||
{{- define "dadjoke-app.fullname" -}}
|
||||
{{- if .Values.fullnameOverride }}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride }}
|
||||
{{- if contains $name .Release.Name }}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create chart name and version as used by the chart label.
|
||||
*/}}
|
||||
{{- define "dadjoke-app.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Common labels
|
||||
*/}}
|
||||
{{- define "dadjoke-app.labels" -}}
|
||||
helm.sh/chart: {{ include "dadjoke-app.chart" . }}
|
||||
{{ include "dadjoke-app.selectorLabels" . }}
|
||||
{{- if .Chart.AppVersion }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
{{- end }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Selector labels
|
||||
*/}}
|
||||
{{- define "dadjoke-app.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "dadjoke-app.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end }}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "dadjoke-app.fullname" . }}
|
||||
labels:
|
||||
{{- include "dadjoke-app.labels" . | nindent 4 }}
|
||||
spec:
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "dadjoke-app.selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
{{- with .Values.podAnnotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "dadjoke-app.selectorLabels" . | nindent 8 }}
|
||||
spec:
|
||||
{{- with .Values.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.securityContext | nindent 12 }}
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: {{ .Values.service.port }}
|
||||
protocol: TCP
|
||||
env:
|
||||
- name: OLLAMA_HOST
|
||||
value: {{ .Values.env.OLLAMA_HOST | quote }}
|
||||
- name: OLLAMA_MODEL
|
||||
value: {{ .Values.env.OLLAMA_MODEL | quote }}
|
||||
- name: PROMPT
|
||||
value: {{ .Values.env.PROMPT | quote }}
|
||||
- name: PORT
|
||||
value: {{ .Values.env.PORT | quote }}
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 30
|
||||
timeoutSeconds: 5
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 15
|
||||
timeoutSeconds: 5
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
{{- if .Values.ingress.enabled -}}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ include "dadjoke-app.fullname" . }}
|
||||
labels:
|
||||
{{- include "dadjoke-app.labels" . | nindent 4 }}
|
||||
{{- with .Values.ingress.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- if .Values.ingress.className }}
|
||||
ingressClassName: {{ .Values.ingress.className }}
|
||||
{{- end }}
|
||||
{{- if .Values.ingress.tls }}
|
||||
tls:
|
||||
{{- range .Values.ingress.tls }}
|
||||
- hosts:
|
||||
{{- range .hosts }}
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
secretName: {{ .secretName }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
rules:
|
||||
{{- range .Values.ingress.hosts }}
|
||||
- host: {{ .host | quote }}
|
||||
http:
|
||||
paths:
|
||||
{{- range .paths }}
|
||||
- path: {{ .path }}
|
||||
pathType: {{ .pathType }}
|
||||
backend:
|
||||
service:
|
||||
name: {{ include "dadjoke-app.fullname" $ }}
|
||||
port:
|
||||
number: {{ $.Values.service.port }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "dadjoke-app.fullname" . }}
|
||||
labels:
|
||||
{{- include "dadjoke-app.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.service.port }}
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
{{- include "dadjoke-app.selectorLabels" . | nindent 4 }}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
replicaCount: 1
|
||||
|
||||
image:
|
||||
repository: dadjoke-app
|
||||
pullPolicy: IfNotPresent
|
||||
tag: ""
|
||||
|
||||
imagePullSecrets: []
|
||||
nameOverride: ""
|
||||
fullnameOverride: ""
|
||||
|
||||
serviceAccount:
|
||||
create: false
|
||||
name: ""
|
||||
|
||||
podAnnotations: {}
|
||||
|
||||
podSecurityContext: {}
|
||||
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 65532
|
||||
runAsGroup: 65532
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 80
|
||||
|
||||
env:
|
||||
OLLAMA_HOST: "http://harbor.freshbrewed.science:11444"
|
||||
OLLAMA_MODEL: "gemma4:e4b"
|
||||
PROMPT: "tell me a dad joke about programming"
|
||||
PORT: "80"
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
className: "nginx"
|
||||
annotations:
|
||||
kubernetes.io/ingress.class: nginx
|
||||
nginx.ingress.kubernetes.io/ssl-redirect: "false"
|
||||
nginx.ingress.kubernetes.io/proxy-connect-timeout: "60"
|
||||
nginx.ingress.kubernetes.io/proxy-read-timeout: "60"
|
||||
nginx.ingress.kubernetes.io/proxy-send-timeout: "60"
|
||||
hosts:
|
||||
- host: dadjoke.local
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
tls: []
|
||||
|
||||
resources:
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 256Mi
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 128Mi
|
||||
|
||||
nodeSelector: {}
|
||||
|
||||
tolerations: []
|
||||
|
||||
affinity: {}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
Flask>=3.0.0
|
||||
requests>=2.31.0
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
[metadata]
|
||||
version = 0.1
|
||||
Loading…
Reference in New Issue