Compare commits

...

20 Commits

Author SHA1 Message Date
512c1cea7b Merge pull request 'bugfix/main-16-PreventException' (#18) from bugfix/main-16-PreventException into main
Reviewed-on: https://openbokeron.uma.es/gitea/OpenBokeron/TallerCiCd/pulls/18
2025-12-28 11:35:40 +01:00
5845fed88f Ruff fix 2025-12-28 11:34:30 +01:00
938fd8170c Return error status 2025-12-28 11:34:01 +01:00
1f0c10b458 Merge pull request 'bugfix/main-13-UpdateConfigurations' (#17) from bugfix/main-13-UpdateConfigurations into main
Reviewed-on: https://openbokeron.uma.es/gitea/OpenBokeron/TallerCiCd/pulls/17
2025-12-28 11:15:36 +01:00
b15630c7ea Delete trigger 2025-12-28 11:05:24 +01:00
bc044a10c9 Change base images 2025-12-28 11:01:55 +01:00
558a3198f4 Revert image rotation 2025-12-24 18:23:35 +01:00
8901941e9f Delete conditions 2025-12-24 17:52:14 +01:00
c6780b53fc Use docker compose instead of docker-compose 2025-12-24 14:00:06 +01:00
d49e756c21 Update intervals 2025-12-24 13:39:10 +01:00
12817a7e82 Rotate images 2025-12-24 13:37:00 +01:00
a9baf6da95 Parametrize configurations¡ 2025-12-24 12:44:34 +01:00
a2f21e1286 Add vite api base 2025-12-23 18:53:36 +01:00
7de0f434c3 docker network 2025-12-23 18:40:11 +01:00
c74cc19d1b Add context path 2025-12-23 18:30:53 +01:00
1191ef9f1f Update context path 2025-12-23 18:02:41 +01:00
26128feb7e Update listening port 2025-12-23 17:46:32 +01:00
48dd10ea05 Prevent creation of pycache directory 2025-12-23 17:34:06 +01:00
93fdce1d6e Change jenkins url 2025-12-23 17:33:48 +01:00
b5ace86a03 Change port 2025-12-23 17:33:30 +01:00
17 changed files with 180 additions and 116 deletions

7
.env.example Normal file
View File

@@ -0,0 +1,7 @@
BACKEND_TAG=latest
FRONTEND_TAG=latest
VITE_API_BASE=/taller/api
JENKINS_BASE_URL=http://host.docker.internal:8080
JENKINS_JOB_NAME=TallerCiCd
JENKINS_USER=
JENKINS_TOKEN=

View File

@@ -2,13 +2,36 @@ pipeline {
agent none
options {
disableConcurrentBuilds()
timestamps()
}
parameters {
string(
name: 'JENKINS_BASE_URL',
defaultValue: 'https://openbokeron.org/jenkins',
description: 'Base URL del Jenkins objetivo'
)
string(
name: 'JENKINS_JOB_NAME',
defaultValue: 'CD',
description: 'Nombre del job que se consulta en Jenkins'
)
string(
name: 'VITE_API_BASE',
defaultValue: '/taller/api',
description: 'Base path/API para el frontend (build time)'
)
}
environment {
NODE_OPTIONS = '--max_old_space_size=2048'
APP_VERSION = "1.0.${BUILD_NUMBER}"
DOCKER_BUILDKIT = '1'
JENKINS_BASE_URL = "${params.JENKINS_BASE_URL}"
JENKINS_JOB_NAME = "${params.JENKINS_JOB_NAME}"
VITE_API_BASE = "${params.VITE_API_BASE}"
PYTHONDONTWRITEBYTECODE = 1
}
stages {
@@ -18,9 +41,6 @@ pipeline {
========================= */
stage('Backend: test (main)') {
when {
branch 'main'
}
agent {
docker {
image 'python:3.11-slim'
@@ -47,9 +67,6 @@ pipeline {
========================= */
stage('Docker: build images') {
when {
branch 'main'
}
agent any
steps {
@@ -72,13 +89,12 @@ pipeline {
--build-arg GIT_COMMIT=${COMMIT_SHORT} \
--build-arg COMMIT_AUTHOR="${COMMIT_AUTHOR}" \
--build-arg BUILD_NUMBER=${BUILD_NUMBER} \
-t cafeteria-backend:${BUILD_NUMBER} \
-t cafeteria-backend:latest \
-t cafeteria-backend:${APP_VERSION} \
./backend
docker build \
-t cafeteria-frontend:${BUILD_NUMBER} \
-t cafeteria-frontend:latest \
--build-arg VITE_API_BASE=${VITE_API_BASE} \
-t cafeteria-frontend:${APP_VERSION} \
./frontend
'''
}
@@ -89,14 +105,7 @@ pipeline {
========================= */
stage('Deploy (docker compose)') {
when {
branch 'main'
}
agent any
environment {
JENKINS_BASE_URL = 'http://jenkins:8080'
JENKINS_JOB_NAME = 'Espetos'
}
steps {
withCredentials([
usernamePassword(
@@ -108,17 +117,16 @@ pipeline {
sh '''
set -e
echo "Deploying backend ${BUILD_NUMBER}"
echo "Deploying ${APP_VERSION}"
echo "BACKEND_TAG=${BUILD_NUMBER}" > .env
echo "FRONTEND_TAG=${BUILD_NUMBER}" >> .env
docker-compose up -d
BACKEND_TAG=${APP_VERSION} FRONTEND_TAG=${APP_VERSION} docker compose up -d
'''
}
}
}
stage('Cleanup') {
agent any
steps {

View File

@@ -41,6 +41,11 @@ pipeline {
args '-u root'
}
}
environment {
PYTHONDONTWRITEBYTECODE = 1
}
steps {
dir('backend') {
sh '''

View File

@@ -7,6 +7,16 @@
- Python 3.11+ y `pip`
- Node 18+ y `npm`
## Configuracion de entorno (local/prod)
Variables que usa `docker-compose` y el frontend:
```bash
cp .env.example .env
```
Notas:
- `VITE_API_BASE` por defecto apunta a `/taller/api` y el frontend proxya a la API.
- Para Jenkins local en contenedor: `JENKINS_BASE_URL=http://jenkins:8080`. Se hace necesario que back y jenkins estén en la misma red de Docker si se quiere probar en local.
- Para VPS: `JENKINS_BASE_URL=https://openbokeron.org/jenkins`.
## Backend (FastAPI)
```bash
cd backend
@@ -42,6 +52,7 @@ cd frontend
npm install
npm run dev -- --host --port 5173
```
Abrir `http://localhost:5173/taller/`.
Tests, lint/check:
```bash
cd frontend

View File

@@ -1,4 +1,4 @@
FROM python:3.11-slim AS builder
FROM docker.io/library/python:3.11-slim AS builder
WORKDIR /build
RUN apt-get update && apt-get install -y --no-install-recommends \
@@ -9,7 +9,7 @@ COPY requirements.txt .
RUN pip wheel --no-cache-dir --no-deps -r requirements.txt -w /build/wheels
FROM python:3.11-slim
FROM docker.io/library/python:3.11-slim
WORKDIR /app
# ---- Build args (desde Jenkins) ----

View File

@@ -1,44 +0,0 @@
{
"builds": [
{
"number": 205,
"status": "success",
"branch": "main",
"commit": "9ac3f91",
"author": "Miau",
"finished_at": "2024-05-04T10:20:00Z",
"duration_seconds": 312
},
{
"number": 204,
"status": "failed",
"branch": "feature/nosetioestoesunmock",
"commit": "75c4ba2",
"author": "Miau",
"finished_at": "2024-05-04T09:50:00Z",
"duration_seconds": 188,
"failed_stage": "tests",
"fun_message": "woops"
},
{
"number": 203,
"status": "failed",
"branch": "main",
"commit": "512ca7e",
"author": "Miau",
"finished_at": "2024-05-04T09:10:00Z",
"duration_seconds": 140,
"failed_stage": "lint",
"fun_message": "Nadie pasa en local el linter"
},
{
"number": 202,
"status": "success",
"branch": "hotfix/tehedichoqueestoesunmock?",
"commit": "c73d8ab",
"author": "Miau",
"finished_at": "2024-05-03T18:30:00Z",
"duration_seconds": 276
}
]
}

View File

@@ -16,8 +16,10 @@
import time
from fastapi import FastAPI
import requests
from fastapi import FastAPI, status
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from app.services.builds import build_history
from app.services.menu import build_menu
@@ -76,4 +78,10 @@ def price_for_item(item: str):
@app.get("/builds")
def builds():
return build_history()
try:
return build_history()
except (requests.RequestException, ValueError):
return JSONResponse(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
content={"builds": [], "error": "jenkins_unavailable"},
)

View File

@@ -15,26 +15,17 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import base64
import json
from pathlib import Path
from typing import Dict, List
import requests
from app.settings import settings
DATA_DIR = Path(__file__).resolve().parent.parent / "data"
def _load_json(filename: str) -> Dict:
path = DATA_DIR / filename
with open(path, encoding="utf-8") as file:
return json.load(file)
def _sort_builds(builds: List[Dict]) -> List[Dict]:
return sorted(builds, key=lambda build: build.get("number", 0), reverse=True)
def normalize_build(build: Dict) -> Dict:
changes = build.get("changeSets", [])
commits = []
@@ -58,11 +49,16 @@ def normalize_build(build: Dict) -> Dict:
def _auth_header() -> Dict[str, str]:
if not settings.jenkins_user or not settings.jenkins_token:
return {}
token = f"{settings.jenkins_user}:{settings.jenkins_token}"
encoded = base64.b64encode(token.encode()).decode()
return {"Authorization": f"Basic {encoded}"}
def fetch_builds(limit: int = 5) -> List[Dict]:
if not settings.jenkins_job_name:
raise ValueError("JENKINS_JOB_NAME not configured")
url = (
f"{settings.jenkins_base_url}/job/{settings.jenkins_job_name}/api/json"
"?tree=builds[number,url,result,timestamp,duration,"
@@ -78,6 +74,4 @@ def fetch_builds(limit: int = 5) -> List[Dict]:
def build_history() -> Dict:
"""Return Jenkins build history data."""
builds = fetch_builds()
return {
"builds": [normalize_build(b) for b in builds]
}
return {"builds": [normalize_build(b) for b in builds]}

View File

@@ -24,7 +24,10 @@ class RuntimeConfig:
git_commit: str = os.getenv("GIT_COMMIT", "local")
build_number: str = os.getenv("BUILD_NUMBER", "-")
commit_author: str = os.getenv("COMMIT_AUTHOR", "local")
jenkins_base_url: str = os.getenv("JENKINS_BASE_URL", "localhost:8080")
jenkins_base_url: str = os.getenv(
"JENKINS_BASE_URL",
"http://localhost:8080"
).rstrip("/")
jenkins_job_name: str = os.getenv("JENKINS_JOB_NAME", "")
jenkins_user: str = os.getenv("JENKINS_USER", "")
jenkins_token: str = os.getenv("JENKINS_TOKEN", "")

View File

@@ -14,6 +14,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import requests
from fastapi.testclient import TestClient
from app.main import app
@@ -106,3 +107,16 @@ def test_build_history(monkeypatch):
assert second["status"] == "running"
assert second["duration_seconds"] == 1
assert second["commits"] == []
def test_build_history_error_returns_empty(monkeypatch):
def raise_error(limit=5):
raise requests.RequestException("boom")
monkeypatch.setattr("app.services.builds.fetch_builds", raise_error)
response = client.get("/builds")
assert response.status_code == 503
body = response.json()
assert body["builds"] == []
assert body["error"] == "jenkins_unavailable"

View File

@@ -2,14 +2,16 @@ services:
backend:
build:
context: ./backend
image: cafeteria-backend:${BACKEND_TAG}
image: cafeteria-backend:${BACKEND_TAG:-latest}
networks:
- cafeteria
ports:
- "8000:8000"
environment:
JENKINS_BASE_URL: ${JENKINS_BASE_URL}
JENKINS_JOB_NAME: ${JENKINS_JOB_NAME}
JENKINS_USER: ${JENKINS_USER}
JENKINS_TOKEN: ${JENKINS_TOKEN}
JENKINS_BASE_URL: ${JENKINS_BASE_URL:-http://jenkins:8080}
JENKINS_JOB_NAME: ${JENKINS_JOB_NAME:-TallerCiCd}
JENKINS_USER: ${JENKINS_USER:-}
JENKINS_TOKEN: ${JENKINS_TOKEN:-}
restart: unless-stopped
healthcheck:
test:
@@ -26,10 +28,21 @@ services:
frontend:
build:
context: ./frontend
image: cafeteria-frontend:${FRONTEND_TAG}
args:
VITE_API_BASE: ${VITE_API_BASE:-/taller/api}
image: cafeteria-frontend:${FRONTEND_TAG:-latest}
networks:
- cafeteria
ports:
- "80:80"
- "8081:8081"
depends_on:
backend:
condition: service_healthy
restart: unless-stopped
networks:
cafeteria:
enable_ipv6: true
ipam:
config:
- subnet: 2001:db8::/64

View File

@@ -1,12 +1,14 @@
FROM node:20-slim AS build
FROM docker.io/library/node:20-slim AS build
WORKDIR /app
COPY package*.json ./
ARG VITE_API_BASE
ENV VITE_API_BASE=$VITE_API_BASE
RUN npm install --no-progress
COPY . .
RUN npm run build
FROM nginx:1.27-alpine AS final
FROM docker.io/library/nginx:1.27-alpine AS final
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
COPY --from=build /app/dist /usr/share/nginx/html/taller
EXPOSE 8081
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -1,11 +1,24 @@
server {
listen 80;
listen 8081;
server_name _;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri /index.html;
location = /taller {
return 301 /taller/;
}
location /taller/api/ {
proxy_pass http://backend:8000/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /taller/ {
try_files $uri $uri/ /taller/index.html;
}
}

View File

@@ -96,8 +96,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
if (!ENABLE_POLLING) return;
const pricesInterval = setInterval(fetchPrices, 8000);
const ciInterval = setInterval(fetchCiStatus, 10000);
const pricesInterval = setInterval(fetchPrices, 180000);
const ciInterval = setInterval(fetchCiStatus, 300000);
return () => {
clearInterval(pricesInterval);
@@ -467,7 +467,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<div class="openbokeron-logo">
<div class="logo-bubble">
<img
src="/open-bokeron-logo.png"
src={`${import.meta.env.BASE_URL}/open-bokeron-logo.png`}
alt="Logo de Open Bokeron"
loading="lazy"
/>

View File

@@ -16,5 +16,9 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export const API_BASE = import.meta.env.VITE_API_BASE || 'http://localhost:8000';
const baseUrl = import.meta.env.BASE_URL || '/';
const defaultApiBase = `${baseUrl.replace(/\/$/, '')}/api`;
const rawApiBase = import.meta.env.VITE_API_BASE || defaultApiBase;
export const API_BASE = rawApiBase.replace(/\/$/, '');
export const ENABLE_POLLING = import.meta.env.MODE !== 'test';

View File

@@ -1,13 +1,34 @@
import { defineConfig } from 'vite';
import { defineConfig, loadEnv } from 'vite';
import { svelte } from '@sveltejs/vite-plugin-svelte';
export default defineConfig({
plugins: [svelte()],
test: {
environment: 'jsdom',
globals: true,
},
server: {
port: 5173,
},
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), 'VITE_');
const basePath = '/taller/';
const defaultApiBase = `${basePath.replace(/\/$/, '')}/api`;
const apiBase = (env.VITE_API_BASE || defaultApiBase).replace(/\/$/, '');
const apiProxyPath = apiBase.startsWith('http') ? null : apiBase;
return {
base: basePath,
plugins: [svelte()],
test: {
environment: 'jsdom',
globals: true,
},
server: {
port: 5173,
proxy: apiProxyPath
? {
[apiProxyPath]: {
target: 'http://localhost:8000',
changeOrigin: true,
rewrite: (path) =>
path.startsWith(apiProxyPath)
? path.slice(apiProxyPath.length) || '/'
: path,
},
}
: undefined,
},
};
});

View File

@@ -3,10 +3,15 @@ FROM jenkins/jenkins:lts
USER root
RUN apt-get update \
&& apt-get install -y docker.io curl \
&& curl -L https://github.com/docker/compose/releases/download/v2.27.0/docker-compose-linux-x86_64 \
-o /usr/local/bin/docker-compose \
&& chmod +x /usr/local/bin/docker-compose \
&& apt-get install -y ca-certificates curl gnupg \
&& install -m 0755 -d /etc/apt/keyrings \
&& curl -fsSL https://download.docker.com/linux/debian/gpg \
| gpg --dearmor -o /etc/apt/keyrings/docker.gpg \
&& chmod a+r /etc/apt/keyrings/docker.gpg \
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian $(. /etc/os-release && echo \"$VERSION_CODENAME\") stable" \
> /etc/apt/sources.list.d/docker.list \
&& apt-get update \
&& apt-get install -y docker-ce-cli docker-compose-plugin \
&& rm -rf /var/lib/apt/lists/*
USER jenkins