first
Build and Publish Docker Image / Build and Push Docker Image (push) Failing after 34s Details

This commit is contained in:
Isaac Johnson 2026-09-19 15:49:53 -05:00
commit 2d1f6a21b7
15 changed files with 949 additions and 0 deletions

5
.dockerignore Normal file
View File

@ -0,0 +1,5 @@
__pycache__
*.pyc
*.pyo
*.pyd
.git

View File

@ -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 }}

191
.gitignore vendored Normal file
View File

@ -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

16
Dockerfile Normal file
View File

@ -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"]

390
app.py Normal file
View File

@ -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
cicd.yaml Normal file
View File

5
helm-chart/.helmignore Normal file
View File

@ -0,0 +1,5 @@
.DS_Store
*.tgz
.git/
.gitignore
.dockerignore

6
helm-chart/Chart.yaml Normal file
View File

@ -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"

View File

@ -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 }}

View File

@ -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 }}

View File

@ -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 }}

View File

@ -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 }}

63
helm-chart/values.yaml Normal file
View File

@ -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: {}

2
requirements.txt Normal file
View File

@ -0,0 +1,2 @@
Flask>=3.0.0
requests>=2.31.0

2
version.ini Normal file
View File

@ -0,0 +1,2 @@
[metadata]
version = 0.1