Initial commit: Setup GitOps structure with Kustomize
- Added Kustomize base and overlays (dev, staging, prod) - Added Gitea Actions CI/CD workflow - Configured multi-environment deployment
This commit is contained in:
22
.env.example
Normal file
22
.env.example
Normal file
@@ -0,0 +1,22 @@
|
||||
# ===========================================
|
||||
# Copie este arquivo para .env
|
||||
# cp .env.example .env
|
||||
# ===========================================
|
||||
|
||||
# Banco de Dados
|
||||
DB_HOST=postgres
|
||||
DB_PORT=5432
|
||||
DB_NAME=inventory
|
||||
DB_USER=postgres
|
||||
DB_PASSWORD=postgres123
|
||||
|
||||
# Redis
|
||||
REDIS_HOST=redis
|
||||
REDIS_PORT=6379
|
||||
|
||||
# Autenticação
|
||||
JWT_SECRET=mude-isso-em-producao
|
||||
AUTH_TOKEN=Tistech2018!#
|
||||
|
||||
# API
|
||||
API_PORT=3001
|
||||
112
.gitea/workflows/ci-cd.yaml
Normal file
112
.gitea/workflows/ci-cd.yaml
Normal file
@@ -0,0 +1,112 @@
|
||||
name: CI/CD Pipeline
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, develop, staging]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
env:
|
||||
REGISTRY: gitea.tisdev.cloud
|
||||
IMAGE_PREFIX: gitea.tisdev.cloud/gitadmin/inventory-app
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Install Backend Dependencies
|
||||
run: cd backend && npm install
|
||||
|
||||
- name: Install Frontend Dependencies
|
||||
run: cd frontend && npm install
|
||||
|
||||
- name: Build Frontend
|
||||
run: cd frontend && npm run build
|
||||
|
||||
build-and-push:
|
||||
needs: test
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Gitea Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: gitadmin
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
|
||||
- name: Get short SHA
|
||||
id: sha
|
||||
run: echo "short=$(echo ${{ gitea.sha }} | cut -c1-7)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build and Push API
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: ./backend
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.IMAGE_PREFIX }}/api:${{ steps.sha.outputs.short }}
|
||||
${{ env.IMAGE_PREFIX }}/api:${{ gitea.ref_name }}
|
||||
${{ env.IMAGE_PREFIX }}/api:latest
|
||||
|
||||
- name: Build and Push Frontend
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: ./frontend
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.IMAGE_PREFIX }}/frontend:${{ steps.sha.outputs.short }}
|
||||
${{ env.IMAGE_PREFIX }}/frontend:${{ gitea.ref_name }}
|
||||
${{ env.IMAGE_PREFIX }}/frontend:latest
|
||||
|
||||
update-manifests:
|
||||
needs: build-and-push
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GITEA_TOKEN }}
|
||||
|
||||
- name: Get short SHA
|
||||
id: sha
|
||||
run: echo "short=$(echo ${{ gitea.sha }} | cut -c1-7)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Determine environment
|
||||
id: env
|
||||
run: |
|
||||
if [ "${{ gitea.ref_name }}" == "main" ]; then
|
||||
echo "overlay=prod" >> $GITHUB_OUTPUT
|
||||
elif [ "${{ gitea.ref_name }}" == "staging" ]; then
|
||||
echo "overlay=staging" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "overlay=dev" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Update Kustomization
|
||||
run: |
|
||||
cd k8s/overlays/${{ steps.env.outputs.overlay }}
|
||||
# Update image tags in kustomization.yaml
|
||||
sed -i "s/newTag: .*/newTag: ${{ steps.sha.outputs.short }}/g" kustomization.yaml
|
||||
|
||||
- name: Commit and Push
|
||||
run: |
|
||||
git config user.name "Gitea Actions"
|
||||
git config user.email "actions@gitea.tisdev.cloud"
|
||||
git add -A
|
||||
git diff --staged --quiet || git commit -m "Deploy: Update images to ${{ steps.sha.outputs.short }} [${{ steps.env.outputs.overlay }}]"
|
||||
git push
|
||||
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
node_modules/
|
||||
.env
|
||||
*.log
|
||||
dist/
|
||||
.DS_Store
|
||||
7
backend/Dockerfile
Normal file
7
backend/Dockerfile
Normal file
@@ -0,0 +1,7 @@
|
||||
FROM node:20-alpine
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm install --production
|
||||
COPY . .
|
||||
EXPOSE 3001
|
||||
CMD ["node", "server.js"]
|
||||
21
backend/package.json
Normal file
21
backend/package.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "inventory-api",
|
||||
"version": "1.0.0",
|
||||
"description": "API para gerenciamento de inventário",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"dev": "nodemon server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"pg": "^8.11.3",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"helmet": "^7.1.0",
|
||||
"redis": "^4.6.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.2"
|
||||
}
|
||||
}
|
||||
302
backend/server.js
Normal file
302
backend/server.js
Normal file
@@ -0,0 +1,302 @@
|
||||
const express = require('express');
|
||||
const { Pool } = require('pg');
|
||||
const cors = require('cors');
|
||||
const helmet = require('helmet');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3001;
|
||||
const AUTH_TOKEN = process.env.AUTH_TOKEN || 'Tistech2018!#';
|
||||
const JWT_SECRET = process.env.JWT_SECRET || crypto.randomBytes(32).toString('hex');
|
||||
|
||||
// Middleware
|
||||
app.use(helmet());
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
// Simple JWT functions
|
||||
const createToken = (payload) => {
|
||||
const header = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString('base64url');
|
||||
const body = Buffer.from(JSON.stringify({ ...payload, exp: Date.now() + 24 * 60 * 60 * 1000 })).toString('base64url');
|
||||
const signature = crypto.createHmac('sha256', JWT_SECRET).update(`${header}.${body}`).digest('base64url');
|
||||
return `${header}.${body}.${signature}`;
|
||||
};
|
||||
|
||||
const verifyToken = (token) => {
|
||||
try {
|
||||
const [header, body, signature] = token.split('.');
|
||||
const expectedSig = crypto.createHmac('sha256', JWT_SECRET).update(`${header}.${body}`).digest('base64url');
|
||||
if (signature !== expectedSig) return null;
|
||||
const payload = JSON.parse(Buffer.from(body, 'base64url').toString());
|
||||
if (payload.exp < Date.now()) return null;
|
||||
return payload;
|
||||
} catch { return null; }
|
||||
};
|
||||
|
||||
// Auth middleware
|
||||
const authMiddleware = (req, res, next) => {
|
||||
if (req.path === '/api/auth/login' || req.path === '/health') return next();
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
return res.status(401).json({ error: 'Token não fornecido' });
|
||||
}
|
||||
const token = authHeader.split(' ')[1];
|
||||
const payload = verifyToken(token);
|
||||
if (!payload) {
|
||||
return res.status(401).json({ error: 'Token inválido ou expirado' });
|
||||
}
|
||||
req.user = payload;
|
||||
next();
|
||||
};
|
||||
|
||||
// Login endpoint
|
||||
app.post('/api/auth/login', (req, res) => {
|
||||
const { token } = req.body;
|
||||
if (token === AUTH_TOKEN) {
|
||||
const jwt = createToken({ authenticated: true, loginTime: Date.now() });
|
||||
res.json({ success: true, token: jwt });
|
||||
} else {
|
||||
res.status(401).json({ error: 'Token inválido' });
|
||||
}
|
||||
});
|
||||
|
||||
// Verify token endpoint
|
||||
app.get('/api/auth/verify', authMiddleware, (req, res) => {
|
||||
res.json({ valid: true, user: req.user });
|
||||
});
|
||||
|
||||
// Apply auth middleware to all /api routes except login
|
||||
app.use('/api', authMiddleware);
|
||||
|
||||
// Database connection
|
||||
const pool = new Pool({
|
||||
host: process.env.DB_HOST || 'postgres-postgresql.inventory-app.svc.cluster.local',
|
||||
port: process.env.DB_PORT || 5432,
|
||||
database: process.env.DB_NAME || 'inventory',
|
||||
user: process.env.DB_USER || 'postgres',
|
||||
password: process.env.DB_PASSWORD || 'Postgres@2024!'
|
||||
});
|
||||
|
||||
// Health check
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({ status: 'ok', timestamp: new Date() });
|
||||
});
|
||||
|
||||
// ============ ENVIRONMENTS ============
|
||||
app.get('/api/environments', async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query('SELECT * FROM environments ORDER BY name');
|
||||
res.json(result.rows);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// ============ SERVERS ============
|
||||
app.get('/api/servers', async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query(`
|
||||
SELECT s.*, e.name as environment_name, e.color as environment_color
|
||||
FROM servers s
|
||||
LEFT JOIN environments e ON s.environment_id = e.id
|
||||
ORDER BY s.name
|
||||
`);
|
||||
res.json(result.rows);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/servers/:id', async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query(`
|
||||
SELECT s.*, e.name as environment_name
|
||||
FROM servers s
|
||||
LEFT JOIN environments e ON s.environment_id = e.id
|
||||
WHERE s.id = $1
|
||||
`, [req.params.id]);
|
||||
res.json(result.rows[0]);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/servers', async (req, res) => {
|
||||
const { name, hostname, ip_address, port, os, cpu_cores, ram_gb, disk_gb, environment_id, notes } = req.body;
|
||||
try {
|
||||
const result = await pool.query(`
|
||||
INSERT INTO servers (name, hostname, ip_address, port, os, cpu_cores, ram_gb, disk_gb, environment_id, notes)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||
RETURNING *
|
||||
`, [name, hostname, ip_address, port, os, cpu_cores, ram_gb, disk_gb, environment_id, notes]);
|
||||
res.status(201).json(result.rows[0]);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.put('/api/servers/:id', async (req, res) => {
|
||||
const { name, hostname, ip_address, port, os, cpu_cores, ram_gb, disk_gb, environment_id, status, notes } = req.body;
|
||||
try {
|
||||
const result = await pool.query(`
|
||||
UPDATE servers SET name=$1, hostname=$2, ip_address=$3, port=$4, os=$5, cpu_cores=$6, ram_gb=$7, disk_gb=$8, environment_id=$9, status=$10, notes=$11
|
||||
WHERE id=$12 RETURNING *
|
||||
`, [name, hostname, ip_address, port, os, cpu_cores, ram_gb, disk_gb, environment_id, status, notes, req.params.id]);
|
||||
res.json(result.rows[0]);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.delete('/api/servers/:id', async (req, res) => {
|
||||
try {
|
||||
await pool.query('DELETE FROM servers WHERE id = $1', [req.params.id]);
|
||||
res.json({ message: 'Servidor deletado' });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// ============ APPLICATIONS ============
|
||||
app.get('/api/applications', async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query(`
|
||||
SELECT a.*, e.name as environment_name, e.color as environment_color
|
||||
FROM applications a
|
||||
LEFT JOIN environments e ON a.environment_id = e.id
|
||||
ORDER BY a.name
|
||||
`);
|
||||
res.json(result.rows);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/applications/:id', async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query(`
|
||||
SELECT a.*, e.name as environment_name
|
||||
FROM applications a
|
||||
LEFT JOIN environments e ON a.environment_id = e.id
|
||||
WHERE a.id = $1
|
||||
`, [req.params.id]);
|
||||
res.json(result.rows[0]);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/applications', async (req, res) => {
|
||||
const { name, description, url, repository_url, version, environment_id, port } = req.body;
|
||||
try {
|
||||
const result = await pool.query(`
|
||||
INSERT INTO applications (name, description, url, repository_url, version, environment_id, port)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *
|
||||
`, [name, description, url, repository_url, version, environment_id, port]);
|
||||
res.status(201).json(result.rows[0]);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.delete('/api/applications/:id', async (req, res) => {
|
||||
try {
|
||||
await pool.query('DELETE FROM applications WHERE id = $1', [req.params.id]);
|
||||
res.json({ message: 'Aplicação deletada' });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// ============ CREDENTIALS ============
|
||||
app.get('/api/credentials', async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query(`
|
||||
SELECT c.*, s.name as server_name, s.ip_address as server_ip
|
||||
FROM credentials c
|
||||
LEFT JOIN servers s ON c.server_id = s.id
|
||||
ORDER BY c.name
|
||||
`);
|
||||
res.json(result.rows);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/credentials', async (req, res) => {
|
||||
const { name, username, password, ssh_key, credential_type, server_id, notes } = req.body;
|
||||
try {
|
||||
const result = await pool.query(`
|
||||
INSERT INTO credentials (name, username, password, ssh_key, credential_type, server_id, notes)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *
|
||||
`, [name, username, password, ssh_key, credential_type, server_id, notes]);
|
||||
res.status(201).json(result.rows[0]);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.delete('/api/credentials/:id', async (req, res) => {
|
||||
try {
|
||||
await pool.query('DELETE FROM credentials WHERE id = $1', [req.params.id]);
|
||||
res.json({ message: 'Credencial deletada' });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// ============ APP CREDENTIALS ============
|
||||
app.get('/api/app-credentials', async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query(`
|
||||
SELECT ac.*, a.name as application_name, a.url as application_url
|
||||
FROM app_credentials ac
|
||||
LEFT JOIN applications a ON ac.application_id = a.id
|
||||
ORDER BY a.name, ac.name
|
||||
`);
|
||||
res.json(result.rows);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/app-credentials', async (req, res) => {
|
||||
const { application_id, name, username, password, notes } = req.body;
|
||||
try {
|
||||
const result = await pool.query(`
|
||||
INSERT INTO app_credentials (application_id, name, username, password, notes)
|
||||
VALUES ($1, $2, $3, $4, $5) RETURNING *
|
||||
`, [application_id, name, username, password, notes]);
|
||||
res.status(201).json(result.rows[0]);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// ============ DASHBOARD STATS ============
|
||||
app.get('/api/dashboard/stats', async (req, res) => {
|
||||
try {
|
||||
const servers = await pool.query('SELECT COUNT(*) as count FROM servers');
|
||||
const applications = await pool.query('SELECT COUNT(*) as count FROM applications');
|
||||
const credentials = await pool.query('SELECT COUNT(*) as count FROM credentials');
|
||||
const appCredentials = await pool.query('SELECT COUNT(*) as count FROM app_credentials');
|
||||
const totalCpu = await pool.query('SELECT SUM(cpu_cores) as total FROM servers');
|
||||
const totalRam = await pool.query('SELECT SUM(ram_gb) as total FROM servers');
|
||||
const totalDisk = await pool.query('SELECT SUM(disk_gb) as total FROM servers');
|
||||
|
||||
res.json({
|
||||
servers: parseInt(servers.rows[0].count),
|
||||
applications: parseInt(applications.rows[0].count),
|
||||
credentials: parseInt(credentials.rows[0].count) + parseInt(appCredentials.rows[0].count),
|
||||
totalCpu: parseInt(totalCpu.rows[0].total) || 0,
|
||||
totalRam: parseInt(totalRam.rows[0].total) || 0,
|
||||
totalDisk: parseInt(totalDisk.rows[0].total) || 0
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`API rodando na porta ${PORT}`);
|
||||
});
|
||||
74
docker-compose.yml
Normal file
74
docker-compose.yml
Normal file
@@ -0,0 +1,74 @@
|
||||
version: '3.8'
|
||||
|
||||
# ===========================================
|
||||
# AMBIENTE DE DESENVOLVIMENTO LOCAL
|
||||
# Roda: docker compose up
|
||||
# ===========================================
|
||||
|
||||
services:
|
||||
# Frontend React (com hot reload)
|
||||
frontend:
|
||||
build: ./frontend
|
||||
ports:
|
||||
- "3000:80"
|
||||
volumes:
|
||||
- ./frontend/src:/app/src # Hot reload
|
||||
depends_on:
|
||||
- backend
|
||||
environment:
|
||||
- VITE_API_URL=http://localhost:3001
|
||||
|
||||
# Backend Node.js (com hot reload)
|
||||
backend:
|
||||
build: ./backend
|
||||
ports:
|
||||
- "3001:3001"
|
||||
volumes:
|
||||
- ./backend:/app
|
||||
- /app/node_modules
|
||||
depends_on:
|
||||
- postgres
|
||||
- redis
|
||||
environment:
|
||||
- DB_HOST=postgres
|
||||
- DB_PORT=5432
|
||||
- DB_NAME=inventory
|
||||
- DB_USER=postgres
|
||||
- DB_PASSWORD=postgres123
|
||||
- REDIS_HOST=redis
|
||||
- REDIS_PORT=6379
|
||||
- JWT_SECRET=dev-secret-change-in-production
|
||||
- AUTH_TOKEN=Tistech2018!#
|
||||
command: npm run dev # nodemon para hot reload
|
||||
|
||||
# PostgreSQL
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
ports:
|
||||
- "5432:5432"
|
||||
environment:
|
||||
- POSTGRES_DB=inventory
|
||||
- POSTGRES_USER=postgres
|
||||
- POSTGRES_PASSWORD=postgres123
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
|
||||
# Redis (cache/sessões)
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
|
||||
# Adminer (visualizar banco de dados)
|
||||
adminer:
|
||||
image: adminer
|
||||
ports:
|
||||
- "8080:8080"
|
||||
depends_on:
|
||||
- postgres
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
redis_data:
|
||||
12
frontend/Dockerfile
Normal file
12
frontend/Dockerfile
Normal file
@@ -0,0 +1,12 @@
|
||||
FROM node:20-alpine as builder
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
FROM nginx:alpine
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
EXPOSE 80
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
13
frontend/index.html
Normal file
13
frontend/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-BR">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Inventory Manager</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
17
frontend/nginx.conf
Normal file
17
frontend/nginx.conf
Normal file
@@ -0,0 +1,17 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location /api {
|
||||
proxy_pass http://inventory-api:3001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
}
|
||||
27
frontend/package.json
Normal file
27
frontend/package.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "inventory-frontend",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.20.0",
|
||||
"axios": "^1.6.2",
|
||||
"lucide-react": "^0.294.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.37",
|
||||
"@types/react-dom": "^18.2.15",
|
||||
"@vitejs/plugin-react": "^4.2.0",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"postcss": "^8.4.31",
|
||||
"tailwindcss": "^3.3.5",
|
||||
"vite": "^5.0.0"
|
||||
}
|
||||
}
|
||||
6
frontend/postcss.config.js
Normal file
6
frontend/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
413
frontend/src/App.jsx
Normal file
413
frontend/src/App.jsx
Normal file
@@ -0,0 +1,413 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import axios from 'axios'
|
||||
import { Server, AppWindow, Key, Database, Cpu, HardDrive, MemoryStick, Eye, EyeOff, LogOut, Lock } from 'lucide-react'
|
||||
|
||||
const API_URL = '/api'
|
||||
|
||||
// Configure axios to use token
|
||||
const api = axios.create({ baseURL: API_URL })
|
||||
|
||||
api.interceptors.request.use((config) => {
|
||||
const token = localStorage.getItem('authToken')
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
return config
|
||||
})
|
||||
|
||||
api.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
if (error.response?.status === 401) {
|
||||
localStorage.removeItem('authToken')
|
||||
window.location.reload()
|
||||
}
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
function App() {
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [loginToken, setLoginToken] = useState('')
|
||||
const [loginError, setLoginError] = useState('')
|
||||
const [activeTab, setActiveTab] = useState('dashboard')
|
||||
const [stats, setStats] = useState({})
|
||||
const [servers, setServers] = useState([])
|
||||
const [applications, setApplications] = useState([])
|
||||
const [credentials, setCredentials] = useState([])
|
||||
const [appCredentials, setAppCredentials] = useState([])
|
||||
const [showPasswords, setShowPasswords] = useState({})
|
||||
|
||||
useEffect(() => {
|
||||
checkAuth()
|
||||
}, [])
|
||||
|
||||
const checkAuth = async () => {
|
||||
const token = localStorage.getItem('authToken')
|
||||
if (token) {
|
||||
try {
|
||||
await api.get('/auth/verify')
|
||||
setIsAuthenticated(true)
|
||||
fetchData()
|
||||
} catch {
|
||||
localStorage.removeItem('authToken')
|
||||
}
|
||||
}
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
const handleLogin = async (e) => {
|
||||
e.preventDefault()
|
||||
setLoginError('')
|
||||
try {
|
||||
const response = await axios.post(`${API_URL}/auth/login`, { token: loginToken })
|
||||
localStorage.setItem('authToken', response.data.token)
|
||||
setIsAuthenticated(true)
|
||||
fetchData()
|
||||
} catch (err) {
|
||||
setLoginError('Token inválido')
|
||||
}
|
||||
}
|
||||
|
||||
const handleLogout = () => {
|
||||
localStorage.removeItem('authToken')
|
||||
setIsAuthenticated(false)
|
||||
setLoginToken('')
|
||||
}
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const [statsRes, serversRes, appsRes, credsRes, appCredsRes] = await Promise.all([
|
||||
api.get('/dashboard/stats'),
|
||||
api.get('/servers'),
|
||||
api.get('/applications'),
|
||||
api.get('/credentials'),
|
||||
api.get('/app-credentials')
|
||||
])
|
||||
setStats(statsRes.data)
|
||||
setServers(serversRes.data)
|
||||
setApplications(appsRes.data)
|
||||
setCredentials(credsRes.data)
|
||||
setAppCredentials(appCredsRes.data)
|
||||
} catch (err) {
|
||||
console.error('Erro ao carregar dados:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const togglePassword = (id) => {
|
||||
setShowPasswords(prev => ({ ...prev, [id]: !prev[id] }))
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 flex items-center justify-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500"></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 flex items-center justify-center p-4">
|
||||
<div className="bg-gray-800 rounded-2xl shadow-2xl p-8 w-full max-w-md border border-gray-700">
|
||||
<div className="flex flex-col items-center mb-8">
|
||||
<div className="bg-blue-600 p-4 rounded-full mb-4">
|
||||
<Lock className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-white">Inventory Manager</h1>
|
||||
<p className="text-gray-400 mt-2">Digite o token de acesso</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleLogin}>
|
||||
<div className="mb-6">
|
||||
<label className="block text-gray-300 text-sm font-medium mb-2">
|
||||
Token de Acesso
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={loginToken}
|
||||
onChange={(e) => setLoginToken(e.target.value)}
|
||||
className="w-full px-4 py-3 bg-gray-700 border border-gray-600 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
placeholder="Digite seu token"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
{loginError && (
|
||||
<div className="mb-4 p-3 bg-red-900/50 border border-red-700 rounded-lg text-red-300 text-sm">
|
||||
{loginError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full bg-blue-600 hover:bg-blue-700 text-white font-semibold py-3 px-4 rounded-lg transition-colors"
|
||||
>
|
||||
Entrar
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const StatCard = ({ icon: Icon, title, value, color }) => (
|
||||
<div className="bg-white rounded-xl shadow-sm p-6 border border-gray-100">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className={`p-3 rounded-lg ${color}`}>
|
||||
<Icon className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-gray-500 text-sm">{title}</p>
|
||||
<p className="text-2xl font-bold text-gray-800">{value}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const Dashboard = () => (
|
||||
<div className="space-y-6">
|
||||
<h2 className="text-2xl font-bold text-gray-800">Dashboard</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<StatCard icon={Server} title="Servidores" value={stats.servers || 0} color="bg-blue-500" />
|
||||
<StatCard icon={AppWindow} title="Aplicacoes" value={stats.applications || 0} color="bg-green-500" />
|
||||
<StatCard icon={Key} title="Credenciais" value={stats.credentials || 0} color="bg-purple-500" />
|
||||
<StatCard icon={Cpu} title="Total CPU" value={`${stats.totalCpu || 0} cores`} color="bg-orange-500" />
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<StatCard icon={MemoryStick} title="Total RAM" value={`${stats.totalRam || 0} GB`} color="bg-pink-500" />
|
||||
<StatCard icon={HardDrive} title="Total Disco" value={`${stats.totalDisk || 0} GB`} color="bg-cyan-500" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const ServersTab = () => (
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-2xl font-bold text-gray-800">Servidores</h2>
|
||||
<div className="bg-white rounded-xl shadow-sm overflow-hidden">
|
||||
<table className="w-full">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Nome</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">IP</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">OS</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">CPU</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">RAM</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200">
|
||||
{servers.map(server => (
|
||||
<tr key={server.id} className="hover:bg-gray-50">
|
||||
<td className="px-6 py-4">
|
||||
<div className="font-medium text-gray-900">{server.name}</div>
|
||||
<div className="text-sm text-gray-500">{server.hostname}</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-gray-600">{server.ip_address}</td>
|
||||
<td className="px-6 py-4 text-gray-600 text-sm">{server.os}</td>
|
||||
<td className="px-6 py-4 text-gray-600">{server.cpu_cores} cores</td>
|
||||
<td className="px-6 py-4 text-gray-600">{server.ram_gb} GB</td>
|
||||
<td className="px-6 py-4">
|
||||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${
|
||||
server.status === 'active' ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
|
||||
}`}>
|
||||
{server.status}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const ApplicationsTab = () => (
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-2xl font-bold text-gray-800">Aplicacoes</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{applications.map(app => (
|
||||
<div key={app.id} className="bg-white rounded-xl shadow-sm p-6 border border-gray-100">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<h3 className="font-semibold text-lg text-gray-800">{app.name}</h3>
|
||||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${
|
||||
app.status === 'running' ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800'
|
||||
}`}>
|
||||
{app.status}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-gray-600 text-sm mb-4">{app.description}</p>
|
||||
{app.url && (
|
||||
<a
|
||||
href={app.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2 text-blue-600 hover:text-blue-800 text-sm"
|
||||
>
|
||||
Acessar
|
||||
</a>
|
||||
)}
|
||||
{app.port && (
|
||||
<p className="text-gray-500 text-sm mt-2">Porta: {app.port}</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const CredentialsTab = () => (
|
||||
<div className="space-y-6">
|
||||
<h2 className="text-2xl font-bold text-gray-800">Credenciais</h2>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold text-gray-700">Credenciais de Servidores</h3>
|
||||
<div className="bg-white rounded-xl shadow-sm overflow-hidden">
|
||||
<table className="w-full">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Nome</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Servidor</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Usuario</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Senha</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200">
|
||||
{credentials.map(cred => (
|
||||
<tr key={cred.id} className="hover:bg-gray-50">
|
||||
<td className="px-6 py-4 font-medium text-gray-900">{cred.name}</td>
|
||||
<td className="px-6 py-4 text-gray-600">
|
||||
{cred.server_name} ({cred.server_ip})
|
||||
</td>
|
||||
<td className="px-6 py-4 text-gray-600">{cred.username}</td>
|
||||
<td className="px-6 py-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<code className="bg-gray-100 px-2 py-1 rounded text-sm">
|
||||
{showPasswords[cred.id] ? cred.password : '**********'}
|
||||
</code>
|
||||
<button
|
||||
onClick={() => togglePassword(cred.id)}
|
||||
className="text-gray-500 hover:text-gray-700"
|
||||
>
|
||||
{showPasswords[cred.id] ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold text-gray-700">Credenciais de Aplicacoes</h3>
|
||||
<div className="bg-white rounded-xl shadow-sm overflow-hidden">
|
||||
<table className="w-full">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Aplicacao</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Nome</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Usuario</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Senha</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">URL</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200">
|
||||
{appCredentials.map(cred => (
|
||||
<tr key={cred.id} className="hover:bg-gray-50">
|
||||
<td className="px-6 py-4 font-medium text-gray-900">{cred.application_name}</td>
|
||||
<td className="px-6 py-4 text-gray-600">{cred.name}</td>
|
||||
<td className="px-6 py-4 text-gray-600">{cred.username}</td>
|
||||
<td className="px-6 py-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<code className="bg-gray-100 px-2 py-1 rounded text-sm">
|
||||
{showPasswords[`app-${cred.id}`] ? cred.password : '**********'}
|
||||
</code>
|
||||
<button
|
||||
onClick={() => togglePassword(`app-${cred.id}`)}
|
||||
className="text-gray-500 hover:text-gray-700"
|
||||
>
|
||||
{showPasswords[`app-${cred.id}`] ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
{cred.application_url && (
|
||||
<a
|
||||
href={cred.application_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 hover:text-blue-800"
|
||||
>
|
||||
Abrir
|
||||
</a>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const tabs = [
|
||||
{ id: 'dashboard', label: 'Dashboard', icon: Database },
|
||||
{ id: 'servers', label: 'Servidores', icon: Server },
|
||||
{ id: 'applications', label: 'Aplicacoes', icon: AppWindow },
|
||||
{ id: 'credentials', label: 'Credenciais', icon: Key },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<nav className="bg-white shadow-sm border-b border-gray-200">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex justify-between h-16">
|
||||
<div className="flex items-center">
|
||||
<Database className="w-8 h-8 text-blue-600" />
|
||||
<span className="ml-2 text-xl font-bold text-gray-800">Inventory Manager</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="flex items-center gap-2 px-4 py-2 text-gray-600 hover:text-red-600 transition-colors"
|
||||
>
|
||||
<LogOut className="w-5 h-5" />
|
||||
Sair
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="flex gap-4 mb-8 border-b border-gray-200">
|
||||
{tabs.map(tab => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className={`flex items-center gap-2 px-4 py-2 -mb-px border-b-2 transition-colors ${
|
||||
activeTab === tab.id
|
||||
? 'border-blue-600 text-blue-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
<tab.icon className="w-5 h-5" />
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{activeTab === 'dashboard' && <Dashboard />}
|
||||
{activeTab === 'servers' && <ServersTab />}
|
||||
{activeTab === 'applications' && <ApplicationsTab />}
|
||||
{activeTab === 'credentials' && <CredentialsTab />}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
7
frontend/src/index.css
Normal file
7
frontend/src/index.css
Normal file
@@ -0,0 +1,7 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
body {
|
||||
font-family: 'Inter', system-ui, sans-serif;
|
||||
}
|
||||
10
frontend/src/main.jsx
Normal file
10
frontend/src/main.jsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App.jsx'
|
||||
import './index.css'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
)
|
||||
7
frontend/tailwind.config.js
Normal file
7
frontend/tailwind.config.js
Normal file
@@ -0,0 +1,7 @@
|
||||
export default {
|
||||
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
10
frontend/vite.config.js
Normal file
10
frontend/vite.config.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: 5173
|
||||
}
|
||||
})
|
||||
57
k8s/base/deployment-api.yaml
Normal file
57
k8s/base/deployment-api.yaml
Normal file
@@ -0,0 +1,57 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: inventory-api
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: inventory-api
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: inventory-api
|
||||
spec:
|
||||
containers:
|
||||
- name: inventory-api
|
||||
image: gitea.tisdev.cloud/gitadmin/inventory-app/api:latest
|
||||
ports:
|
||||
- containerPort: 3001
|
||||
env:
|
||||
- name: DB_HOST
|
||||
value: "postgres-postgresql"
|
||||
- name: DB_PORT
|
||||
value: "5432"
|
||||
- name: DB_NAME
|
||||
value: "inventory"
|
||||
- name: DB_USER
|
||||
value: "postgres"
|
||||
- name: DB_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: postgres-secret
|
||||
key: postgres-password
|
||||
- name: JWT_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: inventory-secrets
|
||||
key: jwt-secret
|
||||
resources:
|
||||
requests:
|
||||
memory: "128Mi"
|
||||
cpu: "100m"
|
||||
limits:
|
||||
memory: "256Mi"
|
||||
cpu: "500m"
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: inventory-api
|
||||
spec:
|
||||
selector:
|
||||
app: inventory-api
|
||||
ports:
|
||||
- port: 3001
|
||||
targetPort: 3001
|
||||
type: ClusterIP
|
||||
38
k8s/base/deployment-frontend.yaml
Normal file
38
k8s/base/deployment-frontend.yaml
Normal file
@@ -0,0 +1,38 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: inventory-frontend
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: inventory-frontend
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: inventory-frontend
|
||||
spec:
|
||||
containers:
|
||||
- name: inventory-frontend
|
||||
image: gitea.tisdev.cloud/gitadmin/inventory-app/frontend:latest
|
||||
ports:
|
||||
- containerPort: 80
|
||||
resources:
|
||||
requests:
|
||||
memory: "64Mi"
|
||||
cpu: "50m"
|
||||
limits:
|
||||
memory: "128Mi"
|
||||
cpu: "200m"
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: inventory-frontend
|
||||
spec:
|
||||
selector:
|
||||
app: inventory-frontend
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 80
|
||||
type: ClusterIP
|
||||
7
k8s/base/kustomization.yaml
Normal file
7
k8s/base/kustomization.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
resources:
|
||||
- deployment-api.yaml
|
||||
- deployment-frontend.yaml
|
||||
- secrets.yaml
|
||||
15
k8s/base/secrets.yaml
Normal file
15
k8s/base/secrets.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: postgres-secret
|
||||
type: Opaque
|
||||
stringData:
|
||||
postgres-password: postgres123
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: inventory-secrets
|
||||
type: Opaque
|
||||
stringData:
|
||||
jwt-secret: super-secret-jwt-key-change-in-production
|
||||
22
k8s/overlays/dev/kustomization.yaml
Normal file
22
k8s/overlays/dev/kustomization.yaml
Normal file
@@ -0,0 +1,22 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
namespace: inventory-dev
|
||||
|
||||
resources:
|
||||
- ../../base
|
||||
|
||||
replicas:
|
||||
- name: inventory-api
|
||||
count: 1
|
||||
- name: inventory-frontend
|
||||
count: 1
|
||||
|
||||
images:
|
||||
- name: gitea.tisdev.cloud/gitadmin/inventory-app/api
|
||||
newTag: latest
|
||||
- name: gitea.tisdev.cloud/gitadmin/inventory-app/frontend
|
||||
newTag: latest
|
||||
|
||||
commonLabels:
|
||||
environment: dev
|
||||
31
k8s/overlays/prod/kustomization.yaml
Normal file
31
k8s/overlays/prod/kustomization.yaml
Normal file
@@ -0,0 +1,31 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
namespace: inventory-app
|
||||
|
||||
resources:
|
||||
- ../../base
|
||||
|
||||
replicas:
|
||||
- name: inventory-api
|
||||
count: 2
|
||||
- name: inventory-frontend
|
||||
count: 2
|
||||
|
||||
images:
|
||||
- name: gitea.tisdev.cloud/gitadmin/inventory-app/api
|
||||
newTag: latest
|
||||
- name: gitea.tisdev.cloud/gitadmin/inventory-app/frontend
|
||||
newTag: latest
|
||||
|
||||
commonLabels:
|
||||
environment: production
|
||||
|
||||
patches:
|
||||
- patch: |-
|
||||
- op: replace
|
||||
path: /stringData/jwt-secret
|
||||
value: prod-super-secret-jwt-key-2024
|
||||
target:
|
||||
kind: Secret
|
||||
name: inventory-secrets
|
||||
22
k8s/overlays/staging/kustomization.yaml
Normal file
22
k8s/overlays/staging/kustomization.yaml
Normal file
@@ -0,0 +1,22 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
namespace: inventory-staging
|
||||
|
||||
resources:
|
||||
- ../../base
|
||||
|
||||
replicas:
|
||||
- name: inventory-api
|
||||
count: 1
|
||||
- name: inventory-frontend
|
||||
count: 1
|
||||
|
||||
images:
|
||||
- name: gitea.tisdev.cloud/gitadmin/inventory-app/api
|
||||
newTag: latest
|
||||
- name: gitea.tisdev.cloud/gitadmin/inventory-app/frontend
|
||||
newTag: latest
|
||||
|
||||
commonLabels:
|
||||
environment: staging
|
||||
56
scripts/deploy.sh
Normal file
56
scripts/deploy.sh
Normal file
@@ -0,0 +1,56 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ===========================================
|
||||
# Script para deploy no Kubernetes
|
||||
# Execute: ./scripts/deploy.sh [dev|staging|prod]
|
||||
# ===========================================
|
||||
|
||||
ENV=${1:-prod}
|
||||
|
||||
echo "🚀 Fazendo deploy no ambiente: $ENV"
|
||||
|
||||
# Cores
|
||||
GREEN='\033[0;32m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m'
|
||||
|
||||
# Verificar se está no diretório correto
|
||||
if [ ! -f "docker-compose.yml" ]; then
|
||||
echo -e "${RED}Erro: Execute este script na raiz do projeto${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Build das imagens
|
||||
echo -e "\n[1/4] Construindo imagens Docker..."
|
||||
docker build -t inventory-api:latest ./backend
|
||||
docker build -t inventory-frontend:latest ./frontend
|
||||
|
||||
# Tag com versão
|
||||
VERSION=$(date +%Y%m%d-%H%M%S)
|
||||
docker tag inventory-api:latest inventory-api:$VERSION
|
||||
docker tag inventory-frontend:latest inventory-frontend:$VERSION
|
||||
|
||||
echo -e "${GREEN}✓ Imagens construídas: $VERSION${NC}"
|
||||
|
||||
# Commit e push
|
||||
echo -e "\n[2/4] Enviando código para GitHub..."
|
||||
git add -A
|
||||
git commit -m "Deploy $ENV - $VERSION" || true
|
||||
git push origin main
|
||||
|
||||
echo -e "${GREEN}✓ Código enviado${NC}"
|
||||
|
||||
# ArgoCD vai sincronizar automaticamente
|
||||
echo -e "\n[3/4] ArgoCD sincronizando..."
|
||||
echo "Aguarde o ArgoCD detectar as mudanças (até 3 minutos)"
|
||||
echo "Ou force a sincronização em: https://argocd.dev-ai.cloud"
|
||||
|
||||
echo -e "\n${GREEN}========================================${NC}"
|
||||
echo -e "${GREEN}✅ Deploy iniciado com sucesso!${NC}"
|
||||
echo -e "${GREEN}========================================${NC}"
|
||||
echo ""
|
||||
echo "Acompanhe em:"
|
||||
echo " ArgoCD: https://argocd.dev-ai.cloud"
|
||||
echo " Portainer: https://kube.dev-ai.cloud"
|
||||
echo " App: https://inv.dev-ai.cloud"
|
||||
echo ""
|
||||
77
scripts/setup-local.sh
Normal file
77
scripts/setup-local.sh
Normal file
@@ -0,0 +1,77 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ===========================================
|
||||
# Script para configurar ambiente local
|
||||
# Execute: chmod +x scripts/setup-local.sh && ./scripts/setup-local.sh
|
||||
# ===========================================
|
||||
|
||||
echo "🚀 Configurando ambiente de desenvolvimento local..."
|
||||
|
||||
# Cores
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
# 1. Verificar Docker
|
||||
echo -e "\n${YELLOW}[1/5] Verificando Docker...${NC}"
|
||||
if command -v docker &> /dev/null; then
|
||||
echo -e "${GREEN}✓ Docker instalado${NC}"
|
||||
else
|
||||
echo "Docker não encontrado. Instalando..."
|
||||
sudo apt update && sudo apt install -y docker.io docker-compose-v2
|
||||
sudo usermod -aG docker $USER
|
||||
echo "⚠️ Faça logout e login novamente para usar Docker sem sudo"
|
||||
fi
|
||||
|
||||
# 2. Verificar Node.js
|
||||
echo -e "\n${YELLOW}[2/5] Verificando Node.js...${NC}"
|
||||
if command -v node &> /dev/null; then
|
||||
echo -e "${GREEN}✓ Node.js $(node -v) instalado${NC}"
|
||||
else
|
||||
echo "Node.js não encontrado. Instalando..."
|
||||
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
|
||||
sudo apt install -y nodejs
|
||||
fi
|
||||
|
||||
# 3. Verificar Git
|
||||
echo -e "\n${YELLOW}[3/5] Verificando Git...${NC}"
|
||||
if command -v git &> /dev/null; then
|
||||
echo -e "${GREEN}✓ Git instalado${NC}"
|
||||
else
|
||||
echo "Git não encontrado. Instalando..."
|
||||
sudo apt install -y git
|
||||
fi
|
||||
|
||||
# 4. Criar arquivo .env
|
||||
echo -e "\n${YELLOW}[4/5] Criando arquivo .env...${NC}"
|
||||
if [ ! -f .env ]; then
|
||||
cp .env.example .env
|
||||
echo -e "${GREEN}✓ Arquivo .env criado${NC}"
|
||||
else
|
||||
echo -e "${GREEN}✓ Arquivo .env já existe${NC}"
|
||||
fi
|
||||
|
||||
# 5. Instalar dependências
|
||||
echo -e "\n${YELLOW}[5/5] Instalando dependências...${NC}"
|
||||
cd backend && npm install && cd ..
|
||||
cd frontend && npm install && cd ..
|
||||
echo -e "${GREEN}✓ Dependências instaladas${NC}"
|
||||
|
||||
echo -e "\n${GREEN}========================================${NC}"
|
||||
echo -e "${GREEN}✅ Ambiente configurado com sucesso!${NC}"
|
||||
echo -e "${GREEN}========================================${NC}"
|
||||
echo ""
|
||||
echo "Para iniciar o desenvolvimento:"
|
||||
echo ""
|
||||
echo " docker compose up # Inicia todos os serviços"
|
||||
echo ""
|
||||
echo "Ou sem Docker:"
|
||||
echo ""
|
||||
echo " cd backend && npm run dev # Terminal 1"
|
||||
echo " cd frontend && npm run dev # Terminal 2"
|
||||
echo ""
|
||||
echo "URLs:"
|
||||
echo " Frontend: http://localhost:3000"
|
||||
echo " Backend: http://localhost:3001"
|
||||
echo " Adminer: http://localhost:8080"
|
||||
echo ""
|
||||
Reference in New Issue
Block a user