Compare commits
20 Commits
19d13711d2
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 512c1cea7b | |||
| 5845fed88f | |||
| 938fd8170c | |||
| 1f0c10b458 | |||
| b15630c7ea | |||
| bc044a10c9 | |||
| 558a3198f4 | |||
| 8901941e9f | |||
| c6780b53fc | |||
| d49e756c21 | |||
| 12817a7e82 | |||
| a9baf6da95 | |||
| a2f21e1286 | |||
| 7de0f434c3 | |||
| c74cc19d1b | |||
| 1191ef9f1f | |||
| 26128feb7e | |||
| 48dd10ea05 | |||
| 93fdce1d6e | |||
| b5ace86a03 |
7
.env.example
Normal file
7
.env.example
Normal 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=
|
||||
@@ -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 {
|
||||
|
||||
@@ -41,6 +41,11 @@ pipeline {
|
||||
args '-u root'
|
||||
}
|
||||
}
|
||||
|
||||
environment {
|
||||
PYTHONDONTWRITEBYTECODE = 1
|
||||
}
|
||||
|
||||
steps {
|
||||
dir('backend') {
|
||||
sh '''
|
||||
|
||||
11
README.md
11
README.md
@@ -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
|
||||
|
||||
@@ -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) ----
|
||||
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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():
|
||||
try:
|
||||
return build_history()
|
||||
except (requests.RequestException, ValueError):
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
content={"builds": [], "error": "jenkins_unavailable"},
|
||||
)
|
||||
|
||||
@@ -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]}
|
||||
|
||||
@@ -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", "")
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;"]
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import { defineConfig, loadEnv } from 'vite';
|
||||
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()],
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
@@ -9,5 +17,18 @@ export default defineConfig({
|
||||
},
|
||||
server: {
|
||||
port: 5173,
|
||||
proxy: apiProxyPath
|
||||
? {
|
||||
[apiProxyPath]: {
|
||||
target: 'http://localhost:8000',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) =>
|
||||
path.startsWith(apiProxyPath)
|
||||
? path.slice(apiProxyPath.length) || '/'
|
||||
: path,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user