This commit is contained in:
2025-09-16 15:43:36 +05:00
commit 111cc834dd
17 changed files with 2078 additions and 0 deletions

53
.dockerignore Normal file
View File

@ -0,0 +1,53 @@
# Зависимости
node_modules
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Сборка
dist
build
# Логи
*.log
# Временные файлы
.tmp
.cache
# IDE файлы
.vscode
.idea
*.swp
*.swo
# OS файлы
.DS_Store
Thumbs.db
# Git
.git
.gitignore
# Docker файлы
Dockerfile*
docker-compose*
.dockerignore
# Документация
README.md
*.md
# Kubernetes файлы
k8s/
# Тестовые файлы
coverage
.nyc_output
# Environment файлы
.env
.env.local
.env.development.local
.env.test.local
.env.production.local

120
.gitignore vendored Normal file
View File

@ -0,0 +1,120 @@
# Dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# Build outputs
dist/
build/
out/
# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# IDE and editor files
.vscode/
.idea/
*.swp
*.swo
*~
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Logs
logs
*.log
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Coverage directory used by tools like istanbul
coverage/
*.lcov
# nyc test coverage
.nyc_output
# Dependency directories
jspm_packages/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
# Gatsby files
.cache/
public
# Storybook build outputs
.out
.storybook-out
# Temporary folders
tmp/
temp/
# Vite
.vite/
# TypeScript
*.tsbuildinfo
# Optional stylelint cache
.stylelintcache
# SvelteKit build / generate output
.svelte-kit
# End of https://www.gitignore.io/api/node

64
README.md Normal file
View File

@ -0,0 +1,64 @@
# Demo SPA App
Максимально простое React SPA приложение для демонстрации развертывания в Kubernetes с OAuth2 аутентификацией.
## Особенности
- ✅ Минимальный React приложение
- ✅ Приветствие пользователя по имени
- ✅ Контейнеризация с nginx
- ✅ Принцип KISS - только необходимое
## Локальная разработка
```bash
# Установка зависимостей
npm install
# Запуск в режиме разработки
npm run dev
# Сборка для продакшена
npm run build
```
## Сборка Docker образа
```bash
# Сборка образа
docker build -t demo-spa-app .
# Запуск контейнера
docker run -p 8080:80 demo-spa-app
```
Приложение будет доступно по адресу: http://localhost:8080
## Структура проекта
```
demo-spa-app/
├── src/
│ ├── App.jsx # Главный компонент с приветствием
│ └── main.jsx # Точка входа React
├── Dockerfile # Контейнеризация
├── nginx.conf # Конфигурация nginx
├── package.json # Зависимости
└── vite.config.js # Конфигурация Vite
```
## Технологии
- **React 18** - UI библиотека
- **Vite** - быстрая сборка
- **nginx** - веб-сервер
- **Docker** - контейнеризация

41
dockerfile Normal file
View File

@ -0,0 +1,41 @@
# Многоэтапная сборка для минимального размера образа
FROM node:18-alpine AS builder
WORKDIR /app
# Копируем package.json и package-lock.json (если есть)
COPY package*.json ./
# Устанавливаем зависимости
RUN npm ci
# Копируем исходный код
COPY . .
# Собираем приложение
RUN npm run build
RUN npm prune --production
# Финальный образ с nginx
FROM nginx:alpine
# Копируем собранное приложение
COPY --from=builder /app/dist /usr/share/nginx/html
# Копируем конфигурацию nginx для SPA
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Открываем порт 80
EXPOSE 80
# Запускаем nginx
CMD ["nginx", "-g", "daemon off;"]

21
index.html Normal file
View File

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Demo SPA App</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

104
k8s/README.md Normal file
View File

@ -0,0 +1,104 @@
# Kubernetes манифесты для Demo SPA App
## Структура манифестов
```
k8s/
├── namespace.yaml # Namespace для приложения
├── deployment.yaml # React SPA приложение
├── service.yaml # Внутренний сервис
├── dex-authenticator.yaml # DexAuthenticator для аутентификации
├── ingress.yaml # Внешний доступ с SSL
└── kustomization.yaml # Kustomize конфигурация
```
## Развертывание
### 1. Подготовка образа
```bash
# Сборка образа
docker build -t demo-spa-app:latest .
# Размещение в registry (замените на ваш registry)
docker tag demo-spa-app:latest your-registry.com/demo-spa-app:latest
docker push your-registry.com/demo-spa-app:latest
```
### 2. Настройка конфигурации
Перед развертыванием обновите следующие параметры:
- **Домен**: `demo-spa.example.com` → ваш домен
- **SSL сертификат**: убедитесь что cert-manager настроен
- **Dex**: должен быть развернут в кластере (обычно в namespace `d8-user-authn`)
### 3. Развертывание в Kubernetes
```bash
# Применение всех манифестов
kubectl apply -k k8s/
# Или по отдельности
kubectl apply -f k8s/namespace.yaml
kubectl apply -f k8s/deployment.yaml
kubectl apply -f k8s/service.yaml
kubectl apply -f k8s/dex-authenticator.yaml
kubectl apply -f k8s/ingress.yaml
```
### 4. Проверка развертывания
```bash
# Проверка подов
kubectl get pods -n demo-spa
# Проверка сервисов
kubectl get svc -n demo-spa
# Проверка ingress
kubectl get ingress -n demo-spa
# Логи приложения
kubectl logs -f deployment/demo-spa-app -n demo-spa
# Логи DexAuthenticator
kubectl logs -f deployment/demo-spa-auth -n demo-spa
```
## Конфигурация DexAuthenticator
### Настройка Dex
DexAuthenticator автоматически интегрируется с Dex, развернутым в кластере. Дополнительная настройка OAuth2 клиентов не требуется - Deckhouse управляет этим автоматически.
### Основные параметры DexAuthenticator
- **applicationDomain**: домен вашего приложения
- **sendAuthorizationHeader**: отправка заголовков авторизации в приложение
- **keepUsersLoggedInFor**: время жизни сессии (по умолчанию 24h)
- **allowedGroups**: список разрешенных групп (пустой = все группы)
- **cookieConfig**: настройки безопасности куки
## Архитектура
```
Internet → Ingress → DexAuthenticator → React SPA
Dex (аутентификация)
```
- **Ingress**: Внешний доступ с SSL
- **DexAuthenticator**: Нативная аутентификация через Dex
- **React SPA**: Основное приложение
- **Dex**: OIDC провайдер
## Мониторинг
```bash
# Проверка health check
curl https://demo-spa.example.com/health
# Проверка аутентификации
curl -I https://demo-spa.example.com/
```

53
k8s/deployment.yaml Normal file
View File

@ -0,0 +1,53 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: demo-spa-app
namespace: demo-spa
labels:
app: demo-spa-app
spec:
replicas: 2
selector:
matchLabels:
app: demo-spa-app
template:
metadata:
labels:
app: demo-spa-app
spec:
containers:
- name: demo-spa-app
image: demo-spa-app:latest
imagePullPolicy: Never
ports:
- containerPort: 80
resources:
requests:
memory: "64Mi"
cpu: "50m"
limits:
memory: "128Mi"
cpu: "100m"
livenessProbe:
httpGet:
path: /health
port: 80
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: 80
initialDelaySeconds: 5
periodSeconds: 5
env:
- name: NODE_ENV
value: "production"

View File

@ -0,0 +1,22 @@
apiVersion: deckhouse.io/v1
kind: DexAuthenticator
metadata:
name: demo-spa-auth
namespace: demo-spa
spec:
# Домен приложения
applicationDomain: "demo-spa.127.0.0.1.sslip.io"
# Отправка заголовков авторизации
sendAuthorizationHeader: true
# SSL сертификат
applicationIngressCertificateSecretName: ingress-tls
applicationIngressClassName: nginx
# Время жизни сессии
keepUsersLoggedInFor: "720h" # 30 дней
# Разрешенные группы (пустой массив = все группы)
allowedGroups: []

27
k8s/ingress.yaml Normal file
View File

@ -0,0 +1,27 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: demo-spa-ingress
namespace: demo-spa
annotations:
nginx.ingress.kubernetes.io/auth-url: "https://demo-spa.127.0.0.1.sslip.io/dex-authenticator/auth"
nginx.ingress.kubernetes.io/auth-signin: "http://demo-spa.127.0.0.1.sslip.io/dex-authenticator/sign_in"
nginx.ingress.kubernetes.io/auth-response-headers: "X-Auth-Request-User,X-Auth-Request-Email,X-Auth-Request-Groups"
spec:
ingressClassName: nginx
tls:
- hosts:
- demo-spa.127.0.0.1.sslip.io
secretName: demo-spa-tls
rules:
- host: demo-spa.127.0.0.1.sslip.io
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: demo-spa-service
port:
number: 80

15
k8s/namespace.yaml Normal file
View File

@ -0,0 +1,15 @@
apiVersion: v1
kind: Namespace
metadata:
name: demo-spa
labels:
name: demo-spa

25
k8s/service.yaml Normal file
View File

@ -0,0 +1,25 @@
apiVersion: v1
kind: Service
metadata:
name: demo-spa-service
namespace: demo-spa
labels:
app: demo-spa-app
spec:
selector:
app: demo-spa-app
ports:
- name: http
port: 80
targetPort: 80
protocol: TCP
type: ClusterIP

48
nginx.conf Normal file
View File

@ -0,0 +1,48 @@
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# Сжатие для лучшей производительности
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
# Кэширование статических ресурсов
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Для SPA - все маршруты направляем на index.html
location / {
try_files $uri $uri/ /index.html;
# Заголовки безопасности
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
}
# API endpoint для получения информации о пользователе
location /api/user {
# В реальном приложении здесь будет проксирование к backend
# Для демо возвращаем информацию из заголовков OAuth2 proxy
add_header Content-Type application/json;
return 200 '{"name": "Демо Пользователь", "email": "demo@example.com"}';
}
# Health check endpoint
location /health {
add_header Content-Type application/json;
return 200 '{"status": "ok"}';
}
}

1293
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

29
package.json Normal file
View File

@ -0,0 +1,29 @@
{
"name": "demo-spa-app",
"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"
},
"devDependencies": {
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@vitejs/plugin-react": "^4.0.0",
"vite": "^4.4.0"
}
}

126
src/App.jsx Normal file
View File

@ -0,0 +1,126 @@
import { useState, useEffect } from 'react'
function App() {
const [userName, setUserName] = useState('Гость')
const [isLoading, setIsLoading] = useState(true)
useEffect(() => {
// Получаем имя пользователя из заголовков (OAuth2 proxy передает)
const getUserInfo = async () => {
try {
// В реальном приложении OAuth2 proxy передает информацию в заголовках
// Для демо используем заголовок X-Forwarded-User или X-Remote-User
const response = await fetch('/api/user', {
headers: {
'Accept': 'application/json'
}
})
if (response.ok) {
const userData = await response.json()
setUserName(userData.name || userData.email || 'Пользователь')
}
} catch (error) {
console.log('Не удалось получить информацию о пользователе:', error)
// Fallback - используем заголовки напрямую
const remoteUser = document.cookie
.split('; ')
.find(row => row.startsWith('X-Remote-User='))
?.split('=')[1]
if (remoteUser) {
setUserName(decodeURIComponent(remoteUser))
}
} finally {
setIsLoading(false)
}
}
getUserInfo()
}, [])
if (isLoading) {
return (
<div style={styles.container}>
<div style={styles.loading}>Загрузка...</div>
</div>
)
}
return (
<div style={styles.container}>
<div style={styles.card}>
<h1 style={styles.title}>🎉 Добро пожаловать!</h1>
<p style={styles.greeting}>
Привет, <strong>{userName}</strong>!
</p>
<p style={styles.description}>
Это простое SPA приложение, развернутое в Kubernetes с аутентификацией через OAuth2.
</p>
<div style={styles.info}>
<p> Аутентификация через OAuth2 Proxy</p>
<p> Развертывание в Kubernetes</p>
<p> Принцип KISS</p>
</div>
</div>
</div>
)
}
const styles = {
container: {
minHeight: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#f5f5f5',
fontFamily: 'system-ui, -apple-system, sans-serif',
margin: 0,
padding: '20px'
},
card: {
backgroundColor: 'white',
borderRadius: '12px',
padding: '40px',
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
textAlign: 'center',
maxWidth: '500px',
width: '100%'
},
title: {
color: '#333',
marginBottom: '20px',
fontSize: '2.5rem'
},
greeting: {
fontSize: '1.5rem',
color: '#666',
marginBottom: '20px'
},
description: {
color: '#888',
lineHeight: '1.6',
marginBottom: '30px'
},
info: {
backgroundColor: '#f8f9fa',
borderRadius: '8px',
padding: '20px',
textAlign: 'left'
},
loading: {
fontSize: '1.2rem',
color: '#666'
}
}
export default App

18
src/main.jsx Normal file
View File

@ -0,0 +1,18 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)

19
vite.config.js Normal file
View File

@ -0,0 +1,19 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
build: {
outDir: 'dist',
assetsDir: 'assets'
}
})