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 agent none
options { options {
disableConcurrentBuilds()
timestamps() 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 { environment {
NODE_OPTIONS = '--max_old_space_size=2048' NODE_OPTIONS = '--max_old_space_size=2048'
APP_VERSION = "1.0.${BUILD_NUMBER}" APP_VERSION = "1.0.${BUILD_NUMBER}"
DOCKER_BUILDKIT = '1' 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 { stages {
@@ -18,9 +41,6 @@ pipeline {
========================= */ ========================= */
stage('Backend: test (main)') { stage('Backend: test (main)') {
when {
branch 'main'
}
agent { agent {
docker { docker {
image 'python:3.11-slim' image 'python:3.11-slim'
@@ -47,9 +67,6 @@ pipeline {
========================= */ ========================= */
stage('Docker: build images') { stage('Docker: build images') {
when {
branch 'main'
}
agent any agent any
steps { steps {
@@ -72,13 +89,12 @@ pipeline {
--build-arg GIT_COMMIT=${COMMIT_SHORT} \ --build-arg GIT_COMMIT=${COMMIT_SHORT} \
--build-arg COMMIT_AUTHOR="${COMMIT_AUTHOR}" \ --build-arg COMMIT_AUTHOR="${COMMIT_AUTHOR}" \
--build-arg BUILD_NUMBER=${BUILD_NUMBER} \ --build-arg BUILD_NUMBER=${BUILD_NUMBER} \
-t cafeteria-backend:${BUILD_NUMBER} \ -t cafeteria-backend:${APP_VERSION} \
-t cafeteria-backend:latest \
./backend ./backend
docker build \ docker build \
-t cafeteria-frontend:${BUILD_NUMBER} \ --build-arg VITE_API_BASE=${VITE_API_BASE} \
-t cafeteria-frontend:latest \ -t cafeteria-frontend:${APP_VERSION} \
./frontend ./frontend
''' '''
} }
@@ -89,14 +105,7 @@ pipeline {
========================= */ ========================= */
stage('Deploy (docker compose)') { stage('Deploy (docker compose)') {
when {
branch 'main'
}
agent any agent any
environment {
JENKINS_BASE_URL = 'http://jenkins:8080'
JENKINS_JOB_NAME = 'Espetos'
}
steps { steps {
withCredentials([ withCredentials([
usernamePassword( usernamePassword(
@@ -108,17 +117,16 @@ pipeline {
sh ''' sh '''
set -e set -e
echo "Deploying backend ${BUILD_NUMBER}" echo "Deploying ${APP_VERSION}"
echo "BACKEND_TAG=${BUILD_NUMBER}" > .env BACKEND_TAG=${APP_VERSION} FRONTEND_TAG=${APP_VERSION} docker compose up -d
echo "FRONTEND_TAG=${BUILD_NUMBER}" >> .env
docker-compose up -d
''' '''
} }
} }
} }
stage('Cleanup') { stage('Cleanup') {
agent any agent any
steps { steps {

View File

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

View File

@@ -7,6 +7,16 @@
- Python 3.11+ y `pip` - Python 3.11+ y `pip`
- Node 18+ y `npm` - 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) ## Backend (FastAPI)
```bash ```bash
cd backend cd backend
@@ -42,6 +52,7 @@ cd frontend
npm install npm install
npm run dev -- --host --port 5173 npm run dev -- --host --port 5173
``` ```
Abrir `http://localhost:5173/taller/`.
Tests, lint/check: Tests, lint/check:
```bash ```bash
cd frontend 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 WORKDIR /build
RUN apt-get update && apt-get install -y --no-install-recommends \ 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 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 WORKDIR /app
# ---- Build args (desde Jenkins) ---- # ---- 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 import time
from fastapi import FastAPI import requests
from fastapi import FastAPI, status
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from app.services.builds import build_history from app.services.builds import build_history
from app.services.menu import build_menu from app.services.menu import build_menu
@@ -76,4 +78,10 @@ def price_for_item(item: str):
@app.get("/builds") @app.get("/builds")
def builds(): def builds():
try:
return build_history() 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/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
import base64 import base64
import json
from pathlib import Path
from typing import Dict, List from typing import Dict, List
import requests import requests
from app.settings import settings 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]: def _sort_builds(builds: List[Dict]) -> List[Dict]:
return sorted(builds, key=lambda build: build.get("number", 0), reverse=True) return sorted(builds, key=lambda build: build.get("number", 0), reverse=True)
def normalize_build(build: Dict) -> Dict: def normalize_build(build: Dict) -> Dict:
changes = build.get("changeSets", []) changes = build.get("changeSets", [])
commits = [] commits = []
@@ -58,11 +49,16 @@ def normalize_build(build: Dict) -> Dict:
def _auth_header() -> Dict[str, str]: 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}" token = f"{settings.jenkins_user}:{settings.jenkins_token}"
encoded = base64.b64encode(token.encode()).decode() encoded = base64.b64encode(token.encode()).decode()
return {"Authorization": f"Basic {encoded}"} return {"Authorization": f"Basic {encoded}"}
def fetch_builds(limit: int = 5) -> List[Dict]: def fetch_builds(limit: int = 5) -> List[Dict]:
if not settings.jenkins_job_name:
raise ValueError("JENKINS_JOB_NAME not configured")
url = ( url = (
f"{settings.jenkins_base_url}/job/{settings.jenkins_job_name}/api/json" f"{settings.jenkins_base_url}/job/{settings.jenkins_job_name}/api/json"
"?tree=builds[number,url,result,timestamp,duration," "?tree=builds[number,url,result,timestamp,duration,"
@@ -78,6 +74,4 @@ def fetch_builds(limit: int = 5) -> List[Dict]:
def build_history() -> Dict: def build_history() -> Dict:
"""Return Jenkins build history data.""" """Return Jenkins build history data."""
builds = fetch_builds() builds = fetch_builds()
return { return {"builds": [normalize_build(b) for b in builds]}
"builds": [normalize_build(b) for b in builds]
}

View File

@@ -24,7 +24,10 @@ class RuntimeConfig:
git_commit: str = os.getenv("GIT_COMMIT", "local") git_commit: str = os.getenv("GIT_COMMIT", "local")
build_number: str = os.getenv("BUILD_NUMBER", "-") build_number: str = os.getenv("BUILD_NUMBER", "-")
commit_author: str = os.getenv("COMMIT_AUTHOR", "local") 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_job_name: str = os.getenv("JENKINS_JOB_NAME", "")
jenkins_user: str = os.getenv("JENKINS_USER", "") jenkins_user: str = os.getenv("JENKINS_USER", "")
jenkins_token: str = os.getenv("JENKINS_TOKEN", "") 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 # You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
import requests
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from app.main import app from app.main import app
@@ -106,3 +107,16 @@ def test_build_history(monkeypatch):
assert second["status"] == "running" assert second["status"] == "running"
assert second["duration_seconds"] == 1 assert second["duration_seconds"] == 1
assert second["commits"] == [] 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: backend:
build: build:
context: ./backend context: ./backend
image: cafeteria-backend:${BACKEND_TAG} image: cafeteria-backend:${BACKEND_TAG:-latest}
networks:
- cafeteria
ports: ports:
- "8000:8000" - "8000:8000"
environment: environment:
JENKINS_BASE_URL: ${JENKINS_BASE_URL} JENKINS_BASE_URL: ${JENKINS_BASE_URL:-http://jenkins:8080}
JENKINS_JOB_NAME: ${JENKINS_JOB_NAME} JENKINS_JOB_NAME: ${JENKINS_JOB_NAME:-TallerCiCd}
JENKINS_USER: ${JENKINS_USER} JENKINS_USER: ${JENKINS_USER:-}
JENKINS_TOKEN: ${JENKINS_TOKEN} JENKINS_TOKEN: ${JENKINS_TOKEN:-}
restart: unless-stopped restart: unless-stopped
healthcheck: healthcheck:
test: test:
@@ -26,10 +28,21 @@ services:
frontend: frontend:
build: build:
context: ./frontend 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: ports:
- "80:80" - "8081:8081"
depends_on: depends_on:
backend: backend:
condition: service_healthy condition: service_healthy
restart: unless-stopped 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 WORKDIR /app
COPY package*.json ./ COPY package*.json ./
ARG VITE_API_BASE
ENV VITE_API_BASE=$VITE_API_BASE
RUN npm install --no-progress RUN npm install --no-progress
COPY . . COPY . .
RUN npm run build 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 nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist /usr/share/nginx/html COPY --from=build /app/dist /usr/share/nginx/html/taller
EXPOSE 80 EXPOSE 8081
CMD ["nginx", "-g", "daemon off;"] CMD ["nginx", "-g", "daemon off;"]

View File

@@ -1,11 +1,24 @@
server { server {
listen 80; listen 8081;
server_name _; server_name _;
root /usr/share/nginx/html; root /usr/share/nginx/html;
index index.html; index index.html;
location / { location = /taller {
try_files $uri /index.html; 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; if (!ENABLE_POLLING) return;
const pricesInterval = setInterval(fetchPrices, 8000); const pricesInterval = setInterval(fetchPrices, 180000);
const ciInterval = setInterval(fetchCiStatus, 10000); const ciInterval = setInterval(fetchCiStatus, 300000);
return () => { return () => {
clearInterval(pricesInterval); clearInterval(pricesInterval);
@@ -467,7 +467,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<div class="openbokeron-logo"> <div class="openbokeron-logo">
<div class="logo-bubble"> <div class="logo-bubble">
<img <img
src="/open-bokeron-logo.png" src={`${import.meta.env.BASE_URL}/open-bokeron-logo.png`}
alt="Logo de Open Bokeron" alt="Logo de Open Bokeron"
loading="lazy" 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/>. 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'; export const ENABLE_POLLING = import.meta.env.MODE !== 'test';

View File

@@ -1,7 +1,15 @@
import { defineConfig } from 'vite'; import { defineConfig, loadEnv } from 'vite';
import { svelte } from '@sveltejs/vite-plugin-svelte'; import { svelte } from '@sveltejs/vite-plugin-svelte';
export default defineConfig({ 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()], plugins: [svelte()],
test: { test: {
environment: 'jsdom', environment: 'jsdom',
@@ -9,5 +17,18 @@ export default defineConfig({
}, },
server: { server: {
port: 5173, 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 USER root
RUN apt-get update \ RUN apt-get update \
&& apt-get install -y docker.io curl \ && apt-get install -y ca-certificates curl gnupg \
&& curl -L https://github.com/docker/compose/releases/download/v2.27.0/docker-compose-linux-x86_64 \ && install -m 0755 -d /etc/apt/keyrings \
-o /usr/local/bin/docker-compose \ && curl -fsSL https://download.docker.com/linux/debian/gpg \
&& chmod +x /usr/local/bin/docker-compose \ | 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/* && rm -rf /var/lib/apt/lists/*
USER jenkins USER jenkins