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