博客应用:Flask + Dockerfile + K8s 清单 + Jenkinsfile
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
data/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
FROM python:3.12-slim
|
||||||
|
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
|
PYTHONUNBUFFERED=1 \
|
||||||
|
DATA_DIR=/data \
|
||||||
|
PORT=5000
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 用清华源加速,构建时不需要翻墙
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt \
|
||||||
|
-i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||||
|
|
||||||
|
COPY app.py .
|
||||||
|
|
||||||
|
RUN useradd -r -u 10001 blog && mkdir -p /data && chown -R blog /data /app
|
||||||
|
USER blog
|
||||||
|
|
||||||
|
EXPOSE 5000
|
||||||
|
CMD ["python", "app.py"]
|
||||||
Vendored
+116
@@ -0,0 +1,116 @@
|
|||||||
|
pipeline {
|
||||||
|
agent {
|
||||||
|
kubernetes {
|
||||||
|
yaml '''
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Pod
|
||||||
|
spec:
|
||||||
|
serviceAccountName: jenkins
|
||||||
|
containers:
|
||||||
|
- name: kaniko
|
||||||
|
image: gcr.io/kaniko-project/executor:v1.23.2-debug
|
||||||
|
command: ["sleep"]
|
||||||
|
args: ["999999"]
|
||||||
|
volumeMounts:
|
||||||
|
- name: docker-config
|
||||||
|
mountPath: /kaniko/.docker
|
||||||
|
- name: kubectl
|
||||||
|
image: bitnami/kubectl:1.34
|
||||||
|
command: ["sleep"]
|
||||||
|
args: ["999999"]
|
||||||
|
volumes:
|
||||||
|
- name: docker-config
|
||||||
|
secret:
|
||||||
|
secretName: harbor-dockerconfig
|
||||||
|
items:
|
||||||
|
- key: .dockerconfigjson
|
||||||
|
path: config.json
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
options {
|
||||||
|
timestamps()
|
||||||
|
buildDiscarder(logRotator(numToKeepStr: '10'))
|
||||||
|
timeout(time: 20, unit: 'MINUTES')
|
||||||
|
}
|
||||||
|
|
||||||
|
environment {
|
||||||
|
REGISTRY = 'harbor.nineleaves.cn'
|
||||||
|
IMAGE = 'harbor.nineleaves.cn/apps/blog'
|
||||||
|
NS = 'apps'
|
||||||
|
}
|
||||||
|
|
||||||
|
stages {
|
||||||
|
stage('准备') {
|
||||||
|
steps {
|
||||||
|
echo "构建号 #${env.BUILD_NUMBER},分支 ${env.BRANCH_NAME ?: 'main'}"
|
||||||
|
sh 'ls -la'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('构建镜像并推送 Harbor') {
|
||||||
|
steps {
|
||||||
|
container('kaniko') {
|
||||||
|
sh '''
|
||||||
|
set -e
|
||||||
|
TAG="build-${BUILD_NUMBER}"
|
||||||
|
echo "开始构建 ${IMAGE}:${TAG}"
|
||||||
|
/kaniko/executor \
|
||||||
|
--context=dir://${WORKSPACE} \
|
||||||
|
--dockerfile=${WORKSPACE}/Dockerfile \
|
||||||
|
--destination=${IMAGE}:${TAG} \
|
||||||
|
--destination=${IMAGE}:latest \
|
||||||
|
--skip-tls-verify \
|
||||||
|
--skip-tls-verify-pull \
|
||||||
|
--cache=false \
|
||||||
|
--verbosity=info
|
||||||
|
echo "镜像已推送: ${IMAGE}:${TAG}"
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('部署到 Kubernetes') {
|
||||||
|
steps {
|
||||||
|
container('kubectl') {
|
||||||
|
sh '''
|
||||||
|
set -e
|
||||||
|
TAG="build-${BUILD_NUMBER}"
|
||||||
|
kubectl apply -f ${WORKSPACE}/k8s/deployment.yaml
|
||||||
|
kubectl -n ${NS} set image deployment/blog blog=${IMAGE}:${TAG}
|
||||||
|
kubectl -n ${NS} rollout status deployment/blog --timeout=180s
|
||||||
|
kubectl -n ${NS} get pods -l app=blog
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('冒烟测试') {
|
||||||
|
steps {
|
||||||
|
container('kubectl') {
|
||||||
|
sh '''
|
||||||
|
set -e
|
||||||
|
for i in $(seq 1 20); do
|
||||||
|
CODE=$(kubectl -n apps run smoke-$$ --rm -i --restart=Never \
|
||||||
|
--image=curlimages/curl:8.10.1 --quiet -- \
|
||||||
|
-s -o /dev/null -w "%{http_code}" http://blog/healthz 2>/dev/null || echo 000)
|
||||||
|
if [ "$CODE" = "200" ]; then echo "健康检查通过"; exit 0; fi
|
||||||
|
echo "第 $i 次: $CODE, 等待中..."; sleep 3
|
||||||
|
done
|
||||||
|
echo "健康检查失败"; exit 1
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
success {
|
||||||
|
echo "✅ 部署成功:https://blog.nineleaves.cn"
|
||||||
|
}
|
||||||
|
failure {
|
||||||
|
echo "❌ 流水线失败,查看上面的日志"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# 我的博客
|
||||||
|
|
||||||
|
一个用 Flask 写的极简博客,用来演示完整的 CI/CD 链路:
|
||||||
|
|
||||||
|
```
|
||||||
|
Gitea(代码) → Jenkins(流水线) → Kaniko(构建镜像) → Harbor(镜像仓库) → Kubernetes(部署)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 功能
|
||||||
|
|
||||||
|
- 首页文章列表
|
||||||
|
- Markdown 文章详情(代码块、表格、引用)
|
||||||
|
- 写文章(`/new`,需要管理口令)
|
||||||
|
- 阅读计数
|
||||||
|
- 文章持久化在 Longhorn 卷上
|
||||||
|
|
||||||
|
## 本地跑
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
DATA_DIR=./data ADMIN_TOKEN=test123 python app.py
|
||||||
|
# 打开 http://localhost:5000
|
||||||
|
```
|
||||||
|
|
||||||
|
## 构建镜像
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t harbor.nineleaves.cn/apps/blog:dev .
|
||||||
|
docker push harbor.nineleaves.cn/apps/blog:dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## 部署
|
||||||
|
|
||||||
|
Jenkins 流水线会自动 apply `k8s/deployment.yaml` 并把镜像替换成本次的构建号。
|
||||||
|
|
||||||
|
手动执行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl apply -f k8s/deployment.yaml
|
||||||
|
kubectl -n apps set image deployment/blog blog=harbor.nineleaves.cn/apps/blog:dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## 访问
|
||||||
|
|
||||||
|
https://blog.nineleaves.cn
|
||||||
@@ -0,0 +1,213 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""极简博客 —— 单文件 Flask 应用,文章以 JSON 存在 PVC 上。"""
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import html
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
import markdown
|
||||||
|
from flask import Flask, request, redirect, abort, render_template_string
|
||||||
|
|
||||||
|
DATA_DIR = os.environ.get("DATA_DIR", "/data")
|
||||||
|
DB_FILE = os.path.join(DATA_DIR, "posts.json")
|
||||||
|
ADMIN_TOKEN = os.environ.get("ADMIN_TOKEN", "change-me")
|
||||||
|
SITE_TITLE = os.environ.get("SITE_TITLE", "我的博客")
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def load_posts():
|
||||||
|
if not os.path.exists(DB_FILE):
|
||||||
|
return []
|
||||||
|
try:
|
||||||
|
with open(DB_FILE, encoding="utf-8") as f:
|
||||||
|
posts = json.load(f)
|
||||||
|
return sorted(posts, key=lambda p: p.get("created", ""), reverse=True)
|
||||||
|
except (json.JSONDecodeError, OSError):
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def save_posts(posts):
|
||||||
|
os.makedirs(DATA_DIR, exist_ok=True)
|
||||||
|
tmp = DB_FILE + ".tmp"
|
||||||
|
with open(tmp, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(posts, f, ensure_ascii=False, indent=2)
|
||||||
|
os.replace(tmp, DB_FILE)
|
||||||
|
|
||||||
|
|
||||||
|
def slugify(text):
|
||||||
|
slug = re.sub(r"[^\w\u4e00-\u9fff-]+", "-", text.strip().lower()).strip("-")
|
||||||
|
return slug[:60] or "post"
|
||||||
|
|
||||||
|
|
||||||
|
BASE_CSS = """
|
||||||
|
<style>
|
||||||
|
:root{--fg:#1a1a1a;--muted:#666;--line:#e5e5e5;--accent:#0b6bcb}
|
||||||
|
*{box-sizing:border-box}
|
||||||
|
body{margin:0;font:16px/1.75 -apple-system,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;
|
||||||
|
color:var(--fg);background:#fafafa}
|
||||||
|
.wrap{max-width:760px;margin:0 auto;padding:48px 24px 96px}
|
||||||
|
header.site{margin-bottom:48px;display:flex;justify-content:space-between;align-items:baseline;
|
||||||
|
border-bottom:1px solid var(--line);padding-bottom:16px}
|
||||||
|
header.site h1{font-size:1.6rem;margin:0;letter-spacing:-.02em}
|
||||||
|
header.site a{color:var(--muted);text-decoration:none;font-size:.9rem}
|
||||||
|
header.site a:hover{color:var(--accent)}
|
||||||
|
article.post{background:#fff;border:1px solid var(--line);border-radius:12px;
|
||||||
|
padding:24px 28px;margin-bottom:20px}
|
||||||
|
article.post h2{margin:0 0 8px;font-size:1.25rem;line-height:1.4}
|
||||||
|
article.post h2 a{color:var(--fg);text-decoration:none}
|
||||||
|
article.post h2 a:hover{color:var(--accent)}
|
||||||
|
.meta{color:var(--muted);font-size:.85rem}
|
||||||
|
.excerpt{color:#444;margin:10px 0 0}
|
||||||
|
.content{background:#fff;border:1px solid var(--line);border-radius:12px;padding:32px 36px}
|
||||||
|
.content h1{margin-top:0}
|
||||||
|
.content pre{background:#f5f5f5;padding:16px;border-radius:8px;overflow:auto}
|
||||||
|
.content code{background:#f5f5f5;padding:2px 6px;border-radius:4px;font-size:.9em}
|
||||||
|
.content pre code{background:none;padding:0}
|
||||||
|
.content img{max-width:100%;height:auto;border-radius:8px}
|
||||||
|
.content blockquote{border-left:3px solid var(--line);margin:0;padding-left:16px;color:var(--muted)}
|
||||||
|
form.new{background:#fff;border:1px solid var(--line);border-radius:12px;padding:24px}
|
||||||
|
form.new label{display:block;font-size:.85rem;color:var(--muted);margin:14px 0 6px}
|
||||||
|
form.new input,form.new textarea{width:100%;padding:10px 12px;border:1px solid var(--line);
|
||||||
|
border-radius:8px;font:inherit}
|
||||||
|
form.new textarea{min-height:320px;font-family:ui-monospace,Menlo,monospace;font-size:.92rem}
|
||||||
|
button{background:var(--accent);color:#fff;border:0;border-radius:8px;padding:10px 20px;
|
||||||
|
font:inherit;cursor:pointer;margin-top:18px}
|
||||||
|
button:hover{opacity:.9}
|
||||||
|
footer{margin-top:64px;color:var(--muted);font-size:.85rem;text-align:center}
|
||||||
|
</style>
|
||||||
|
"""
|
||||||
|
|
||||||
|
LIST_TPL = """
|
||||||
|
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>{{ site }}</title>""" + BASE_CSS + """</head><body><div class="wrap">
|
||||||
|
<header class="site"><h1>{{ site }}</h1><a href="/new">写文章 →</a></header>
|
||||||
|
{% for p in posts %}
|
||||||
|
<article class="post">
|
||||||
|
<h2><a href="/post/{{ p.slug }}">{{ p.title }}</a></h2>
|
||||||
|
<div class="meta">{{ p.created }} · {{ p.views }} 次阅读</div>
|
||||||
|
<p class="excerpt">{{ p.excerpt }}</p>
|
||||||
|
</article>
|
||||||
|
{% else %}
|
||||||
|
<article class="post"><p class="meta">还没有文章。点右上角"写文章"开始。</p></article>
|
||||||
|
{% endfor %}
|
||||||
|
<footer>由 Flask + Kubernetes + Jenkins + Harbor 驱动</footer>
|
||||||
|
</div></body></html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
POST_TPL = """
|
||||||
|
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>{{ p.title }} · {{ site }}</title>""" + BASE_CSS + """</head><body><div class="wrap">
|
||||||
|
<header class="site"><h1><a href="/" style="color:inherit">{{ site }}</a></h1>
|
||||||
|
<a href="/">← 返回列表</a></header>
|
||||||
|
<div class="content">
|
||||||
|
<h1>{{ p.title }}</h1>
|
||||||
|
<div class="meta" style="margin-bottom:24px">{{ p.created }}</div>
|
||||||
|
{{ body|safe }}
|
||||||
|
</div>
|
||||||
|
<footer>阅读 {{ p.views }} 次</footer>
|
||||||
|
</div></body></html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
NEW_TPL = """
|
||||||
|
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>写文章 · {{ site }}</title>""" + BASE_CSS + """</head><body><div class="wrap">
|
||||||
|
<header class="site"><h1><a href="/" style="color:inherit">{{ site }}</a></h1>
|
||||||
|
<a href="/">← 返回列表</a></header>
|
||||||
|
{% if error %}<div class="post" style="border-color:#e0b4b4;background:#fff5f5">
|
||||||
|
<p class="meta" style="color:#b00">{{ error }}</p></div>{% endif %}
|
||||||
|
<form class="new" method="post" action="/new">
|
||||||
|
<label>标题</label><input name="title" required maxlength="120" value="{{ title or '' }}">
|
||||||
|
<label>正文(支持 Markdown)</label><textarea name="body" required>{{ body or '' }}</textarea>
|
||||||
|
<label>管理口令</label><input name="token" type="password" required>
|
||||||
|
<button type="submit">发布</button>
|
||||||
|
</form>
|
||||||
|
</div></body></html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/healthz")
|
||||||
|
def healthz():
|
||||||
|
return "ok", 200
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
def index():
|
||||||
|
posts = []
|
||||||
|
for p in load_posts():
|
||||||
|
posts.append({
|
||||||
|
"slug": p["slug"],
|
||||||
|
"title": p["title"],
|
||||||
|
"created": p["created"],
|
||||||
|
"views": p.get("views", 0),
|
||||||
|
"excerpt": p.get("excerpt", ""),
|
||||||
|
})
|
||||||
|
return render_template_string(LIST_TPL, site=SITE_TITLE, posts=posts)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/post/<slug>")
|
||||||
|
def post_detail(slug):
|
||||||
|
posts = load_posts()
|
||||||
|
for p in posts:
|
||||||
|
if p["slug"] == slug:
|
||||||
|
p["views"] = p.get("views", 0) + 1
|
||||||
|
save_posts(posts)
|
||||||
|
body = markdown.markdown(p["body"], extensions=["fenced_code", "tables"])
|
||||||
|
return render_template_string(POST_TPL, site=SITE_TITLE, p=p, body=body)
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/new", methods=["GET", "POST"])
|
||||||
|
def new_post():
|
||||||
|
if request.method == "GET":
|
||||||
|
return render_template_string(NEW_TPL, site=SITE_TITLE, error=None, title="", body="")
|
||||||
|
|
||||||
|
title = (request.form.get("title") or "").strip()
|
||||||
|
body = (request.form.get("body") or "").strip()
|
||||||
|
token = request.form.get("token") or ""
|
||||||
|
|
||||||
|
if token != ADMIN_TOKEN:
|
||||||
|
return render_template_string(NEW_TPL, site=SITE_TITLE,
|
||||||
|
error="管理口令不正确", title=title, body=body), 403
|
||||||
|
if not title or not body:
|
||||||
|
return render_template_string(NEW_TPL, site=SITE_TITLE,
|
||||||
|
error="标题和正文都不能为空", title=title, body=body), 400
|
||||||
|
|
||||||
|
posts = load_posts()
|
||||||
|
base = slugify(title)
|
||||||
|
slug = base
|
||||||
|
n = 1
|
||||||
|
existing = {p["slug"] for p in posts}
|
||||||
|
while slug in existing:
|
||||||
|
n += 1
|
||||||
|
slug = f"{base}-{n}"
|
||||||
|
|
||||||
|
plain = re.sub(r"[#>*_`\[\]()!-]", "", body)
|
||||||
|
posts.append({
|
||||||
|
"slug": slug,
|
||||||
|
"title": title,
|
||||||
|
"body": body,
|
||||||
|
"excerpt": html.escape(plain[:120].replace("\n", " ")),
|
||||||
|
"created": datetime.datetime.now().strftime("%Y-%m-%d %H:%M"),
|
||||||
|
"views": 0,
|
||||||
|
})
|
||||||
|
save_posts(posts)
|
||||||
|
return redirect(f"/post/{slug}")
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/post/<slug>/delete")
|
||||||
|
def delete_post(slug):
|
||||||
|
token = request.form.get("token") or ""
|
||||||
|
if token != ADMIN_TOKEN:
|
||||||
|
abort(403)
|
||||||
|
posts = [p for p in load_posts() if p["slug"] != slug]
|
||||||
|
save_posts(posts)
|
||||||
|
return redirect("/")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run(host="0.0.0.0", port=int(os.environ.get("PORT", "5000")))
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: blog
|
||||||
|
namespace: apps
|
||||||
|
labels:
|
||||||
|
app: blog
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
revisionHistoryLimit: 3
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: blog
|
||||||
|
strategy:
|
||||||
|
type: RollingUpdate
|
||||||
|
rollingUpdate:
|
||||||
|
maxSurge: 1
|
||||||
|
maxUnavailable: 0
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: blog
|
||||||
|
spec:
|
||||||
|
securityContext:
|
||||||
|
runAsNonRoot: true
|
||||||
|
runAsUser: 10001
|
||||||
|
fsGroup: 10001
|
||||||
|
containers:
|
||||||
|
- name: blog
|
||||||
|
image: harbor.nineleaves.cn/apps/blog:BUILD_TAG
|
||||||
|
imagePullPolicy: Always
|
||||||
|
ports:
|
||||||
|
- containerPort: 5000
|
||||||
|
env:
|
||||||
|
- name: SITE_TITLE
|
||||||
|
value: "我的博客"
|
||||||
|
- name: DATA_DIR
|
||||||
|
value: /data
|
||||||
|
- name: ADMIN_TOKEN
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: blog-secret
|
||||||
|
key: admin-token
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 20m
|
||||||
|
memory: 64Mi
|
||||||
|
limits:
|
||||||
|
memory: 256Mi
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /healthz
|
||||||
|
port: 5000
|
||||||
|
initialDelaySeconds: 3
|
||||||
|
periodSeconds: 5
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /healthz
|
||||||
|
port: 5000
|
||||||
|
initialDelaySeconds: 15
|
||||||
|
periodSeconds: 20
|
||||||
|
volumeMounts:
|
||||||
|
- name: data
|
||||||
|
mountPath: /data
|
||||||
|
volumes:
|
||||||
|
- name: data
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: blog-data
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: blog-data
|
||||||
|
namespace: apps
|
||||||
|
spec:
|
||||||
|
accessModes: ["ReadWriteOnce"]
|
||||||
|
storageClassName: longhorn
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 1Gi
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: blog
|
||||||
|
namespace: apps
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: blog
|
||||||
|
ports:
|
||||||
|
- port: 80
|
||||||
|
targetPort: 5000
|
||||||
|
---
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: blog
|
||||||
|
namespace: apps
|
||||||
|
annotations:
|
||||||
|
nginx.ingress.kubernetes.io/proxy-body-size: "10m"
|
||||||
|
spec:
|
||||||
|
ingressClassName: nginx
|
||||||
|
rules:
|
||||||
|
- host: blog.nineleaves.cn
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: blog
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
flask==3.1.0
|
||||||
|
markdown==3.7
|
||||||
Reference in New Issue
Block a user