diff --git a/webapp/README.md b/webapp/README.md new file mode 100644 index 0000000..9d6cf67 --- /dev/null +++ b/webapp/README.md @@ -0,0 +1,332 @@ +## Project Structure +Web Interface for generating Kubernetes diagrams from manifests, Helm charts, or Helmfile files using Kubediagrams. +# KubeDiagrams Web App +A modern web interface for generating Kubernetes architecture diagrams from manifests, Helm charts, or Helmfile configurations using [KubeDiagrams](https://github.com/philippemerle/KubeDiagrams). +## ✨ Features +- **Multiple Input Types**: Support for Kubernetes manifests, Helm charts, and Helmfile configurations +- **Flexible Output Formats**: Generate diagrams in PNG, SVG, PDF, DOT, and interactive HTML +- **Interactive Viewer**: Explore diagrams with an interactive web viewer +- **Built-in Examples**: Pre-loaded examples for quick testing +- **History Management**: Keep track of your diagram generations +- **Docker Support**: Easy deployment with Docker and Docker Compose +- **Access Logging**: Apache Combined Log format compatible with GoAccess +--- + +## Quick Start +### Using Docker Compose (Recommended) +1. **Clone the repository** +```bash +git clone +cd kubediagramswebui +``` +2. **Start the application** +```bash +docker-compose up -d +``` +3. **Access the application** +- Open your browser and navigate to `http://localhost:8080` +That's it! The application is now running with both frontend and backend services. +### Stopping the application +```bash +docker-compose down +``` +--- + +## Prerequisites +### Server-Side Tools (Required) (Include in requirements.txt) +The following command-line tools must be installed and available in your PATH: +- **`kube-diagrams/helm-diagrams`** - For generating diagrams from Kubernetes manifests and for generating diagrams from Helm charts ([Installation](https://github.com/philippemerle/KubeDiagrams)) +- **`helmfile`** - Required only for Helmfile tab functionality ([Installation](https://github.com/helmfile/helmfile)) +### Docker Deployment +- Docker Engine 20.10+ +- Docker Compose +### Manual Deployment +- **Backend**: Python 3.8+, pip, venv +- **Frontend**: Node.js 18+, npm +--- + +## Project Structure +``` +webapp/ +├── backend/ # Python/Flask backend +│ ├── routes/ # Flask route handlers +│ │ ├── manifest.py # Manifest diagram generation endpoints +│ │ ├── helm.py # Helm chart diagram endpoints +│ │ ├── helmfile.py # Helmfile diagram endpoints +│ │ └── submit.py # Feedback submission endpoint +│ ├── services/ # Business logic layer +│ │ ├── manifestService.py # Manifest processing service +│ │ ├── helmService.py # Helm processing service +│ │ ├── helmfileService.py # Helmfile processing service +│ │ ├── file_manager.py # File operations manager +│ │ └── models.py # Data models +│ ├── utils/ # Utility modules +│ │ ├── access_logger.py # Request logging +│ │ ├── logger.py # General logging +│ │ ├── response_builder.py # API response builder +│ │ └── validators.py # Input validation +│ ├── app.py # Flask application entry point +│ ├── config.py # Configuration settings +│ ├── constants.py # Global constants +│ ├── requirements.txt # Python dependencies +│ ├── Dockerfile # Backend Docker image +│ └── logs/ # Log files (auto-rotation) +│ +├── frontend/ # React/Vite frontend +│ ├── src/ +│ │ ├── components/ # React components +│ │ │ ├── common/ # Reusable UI components +│ │ │ ├── options/ # Diagram configuration components +│ │ │ └── tabs/ # Main tab components +│ │ │ ├── ManifestTab/ # Kubernetes manifest tab +│ │ │ ├── HelmTab/ # Helm chart tab +│ │ │ ├── HelmFileTab/ # Helmfile tab +│ │ │ └── InteractiveViewerTab/ # Interactive viewer +│ │ ├── examples/ # Example registry +│ │ ├── hooks/ # Custom React hooks +│ │ ├── services/ # API client services +│ │ ├── utils/ # Utility functions +│ │ ├── App.jsx # Main application component +│ │ └── main.jsx # Application entry point +│ ├── public/ +│ │ ├── examples/ # Example YAML files +│ │ │ ├── manifests/ # Manifest examples +│ │ │ └── helmfiles/ # Helmfile examples +│ │ └── interactive_viewer/ # Interactive viewer assets +│ ├── package.json # NPM dependencies +│ ├── vite.config.js # Vite configuration +│ ├── apache.conf # Apache reverse proxy config +│ └── Dockerfile # Frontend Docker image +│ +└── docker-compose.yml # Docker Compose orchestration +``` +--- +## Manual Installation (Without Docker) +### Backend Setup +1. **Navigate to backend directory** +```bash +cd backend +``` +2. **Create and activate virtual environment** +```bash +python3 -m venv venv +source venv/bin/activate # On Windows: venv\Scripts\activate +``` +3. **Install dependencies** +```bash +pip install -r requirements.txt +``` +4. **Start the Flask server** +```bash +python3 app.py +``` +The backend server will be available at `http://localhost:5000` +### Frontend Setup +1. **Navigate to frontend directory** +```bash +cd frontend +``` +2. **Install dependencies** +```bash +npm install +``` +or +```bash +npm ci +``` +3. **Start the development server** +```bash +npm run dev +``` +The frontend will be available at `http://localhost:5173` + +--- +## Docker Deployment +### Architecture +The Docker Compose setup consists of two services: +- **Frontend**: Apache2 server serving React static files and acting as reverse proxy to the backend +- **Backend**: Python/Flask application with Gunicorn WSGI server running KubeDiagrams +### Configuration +#### Environment Variables +**Backend** (`backend/Dockerfile` or `docker-compose.yml`): +- `FLASK_ENV`: Set to `production` for production deployment +- `BEHIND_PROXY`: Set to `true` when running behind a reverse proxy (default: `true`) +- `PROXY_X_FOR`: Number of trusted proxies (default: `1`) +**Frontend** (`docker-compose.yml`): +- `BACKEND_URL`: Backend service URL (default: `http://backend:5000`) +#### Ports +- **Frontend**: Port `8080` (mapped to container port `80`) +- **Backend**: Port `5000` (internal only, accessed via frontend proxy) +### Building and Running +**Build and start services**: +```bash +docker-compose up --build +``` +**Start in detached mode**: +```bash +docker-compose up -d +``` +**View logs**: +```bash +docker-compose logs -f +``` +**Stop services**: +```bash +docker-compose down +``` +### Health Checks +The backend includes a health check endpoint at `/api/health` that Docker uses to monitor service health. + +--- +## Logging +The Flask backend generates detailed access logs in **Apache Combined Log format**, compatible with tools like **GoAccess** for analysis. +### Log Location +- **Docker**: `./backend/logs/access.log` (persisted via volume mount) +- **Manual**: `backend/logs/access.log` +### Log Format +``` +IP - - [datetime] "METHOD PATH PROTOCOL" STATUS SIZE "REFERER" "USER-AGENT" TIME_MS +``` +### Log Rotation +- **Frequency**: Daily at midnight +- **Retention**: 30 days +- **Format**: `access.log.YYYY-MM-DD` +### IP Address Detection +When running behind a reverse proxy (Apache, Nginx), the backend correctly identifies client IPs using `X-Forwarded-For` headers. +**Debug endpoint**: `GET /api/debug/ip` - Returns IP detection information +--- +## Usage Guide + +### 1. Kubernetes Manifest Tab +Generate diagrams from raw Kubernetes YAML manifests. +**Steps**: +1. Select the "Manifest" tab +2. Paste your Kubernetes YAML or load an example +3. Configure diagram options (format, orientation, colors, etc.) +4. Click "Generate Diagram" +5. Download the generated diagram or view it interactively +**Supported Resources**: Deployments, StatefulSets, Services, Ingresses, ConfigMaps, Secrets, PVCs, and more. + +### 2. Helm Chart Tab +Generate diagrams from Helm chart URLs. +**Steps**: +1. Select the "Helm" tab +2. Enter the Helm chart URL (e.g., `https://charts.bitnami.com/bitnami/wordpress`) +3. Optionally provide a values file +4. Configure diagram options +5. Click "Generate Diagram" +**Note**: Supports both public Helm repositories and OCI registries. + +### 3. Helmfile Tab +Generate diagrams from Helmfile configurations. +**Steps**: +1. Select the "Helmfile" tab +2. Paste your Helmfile YAML or load an example +3. Configure diagram options +4. Click "Generate Diagram" +**Requirement**: `helmfile` must be installed on the server. + +### 4. Interactive Viewer +View diagrams in an interactive HTML viewer with zoom, pan, and search capabilities. +**Features**: +- Search nodes by name +- Highlight nodes on hover +- Pan and zoom controls +- Responsive design +--- +## ️ Configuration Options +### Diagram Options +- **Format**: PNG, SVG, PDF, DOT, Interactive HTML +- **CLI Arguments**: Optional custom parameters passed to the underlying KubeDiagrams tools +- **Without Namespace**: Option to generate diagrams without namespace grouping +- **Feedback System**: Rate diagrams (1-5 stars) and provide comments for improvement + +### Available via CLI Arguments +The CLI arguments field allows you to pass additional parameters to customize diagram generation. Refer to [KubeDiagrams documentation](https://github.com/cloudogu/kubediagrams) for available options. + +--- +## Examples +The application includes built-in examples for quick testing: +### Manifest Examples +- **Redis StatefulSet**: Redis cluster with persistent storage +- **Microservices**: Multi-service application with ingress +- **WordPress**: WordPress with MySQL database +- **Cassandra**: Cassandra StatefulSet with headless service +### Helmfile Examples +- **Monitoring Stack**: Prometheus, Grafana, and AlertManager + +### Adding Custom Examples +See `frontend/public/examples/README.md` for instructions on adding new examples. + +--- +## API Endpoints +### Backend API +- `POST /api/manifest/generate` - Generate diagram from manifest +- `POST /api/helm/generate` - Generate diagram from Helm chart +- `POST /api/helmfile/generate` - Generate diagram from Helmfile +- `POST /api/submit` - Submit feedback + +--- +## Troubleshooting +### Common Issues +**Issue**: "Command not found: kube-diagrams" +- **Solution**: Ensure KubeDiagrams is installed and in your PATH +**Issue**: Diagram generation fails with Helm charts +- **Solution**: Verify `helm-diagrams` is installed and the chart URL is accessible +**Issue**: Frontend cannot connect to backend +- **Solution**: Check that backend is running and CORS is properly configured +**Issue**: IP addresses showing as proxy IP instead of client IP +- **Solution**: Verify `BEHIND_PROXY=true` and `PROXY_X_FOR` is set correctly + +### Debug Mode +Enable debug mode by setting `DEBUG=True` in `backend/config.py` for more detailed error messages. + +--- +## Testing +### Quick Test +1. Start both backend and frontend servers +2. Navigate to `http://localhost:8080` (Docker) or `http://localhost:5173` (manual) +3. Select the "Manifest" tab +4. Load an example (e.g., "Redis StatefulSet") +5. Click "Generate Diagram" +6. Verify the diagram is generated and can be downloaded +--- +## Development +### Frontend Development +```bash +cd frontend +npm run dev # Start development server +npm run build # Build for production +npm run lint # Run ESLint +npm run format # Format code with Prettier +``` +### Backend Development +```bash +cd backend +source venv/bin/activate +python3 app.py # Start Flask server in debug mode +``` +### Technologies Used +**Backend**: +- Flask 3.1.2 - Web framework +- Gunicorn 23.0.0 - WSGI server +- KubeDiagrams 0.6.0 - Diagram generation +- PyYAML 6.0.2 - YAML parsing +**Frontend**: +- React 19.1.0 - UI framework +- Vite 6.3.5 - Build tool +- TailwindCSS 4.1.6 - Styling +- Lucide React - Icons +- Motion - Animations +--- + +## Contributing +Contributions are welcome! Please feel free to submit a Pull Request. + +--- +## Support +For issues and questions, please open an issue on the GitHub repository. + +--- +## 🙏 Acknowledgments +- [Graphviz](https://graphviz.org/) - Graph visualization software diff --git a/webapp/backend/.dockerignore b/webapp/backend/.dockerignore new file mode 100644 index 0000000..dbced50 --- /dev/null +++ b/webapp/backend/.dockerignore @@ -0,0 +1,36 @@ +# Virtual environment +venv/ +.venv/ +env/ + +# Python cache +__pycache__/ +*.py[cod] +*$py.class +*.so + +# Logs (will be mounted as volume) +logs/*.log +logs/*.csv + +# Development files +.git +.gitignore +*.md + +# IDE +.vscode/ +.idea/ + +# Test files +test-*.png +test-*.svg +*.test.py + +# OS files +.DS_Store +Thumbs.db + +# Temporary files +*.tmp +*.bak diff --git a/webapp/backend/Dockerfile b/webapp/backend/Dockerfile new file mode 100644 index 0000000..e425d8e --- /dev/null +++ b/webapp/backend/Dockerfile @@ -0,0 +1,63 @@ +# ============================================================================= +# KubeDiagrams Backend - Dockerfile +# ============================================================================= +# Base image from KubeDiagrams project (Philippe Merle) +# Includes: Helm, Graphviz, Python, PyYAML, diagrams +# ============================================================================= + +# --- Stage 1: Get Helm binary --- +FROM docker.io/alpine/helm:3 AS helm + +# --- Stage 2: Build backend --- +FROM docker.io/python:3.13-alpine AS base + +# Install system dependencies +RUN apk update && apk add --no-cache \ + graphviz \ + bash \ + ttf-freefont \ + curl \ + && rm -rf /var/cache/apk/* + +# Copy Helm from first stage +COPY --from=helm /usr/bin/helm /usr/local/bin/helm + +# Install Helmfile from GitHub releases (Alpine/musl version) +ARG HELMFILE_VERSION=1.2.3 +RUN curl -fsSL -o /tmp/helmfile.tar.gz \ + "https://github.com/helmfile/helmfile/releases/download/v${HELMFILE_VERSION}/helmfile_${HELMFILE_VERSION}_linux_386.tar.gz" && \ + tar -xzf /tmp/helmfile.tar.gz -C /usr/local/bin helmfile && \ + chmod +x /usr/local/bin/helmfile && \ + rm /tmp/helmfile.tar.gz + +# Set working directory +WORKDIR /app + +# Copy requirements first (for Docker layer caching) +COPY requirements.txt . + +# Install Python dependencies +RUN pip install --no-cache-dir --upgrade pip && \ + pip install --no-cache-dir -r requirements.txt + +# Copy backend application code +COPY . . + +# Create logs directory +RUN mkdir -p logs && chmod 755 logs + +# Environment variables +ENV FLASK_APP=app.py +ENV FLASK_ENV=production +ENV PYTHONUNBUFFERED=1 +ENV BEHIND_PROXY=true +ENV PROXY_X_FOR=1 + +# Expose port +EXPOSE 5000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \ + CMD curl --fail --silent http://localhost:5000/api/health || exit 1 + +CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "4", "--timeout", "120", "--access-logfile", "-", "--error-logfile", "-", "app:app"] diff --git a/webapp/backend/__pycache__/app.cpython-312.pyc b/webapp/backend/__pycache__/app.cpython-312.pyc new file mode 100644 index 0000000..03587d7 Binary files /dev/null and b/webapp/backend/__pycache__/app.cpython-312.pyc differ diff --git a/webapp/backend/__pycache__/config.cpython-312.pyc b/webapp/backend/__pycache__/config.cpython-312.pyc new file mode 100644 index 0000000..3b7decb Binary files /dev/null and b/webapp/backend/__pycache__/config.cpython-312.pyc differ diff --git a/webapp/backend/__pycache__/constants.cpython-312.pyc b/webapp/backend/__pycache__/constants.cpython-312.pyc new file mode 100644 index 0000000..e01e6bc Binary files /dev/null and b/webapp/backend/__pycache__/constants.cpython-312.pyc differ diff --git a/webapp/backend/app.py b/webapp/backend/app.py new file mode 100644 index 0000000..99919ba --- /dev/null +++ b/webapp/backend/app.py @@ -0,0 +1,112 @@ +"""Flask Application for KubeDiagrams Web UI.""" +from flask import Flask, g, request, jsonify +from flask_cors import CORS +from werkzeug.middleware.proxy_fix import ProxyFix +from config import Config, setup_logging +from routes.manifest import manifest_bp +from routes.helm import helm_bp +from routes.helmfile import helmfile_bp +from routes.submit import submit_bp +from utils.access_logger import log_request, get_real_ip, get_all_ip_headers +from time import time + + +def create_app(): + app = Flask(__name__) + + # Health check endpoint + @app.route('/api/health', methods=['GET']) + def health_check(): + """Health check endpoint for monitoring and testing.""" + return jsonify({ + 'status': 'healthy', + 'service': 'kubediagrams-backend', + 'version': '1.0.0' + }), 200 + + # Debug endpoint for IP testing + @app.route('/api/debug/ip', methods=['GET']) + def debug_ip(): + """ + Debug endpoint to check IP detection. + Returns all IP-related information for debugging proxy configuration. + """ + return jsonify({ + 'detected_ip': get_real_ip(), + 'remote_addr': request.remote_addr, + 'headers': get_all_ip_headers(), + 'behind_proxy': Config.BEHIND_PROXY, + 'proxy_x_for': Config.PROXY_X_FOR + }), 200 + + # Proxy Configuration + # When running behind a reverse proxy (nginx, Apache, HAProxy), the proxy + # forwards requests to the application. By default, Flask sees the proxy's IP + # instead of the real client IP. ProxyFix corrects this by reading the + # X-Forwarded-* headers that the proxy sets. + if Config.BEHIND_PROXY: + app.wsgi_app = ProxyFix( + app.wsgi_app, + x_for=Config.PROXY_X_FOR, # Number of proxies setting X-Forwarded-For + x_proto=Config.PROXY_X_PROTO, # Number of proxies setting X-Forwarded-Proto + x_host=Config.PROXY_X_HOST, # Number of proxies setting X-Forwarded-Host + x_prefix=Config.PROXY_X_PREFIX # Number of proxies setting X-Forwarded-Prefix + ) + + # CORS Configuration + if Config.CORS_ENABLED: + CORS(app) + + # Logging Configuration + setup_logging() + + # Middleware pour logger les requêtes + @app.before_request + def before_request(): + """Save request start time for performance logging.""" + g.request_start_time = time() + + @app.after_request + def after_request(response): + """Log request in Apache Combined Log Format.""" + # Skip logging for health checks to avoid log spam + if request.path == '/api/health': + return response + + # Get response size + response_size = response.calculate_content_length() or 0 + + # Log the request + log_request( + status_code=response.status_code, + response_size=response_size + ) + + return response + + # Blueprints registration + app.register_blueprint(manifest_bp) + app.register_blueprint(helm_bp) + app.register_blueprint(helmfile_bp) + app.register_blueprint(submit_bp) + + return app + + +app = create_app() # App Initialization + +if __name__ == "__main__": + print("=" * 50) + print("Flask Application Configuration") + print("=" * 50) + print(f"BEHIND_PROXY: {Config.BEHIND_PROXY}") + print(f"PROXY_X_FOR: {Config.PROXY_X_FOR}") + print(f"PROXY_X_PROTO: {Config.PROXY_X_PROTO}") + print(f"PROXY_X_HOST: {Config.PROXY_X_HOST}") + print(f"PROXY_X_PREFIX: {Config.PROXY_X_PREFIX}") + print(f"DEBUG: {Config.DEBUG}") + print(f"PORT: {Config.PORT}") + print(f"HOST: {Config.HOST}") + print("=" * 50) + print("") + app.run(port=Config.PORT, debug=Config.DEBUG) diff --git a/webapp/backend/config.py b/webapp/backend/config.py new file mode 100644 index 0000000..d9a8de2 --- /dev/null +++ b/webapp/backend/config.py @@ -0,0 +1,95 @@ +"""Configuration Flask application.""" +import os +import logging +from logging.handlers import TimedRotatingFileHandler +from datetime import datetime, time as dt_time + +class Config: + """Config App and Logger.""" + + # Flask + DEBUG = True + PORT = 5000 + HOST = 'localhost' + + # CORS + CORS_ENABLED = True + + # Proxy Configuration + # Set to True when running behind a reverse proxy (nginx, Apache, etc.) + # This enables proper forwarding of client IP addresses + BEHIND_PROXY = os.environ.get('BEHIND_PROXY', 'false').lower() == 'true' + # Number of trusted proxies in front of the application + # X-Forwarded-For: client, proxy1, proxy2 -> set to 2 if you have 2 proxies + PROXY_X_FOR = int(os.environ.get('PROXY_X_FOR', '1')) + PROXY_X_PROTO = int(os.environ.get('PROXY_X_PROTO', '1')) + PROXY_X_HOST = int(os.environ.get('PROXY_X_HOST', '1')) + PROXY_X_PREFIX = int(os.environ.get('PROXY_X_PREFIX', '1')) + + LOG_DIR = "logs" + LOG_FILE = "access.log" + LOG_FORMAT = '%(message)s' + LOG_LEVEL = logging.INFO + LOG_ROTATION = "midnight" + LOG_INTERVAL = 1 + LOG_BACKUP_COUNT = 30 + LOG_ENCODING = 'utf-8' + LOG_SUFFIX = "%Y-%m-%d" + LOGGER_NAME = "access_logger" + # Disable automatic rotation based on file modification time + # This prevents creating files with old dates + LOG_USE_UTC = False + # Fichiers + FEEDBACK_FILE = "feedback.txt" + + @classmethod + def get_log_path(cls): + """Return path of the logFile.""" + return os.path.join(cls.LOG_DIR, cls.LOG_FILE) + + +def setup_logging(): + """Config log system with proper rotation handling.""" + # Create a logs directory if it doesn't exist + os.makedirs(Config.LOG_DIR, exist_ok=True) + + # Create a formatter + log_formatter = logging.Formatter(Config.LOG_FORMAT) + + log_path = Config.get_log_path() + + # Create a handler with rotation + log_handler = TimedRotatingFileHandler( + log_path, + when=Config.LOG_ROTATION, + interval=Config.LOG_INTERVAL, + backupCount=Config.LOG_BACKUP_COUNT, + encoding=Config.LOG_ENCODING, + utc=Config.LOG_USE_UTC + ) + log_handler.setFormatter(log_formatter) + log_handler.suffix = Config.LOG_SUFFIX + + current_time = datetime.now() + # Calculate next midnight + next_midnight = datetime.combine( + current_time.date(), + dt_time(0, 0, 0) + ) + if current_time >= next_midnight: + # If we're past midnight today, rollover at next midnight + from datetime import timedelta + next_midnight = next_midnight + timedelta(days=1) + + # Set the rollover time (in seconds since epoch) + log_handler.rolloverAt = next_midnight.timestamp() + + # Config logger + logger = logging.getLogger(Config.LOGGER_NAME) + logger.setLevel(Config.LOG_LEVEL) + + # Remove existing handlers to avoid duplicates + logger.handlers.clear() + logger.addHandler(log_handler) + + return logger \ No newline at end of file diff --git a/webapp/backend/constants.py b/webapp/backend/constants.py new file mode 100644 index 0000000..4801fe8 --- /dev/null +++ b/webapp/backend/constants.py @@ -0,0 +1,23 @@ +"""Constantes of the backend""" +import re +# Different format of mime_types +MIME_TYPES = { + "png": "image/png", + "jpg": "image/jpeg", + "jpeg": "image/jpeg", + "gif": "image/gif", + "svg": "image/svg+xml", + "pdf": "application/pdf", + "dot": "text/vnd.graphviz", + "dot_json": "application/json" +} +# no binary format +TEXT_FORMATS = {"svg", "dot", "dot_json"} +# Manifest_detector +MANIFEST_RE = re.compile(r'^\s*apiVersion\s*:\s*.+$', re.MULTILINE) +KIND_RE = re.compile(r'^\s*kind\s*:\s*.+$', re.MULTILINE) +# Max log length +MAX_LOG_LENGTH = 999999 +# file extensions +YAML_EXTENSIONS = ['.yaml', '.yml'] +TGZ_EXTENSIONS = ['.tgz', '.tar.gz'] diff --git a/webapp/backend/feedback.txt b/webapp/backend/feedback.txt new file mode 100644 index 0000000..350caa0 --- /dev/null +++ b/webapp/backend/feedback.txt @@ -0,0 +1,8 @@ +[MANIFEST] +Note: 0 +Commentaire: test + +[HELMFILE] +Note: 5 +Commentaire: ttt + diff --git a/webapp/backend/logs/access.log b/webapp/backend/logs/access.log new file mode 100644 index 0000000..8b8d04a --- /dev/null +++ b/webapp/backend/logs/access.log @@ -0,0 +1,24 @@ +172.20.0.1 - - [27/Jan/2026:10:12:34 +0000] "POST /api/generate-diagram HTTP/1.1" 200 79665 "http://localhost:8080/" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:147.0) Gecko/20100101 Firefox/147.0" 608.50ms +172.20.0.1 - - [27/Jan/2026:10:18:03 +0000] "POST /api/generate-diagram HTTP/1.1" 200 6554 "http://localhost:8080/" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:147.0) Gecko/20100101 Firefox/147.0" 579.44ms +172.20.0.1 - - [27/Jan/2026:10:19:27 +0000] "GET /api/debug/ip HTTP/1.1" 200 278 "-" "curl/8.5.0" 0.74ms +192.168.1.59 - - [27/Jan/2026:10:20:59 +0000] "POST /api/generate-diagram HTTP/1.1" 200 91057 "http://192.168.1.123:8080/" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Mobile Safari/537.36" 476.36ms +192.168.1.123 - - [27/Jan/2026:10:21:23 +0000] "POST /api/generate-helmfile-diagram HTTP/1.1" 200 707287 "http://192.168.1.123:8080/" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:147.0) Gecko/20100101 Firefox/147.0" 10285.02ms +10.201.9.161 - - [27/Jan/2026:12:18:52 +0000] "POST /api/generate-helm-diagram HTTP/1.1" 200 201474 "http://10.201.9.161:8080/" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:147.0) Gecko/20100101 Firefox/147.0" 3347.54ms +10.201.8.211 - - [27/Jan/2026:12:22:49 +0000] "POST /api/generate-diagram HTTP/1.1" 200 79665 "http://10.201.9.161:8080/" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Mobile Safari/537.36" 451.44ms +172.20.0.1 - - [27/Jan/2026:12:24:03 +0000] "POST /api/generate-helmfile-diagram HTTP/1.1" 200 707287 "http://localhost:8080/" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:147.0) Gecko/20100101 Firefox/147.0" 8655.16ms +10.201.9.161 - - [27/Jan/2026:12:24:09 +0000] "POST /api/generate-helm-diagram HTTP/1.1" 200 201474 "http://10.201.9.161:8080/" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:147.0) Gecko/20100101 Firefox/147.0" 3420.22ms +10.201.8.211 - - [27/Jan/2026:12:24:59 +0000] "POST /api/generate-helm-diagram HTTP/1.1" 200 114224 "http://10.201.9.161:8080/" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Mobile Safari/537.36" 1229.77ms +10.201.9.161 - - [27/Jan/2026:12:25:01 +0000] "POST /api/generate-helm-diagram HTTP/1.1" 400 2469 "http://10.201.9.161:8080/" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:147.0) Gecko/20100101 Firefox/147.0" 2978.06ms +172.20.0.1 - - [27/Jan/2026:12:25:15 +0000] "POST /api/generate-helm-diagram HTTP/1.1" 200 154735 "http://localhost:8080/" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:147.0) Gecko/20100101 Firefox/147.0" 13762.43ms +172.20.0.1 - - [27/Jan/2026:12:51:51 +0000] "POST /api/generate-diagram HTTP/1.1" 200 6413 "http://localhost:8080/" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:147.0) Gecko/20100101 Firefox/147.0" 490.70ms +172.20.0.1 - - [27/Jan/2026:12:52:42 +0000] "POST /api/generate-diagram HTTP/1.1" 200 79665 "http://localhost:8080/" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:147.0) Gecko/20100101 Firefox/147.0" 519.98ms +172.20.0.1 - - [27/Jan/2026:12:53:53 +0000] "POST /api/generate-helmfile-diagram HTTP/1.1" 200 707287 "http://localhost:8080/" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:147.0) Gecko/20100101 Firefox/147.0" 8321.61ms +10.201.9.161 - - [27/Jan/2026:13:22:17 +0000] "POST /api/generate-diagram HTTP/1.1" 200 79665 "http://10.201.9.161:8080/" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:147.0) Gecko/20100101 Firefox/147.0" 561.96ms +10.201.9.161 - - [27/Jan/2026:13:24:12 +0000] "POST /api/generate-helmfile-diagram HTTP/1.1" 200 707287 "http://10.201.9.161:8080/" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:147.0) Gecko/20100101 Firefox/147.0" 8609.22ms +10.201.8.211 - - [27/Jan/2026:13:42:54 +0000] "POST /api/generate-diagram HTTP/1.1" 200 79665 "http://10.201.9.161:8080/" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Mobile Safari/537.36" 452.02ms +10.201.9.161 - - [27/Jan/2026:13:50:05 +0000] "POST /api/generate-diagram HTTP/1.1" 200 93003 "http://10.201.9.161:8080/" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:147.0) Gecko/20100101 Firefox/147.0" 646.66ms +10.201.9.161 - - [27/Jan/2026:13:51:25 +0000] "POST /api/generate-helm-diagram HTTP/1.1" 200 2797664 "http://10.201.9.161:8080/" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:147.0) Gecko/20100101 Firefox/147.0" 15009.67ms +10.201.9.161 - - [27/Jan/2026:14:28:46 +0000] "POST /api/generate-diagram HTTP/1.1" 200 92580 "http://10.201.9.161:8080/" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:147.0) Gecko/20100101 Firefox/147.0" 619.55ms +10.201.9.161 - - [27/Jan/2026:14:29:00 +0000] "POST /api/generate-diagram HTTP/1.1" 200 92580 "http://10.201.9.161:8080/" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:147.0) Gecko/20100101 Firefox/147.0" 614.89ms +10.201.9.161 - - [27/Jan/2026:14:29:22 +0000] "POST /api/generate-diagram HTTP/1.1" 200 83194 "http://10.201.9.161:8080/" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:147.0) Gecko/20100101 Firefox/147.0" 536.01ms +10.201.9.161 - - [27/Jan/2026:14:29:36 +0000] "POST /api/generate-diagram HTTP/1.1" 400 1269 "http://10.201.9.161:8080/" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:147.0) Gecko/20100101 Firefox/147.0" 521.76ms diff --git a/webapp/backend/logs/logs.csv b/webapp/backend/logs/logs.csv new file mode 100644 index 0000000..b7c5962 --- /dev/null +++ b/webapp/backend/logs/logs.csv @@ -0,0 +1,121 @@ +2025-11-28 10:46:59,647 ; 127.0.0.1 ; /api/generate-diagram ; format=png;extraArgs=;withoutNamespace=False;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd +2025-11-28 10:47:17,174 ; 127.0.0.1 ; /api/generate-helm-diagram ; format=png;chartUrl=oci://ghcr.io/argoproj/argo-helm/argo-cd;extraArgs= +2025-11-28 10:47:37,037 ; 127.0.0.1 ; /api/generate-helmfile-diagram ; format=png;helmfile=;extraArgs=;withoutNamespace=False +2025-11-28 10:48:04,593 ; 127.0.0.1 ; /api/generate-diagram ; format=png;extraArgs=;withoutNamespace=False;manifest=apiVersion: v1\nkind: Service\nmetadata:\n name: wordpress-mysql\n labels:\n app: wordpress\nspec:\n ports:\n - port: 3306\n selector:\n app: wordpress\n tier: mysql\n clusterIP: None\n---\napiVersion: v1\nkind: PersistentVolumeClaim\nmetadata:\n name: mysql-pv-claim\n labels:\n app: wordpress\nspec:\n accessModes:\n - ReadWriteOnce\n resources:\n requests:\n storage: 512M # instead of 20Gi\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: wordpress-mysql\n labels:\n app: wordpress\nspec:\n selector:\n matchLabels:\n app: wordpress\n tier: mysql\n strategy:\n type: Recreate\n template:\n metadata:\n labels:\n app: wordpress\n tier: mysql\n spec:\n containers:\n - image: mysql:8.0\n name: mysql\n env:\n - name: MYSQL_ROOT_PASSWORD\n valueFrom:\n secretKeyRef:\n name: mysql-pass\n key: password\n - name: MYSQL_DATABASE\n value: wordpress\n - name: MYSQL_USER\n value: wordpress\n - name: MYSQL_PASSWORD\n valueFrom:\n secretKeyRef:\n name: mysql-pass\n key: password\n ports:\n - containerPort: 3306\n name: mysql\n volumeMounts:\n - name: mysql-persistent-storage\n mountPath: /var/lib/mysql\n volumes:\n - name: mysql-persistent-storage\n persistentVolumeClaim:\n claimName: mysql-pv-claim\n---\napiVersion: v1\nkind: Secret\nmetadata:\n name: mysql-pass\n labels:\n app: wordpress\ntype: Opaque\ndata:\n password: WU9VUl9QQVNTV09SRA==\n---\napiVersion: v1\nkind: Service\nmetadata:\n name: wordpress\n labels:\n app: wordpress\nspec:\n ports:\n - port: 80\n selector:\n app: wordpress\n tier: frontend\n type: LoadBalancer\n---\napiVersion: v1\nkind: PersistentVolumeClaim\nmetadata:\n name: wp-pv-claim\n labels:\n app: wordpress\nspec:\n accessModes:\n - ReadWriteOnce\n resources:\n requests:\n storage: 512M # instead of 20Gi\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: wordpress\n labels:\n app: wordpress\nspec:\n selector:\n matchLabels:\n app: wordpress\n tier: frontend\n strategy:\n type: Recreate\n template:\n metadata:\n labels:\n app: wordpress\n tier: frontend\n spec:\n containers:\n - image: wordpress:6.2.1-apache\n name: wordpress\n env:\n - name: WORDPRESS_DB_HOST\n value: wordpress-mysql\n - name: WORDPRESS_DB_PASSWORD\n valueFrom:\n secretKeyRef:\n name: mysql-pass\n key: password\n - name: WORDPRESS_DB_USER\n value: wordpress\n ports:\n - containerPort: 80\n name: wordpress\n volumeMounts:\n - name: wordpress-persistent-storage\n mountPath: /var/www/html\n volumes:\n - name: wordpress-persistent-storage\n persistentVolumeClaim:\n claimName: wp-pv-claim\n +2025-11-28 10:48:07,055 ; 127.0.0.1 ; /api/generate-diagram ; format=jpg;extraArgs=;withoutNamespace=False;manifest=apiVersion: v1\nkind: Service\nmetadata:\n name: wordpress-mysql\n labels:\n app: wordpress\nspec:\n ports:\n - port: 3306\n selector:\n app: wordpress\n tier: mysql\n clusterIP: None\n---\napiVersion: v1\nkind: PersistentVolumeClaim\nmetadata:\n name: mysql-pv-claim\n labels:\n app: wordpress\nspec:\n accessModes:\n - ReadWriteOnce\n resources:\n requests:\n storage: 512M # instead of 20Gi\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: wordpress-mysql\n labels:\n app: wordpress\nspec:\n selector:\n matchLabels:\n app: wordpress\n tier: mysql\n strategy:\n type: Recreate\n template:\n metadata:\n labels:\n app: wordpress\n tier: mysql\n spec:\n containers:\n - image: mysql:8.0\n name: mysql\n env:\n - name: MYSQL_ROOT_PASSWORD\n valueFrom:\n secretKeyRef:\n name: mysql-pass\n key: password\n - name: MYSQL_DATABASE\n value: wordpress\n - name: MYSQL_USER\n value: wordpress\n - name: MYSQL_PASSWORD\n valueFrom:\n secretKeyRef:\n name: mysql-pass\n key: password\n ports:\n - containerPort: 3306\n name: mysql\n volumeMounts:\n - name: mysql-persistent-storage\n mountPath: /var/lib/mysql\n volumes:\n - name: mysql-persistent-storage\n persistentVolumeClaim:\n claimName: mysql-pv-claim\n---\napiVersion: v1\nkind: Secret\nmetadata:\n name: mysql-pass\n labels:\n app: wordpress\ntype: Opaque\ndata:\n password: WU9VUl9QQVNTV09SRA==\n---\napiVersion: v1\nkind: Service\nmetadata:\n name: wordpress\n labels:\n app: wordpress\nspec:\n ports:\n - port: 80\n selector:\n app: wordpress\n tier: frontend\n type: LoadBalancer\n---\napiVersion: v1\nkind: PersistentVolumeClaim\nmetadata:\n name: wp-pv-claim\n labels:\n app: wordpress\nspec:\n accessModes:\n - ReadWriteOnce\n resources:\n requests:\n storage: 512M # instead of 20Gi\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: wordpress\n labels:\n app: wordpress\nspec:\n selector:\n matchLabels:\n app: wordpress\n tier: frontend\n strategy:\n type: Recreate\n template:\n metadata:\n labels:\n app: wordpress\n tier: frontend\n spec:\n containers:\n - image: wordpress:6.2.1-apache\n name: wordpress\n env:\n - name: WORDPRESS_DB_HOST\n value: wordpress-mysql\n - name: WORDPRESS_DB_PASSWORD\n valueFrom:\n secretKeyRef:\n name: mysql-pass\n key: password\n - name: WORDPRESS_DB_USER\n value: wordpress\n ports:\n - containerPort: 80\n name: wordpress\n volumeMounts:\n - name: wordpress-persistent-storage\n mountPath: /var/www/html\n volumes:\n - name: wordpress-persistent-storage\n persistentVolumeClaim:\n claimName: wp-pv-claim\n +2025-11-28 10:48:08,985 ; 127.0.0.1 ; /api/generate-diagram ; format=jpeg;extraArgs=;withoutNamespace=False;manifest=apiVersion: v1\nkind: Service\nmetadata:\n name: wordpress-mysql\n labels:\n app: wordpress\nspec:\n ports:\n - port: 3306\n selector:\n app: wordpress\n tier: mysql\n clusterIP: None\n---\napiVersion: v1\nkind: PersistentVolumeClaim\nmetadata:\n name: mysql-pv-claim\n labels:\n app: wordpress\nspec:\n accessModes:\n - ReadWriteOnce\n resources:\n requests:\n storage: 512M # instead of 20Gi\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: wordpress-mysql\n labels:\n app: wordpress\nspec:\n selector:\n matchLabels:\n app: wordpress\n tier: mysql\n strategy:\n type: Recreate\n template:\n metadata:\n labels:\n app: wordpress\n tier: mysql\n spec:\n containers:\n - image: mysql:8.0\n name: mysql\n env:\n - name: MYSQL_ROOT_PASSWORD\n valueFrom:\n secretKeyRef:\n name: mysql-pass\n key: password\n - name: MYSQL_DATABASE\n value: wordpress\n - name: MYSQL_USER\n value: wordpress\n - name: MYSQL_PASSWORD\n valueFrom:\n secretKeyRef:\n name: mysql-pass\n key: password\n ports:\n - containerPort: 3306\n name: mysql\n volumeMounts:\n - name: mysql-persistent-storage\n mountPath: /var/lib/mysql\n volumes:\n - name: mysql-persistent-storage\n persistentVolumeClaim:\n claimName: mysql-pv-claim\n---\napiVersion: v1\nkind: Secret\nmetadata:\n name: mysql-pass\n labels:\n app: wordpress\ntype: Opaque\ndata:\n password: WU9VUl9QQVNTV09SRA==\n---\napiVersion: v1\nkind: Service\nmetadata:\n name: wordpress\n labels:\n app: wordpress\nspec:\n ports:\n - port: 80\n selector:\n app: wordpress\n tier: frontend\n type: LoadBalancer\n---\napiVersion: v1\nkind: PersistentVolumeClaim\nmetadata:\n name: wp-pv-claim\n labels:\n app: wordpress\nspec:\n accessModes:\n - ReadWriteOnce\n resources:\n requests:\n storage: 512M # instead of 20Gi\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: wordpress\n labels:\n app: wordpress\nspec:\n selector:\n matchLabels:\n app: wordpress\n tier: frontend\n strategy:\n type: Recreate\n template:\n metadata:\n labels:\n app: wordpress\n tier: frontend\n spec:\n containers:\n - image: wordpress:6.2.1-apache\n name: wordpress\n env:\n - name: WORDPRESS_DB_HOST\n value: wordpress-mysql\n - name: WORDPRESS_DB_PASSWORD\n valueFrom:\n secretKeyRef:\n name: mysql-pass\n key: password\n - name: WORDPRESS_DB_USER\n value: wordpress\n ports:\n - containerPort: 80\n name: wordpress\n volumeMounts:\n - name: wordpress-persistent-storage\n mountPath: /var/www/html\n volumes:\n - name: wordpress-persistent-storage\n persistentVolumeClaim:\n claimName: wp-pv-claim\n +2025-11-28 10:48:10,977 ; 127.0.0.1 ; /api/generate-diagram ; format=svg;extraArgs=;withoutNamespace=False;manifest=apiVersion: v1\nkind: Service\nmetadata:\n name: wordpress-mysql\n labels:\n app: wordpress\nspec:\n ports:\n - port: 3306\n selector:\n app: wordpress\n tier: mysql\n clusterIP: None\n---\napiVersion: v1\nkind: PersistentVolumeClaim\nmetadata:\n name: mysql-pv-claim\n labels:\n app: wordpress\nspec:\n accessModes:\n - ReadWriteOnce\n resources:\n requests:\n storage: 512M # instead of 20Gi\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: wordpress-mysql\n labels:\n app: wordpress\nspec:\n selector:\n matchLabels:\n app: wordpress\n tier: mysql\n strategy:\n type: Recreate\n template:\n metadata:\n labels:\n app: wordpress\n tier: mysql\n spec:\n containers:\n - image: mysql:8.0\n name: mysql\n env:\n - name: MYSQL_ROOT_PASSWORD\n valueFrom:\n secretKeyRef:\n name: mysql-pass\n key: password\n - name: MYSQL_DATABASE\n value: wordpress\n - name: MYSQL_USER\n value: wordpress\n - name: MYSQL_PASSWORD\n valueFrom:\n secretKeyRef:\n name: mysql-pass\n key: password\n ports:\n - containerPort: 3306\n name: mysql\n volumeMounts:\n - name: mysql-persistent-storage\n mountPath: /var/lib/mysql\n volumes:\n - name: mysql-persistent-storage\n persistentVolumeClaim:\n claimName: mysql-pv-claim\n---\napiVersion: v1\nkind: Secret\nmetadata:\n name: mysql-pass\n labels:\n app: wordpress\ntype: Opaque\ndata:\n password: WU9VUl9QQVNTV09SRA==\n---\napiVersion: v1\nkind: Service\nmetadata:\n name: wordpress\n labels:\n app: wordpress\nspec:\n ports:\n - port: 80\n selector:\n app: wordpress\n tier: frontend\n type: LoadBalancer\n---\napiVersion: v1\nkind: PersistentVolumeClaim\nmetadata:\n name: wp-pv-claim\n labels:\n app: wordpress\nspec:\n accessModes:\n - ReadWriteOnce\n resources:\n requests:\n storage: 512M # instead of 20Gi\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: wordpress\n labels:\n app: wordpress\nspec:\n selector:\n matchLabels:\n app: wordpress\n tier: frontend\n strategy:\n type: Recreate\n template:\n metadata:\n labels:\n app: wordpress\n tier: frontend\n spec:\n containers:\n - image: wordpress:6.2.1-apache\n name: wordpress\n env:\n - name: WORDPRESS_DB_HOST\n value: wordpress-mysql\n - name: WORDPRESS_DB_PASSWORD\n valueFrom:\n secretKeyRef:\n name: mysql-pass\n key: password\n - name: WORDPRESS_DB_USER\n value: wordpress\n ports:\n - containerPort: 80\n name: wordpress\n volumeMounts:\n - name: wordpress-persistent-storage\n mountPath: /var/www/html\n volumes:\n - name: wordpress-persistent-storage\n persistentVolumeClaim:\n claimName: wp-pv-claim\n +2025-11-28 10:48:14,483 ; 127.0.0.1 ; /api/generate-diagram ; format=pdf;extraArgs=;withoutNamespace=False;manifest=apiVersion: v1\nkind: Service\nmetadata:\n name: wordpress-mysql\n labels:\n app: wordpress\nspec:\n ports:\n - port: 3306\n selector:\n app: wordpress\n tier: mysql\n clusterIP: None\n---\napiVersion: v1\nkind: PersistentVolumeClaim\nmetadata:\n name: mysql-pv-claim\n labels:\n app: wordpress\nspec:\n accessModes:\n - ReadWriteOnce\n resources:\n requests:\n storage: 512M # instead of 20Gi\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: wordpress-mysql\n labels:\n app: wordpress\nspec:\n selector:\n matchLabels:\n app: wordpress\n tier: mysql\n strategy:\n type: Recreate\n template:\n metadata:\n labels:\n app: wordpress\n tier: mysql\n spec:\n containers:\n - image: mysql:8.0\n name: mysql\n env:\n - name: MYSQL_ROOT_PASSWORD\n valueFrom:\n secretKeyRef:\n name: mysql-pass\n key: password\n - name: MYSQL_DATABASE\n value: wordpress\n - name: MYSQL_USER\n value: wordpress\n - name: MYSQL_PASSWORD\n valueFrom:\n secretKeyRef:\n name: mysql-pass\n key: password\n ports:\n - containerPort: 3306\n name: mysql\n volumeMounts:\n - name: mysql-persistent-storage\n mountPath: /var/lib/mysql\n volumes:\n - name: mysql-persistent-storage\n persistentVolumeClaim:\n claimName: mysql-pv-claim\n---\napiVersion: v1\nkind: Secret\nmetadata:\n name: mysql-pass\n labels:\n app: wordpress\ntype: Opaque\ndata:\n password: WU9VUl9QQVNTV09SRA==\n---\napiVersion: v1\nkind: Service\nmetadata:\n name: wordpress\n labels:\n app: wordpress\nspec:\n ports:\n - port: 80\n selector:\n app: wordpress\n tier: frontend\n type: LoadBalancer\n---\napiVersion: v1\nkind: PersistentVolumeClaim\nmetadata:\n name: wp-pv-claim\n labels:\n app: wordpress\nspec:\n accessModes:\n - ReadWriteOnce\n resources:\n requests:\n storage: 512M # instead of 20Gi\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: wordpress\n labels:\n app: wordpress\nspec:\n selector:\n matchLabels:\n app: wordpress\n tier: frontend\n strategy:\n type: Recreate\n template:\n metadata:\n labels:\n app: wordpress\n tier: frontend\n spec:\n containers:\n - image: wordpress:6.2.1-apache\n name: wordpress\n env:\n - name: WORDPRESS_DB_HOST\n value: wordpress-mysql\n - name: WORDPRESS_DB_PASSWORD\n valueFrom:\n secretKeyRef:\n name: mysql-pass\n key: password\n - name: WORDPRESS_DB_USER\n value: wordpress\n ports:\n - containerPort: 80\n name: wordpress\n volumeMounts:\n - name: wordpress-persistent-storage\n mountPath: /var/www/html\n volumes:\n - name: wordpress-persistent-storage\n persistentVolumeClaim:\n claimName: wp-pv-claim\n +2025-11-28 10:48:18,374 ; 127.0.0.1 ; /api/generate-diagram ; format=dot;extraArgs=;withoutNamespace=False;manifest=apiVersion: v1\nkind: Service\nmetadata:\n name: wordpress-mysql\n labels:\n app: wordpress\nspec:\n ports:\n - port: 3306\n selector:\n app: wordpress\n tier: mysql\n clusterIP: None\n---\napiVersion: v1\nkind: PersistentVolumeClaim\nmetadata:\n name: mysql-pv-claim\n labels:\n app: wordpress\nspec:\n accessModes:\n - ReadWriteOnce\n resources:\n requests:\n storage: 512M # instead of 20Gi\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: wordpress-mysql\n labels:\n app: wordpress\nspec:\n selector:\n matchLabels:\n app: wordpress\n tier: mysql\n strategy:\n type: Recreate\n template:\n metadata:\n labels:\n app: wordpress\n tier: mysql\n spec:\n containers:\n - image: mysql:8.0\n name: mysql\n env:\n - name: MYSQL_ROOT_PASSWORD\n valueFrom:\n secretKeyRef:\n name: mysql-pass\n key: password\n - name: MYSQL_DATABASE\n value: wordpress\n - name: MYSQL_USER\n value: wordpress\n - name: MYSQL_PASSWORD\n valueFrom:\n secretKeyRef:\n name: mysql-pass\n key: password\n ports:\n - containerPort: 3306\n name: mysql\n volumeMounts:\n - name: mysql-persistent-storage\n mountPath: /var/lib/mysql\n volumes:\n - name: mysql-persistent-storage\n persistentVolumeClaim:\n claimName: mysql-pv-claim\n---\napiVersion: v1\nkind: Secret\nmetadata:\n name: mysql-pass\n labels:\n app: wordpress\ntype: Opaque\ndata:\n password: WU9VUl9QQVNTV09SRA==\n---\napiVersion: v1\nkind: Service\nmetadata:\n name: wordpress\n labels:\n app: wordpress\nspec:\n ports:\n - port: 80\n selector:\n app: wordpress\n tier: frontend\n type: LoadBalancer\n---\napiVersion: v1\nkind: PersistentVolumeClaim\nmetadata:\n name: wp-pv-claim\n labels:\n app: wordpress\nspec:\n accessModes:\n - ReadWriteOnce\n resources:\n requests:\n storage: 512M # instead of 20Gi\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: wordpress\n labels:\n app: wordpress\nspec:\n selector:\n matchLabels:\n app: wordpress\n tier: frontend\n strategy:\n type: Recreate\n template:\n metadata:\n labels:\n app: wordpress\n tier: frontend\n spec:\n containers:\n - image: wordpress:6.2.1-apache\n name: wordpress\n env:\n - name: WORDPRESS_DB_HOST\n value: wordpress-mysql\n - name: WORDPRESS_DB_PASSWORD\n valueFrom:\n secretKeyRef:\n name: mysql-pass\n key: password\n - name: WORDPRESS_DB_USER\n value: wordpress\n ports:\n - containerPort: 80\n name: wordpress\n volumeMounts:\n - name: wordpress-persistent-storage\n mountPath: /var/www/html\n volumes:\n - name: wordpress-persistent-storage\n persistentVolumeClaim:\n claimName: wp-pv-claim\n +2025-11-28 10:48:25,333 ; 127.0.0.1 ; /api/generate-diagram ; format=dot_json;extraArgs=;withoutNamespace=False;manifest=apiVersion: v1\nkind: Service\nmetadata:\n name: wordpress-mysql\n labels:\n app: wordpress\nspec:\n ports:\n - port: 3306\n selector:\n app: wordpress\n tier: mysql\n clusterIP: None\n---\napiVersion: v1\nkind: PersistentVolumeClaim\nmetadata:\n name: mysql-pv-claim\n labels:\n app: wordpress\nspec:\n accessModes:\n - ReadWriteOnce\n resources:\n requests:\n storage: 512M # instead of 20Gi\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: wordpress-mysql\n labels:\n app: wordpress\nspec:\n selector:\n matchLabels:\n app: wordpress\n tier: mysql\n strategy:\n type: Recreate\n template:\n metadata:\n labels:\n app: wordpress\n tier: mysql\n spec:\n containers:\n - image: mysql:8.0\n name: mysql\n env:\n - name: MYSQL_ROOT_PASSWORD\n valueFrom:\n secretKeyRef:\n name: mysql-pass\n key: password\n - name: MYSQL_DATABASE\n value: wordpress\n - name: MYSQL_USER\n value: wordpress\n - name: MYSQL_PASSWORD\n valueFrom:\n secretKeyRef:\n name: mysql-pass\n key: password\n ports:\n - containerPort: 3306\n name: mysql\n volumeMounts:\n - name: mysql-persistent-storage\n mountPath: /var/lib/mysql\n volumes:\n - name: mysql-persistent-storage\n persistentVolumeClaim:\n claimName: mysql-pv-claim\n---\napiVersion: v1\nkind: Secret\nmetadata:\n name: mysql-pass\n labels:\n app: wordpress\ntype: Opaque\ndata:\n password: WU9VUl9QQVNTV09SRA==\n---\napiVersion: v1\nkind: Service\nmetadata:\n name: wordpress\n labels:\n app: wordpress\nspec:\n ports:\n - port: 80\n selector:\n app: wordpress\n tier: frontend\n type: LoadBalancer\n---\napiVersion: v1\nkind: PersistentVolumeClaim\nmetadata:\n name: wp-pv-claim\n labels:\n app: wordpress\nspec:\n accessModes:\n - ReadWriteOnce\n resources:\n requests:\n storage: 512M # instead of 20Gi\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: wordpress\n labels:\n app: wordpress\nspec:\n selector:\n matchLabels:\n app: wordpress\n tier: frontend\n strategy:\n type: Recreate\n template:\n metadata:\n labels:\n app: wordpress\n tier: frontend\n spec:\n containers:\n - image: wordpress:6.2.1-apache\n name: wordpress\n env:\n - name: WORDPRESS_DB_HOST\n value: wordpress-mysql\n - name: WORDPRESS_DB_PASSWORD\n valueFrom:\n secretKeyRef:\n name: mysql-pass\n key: password\n - name: WORDPRESS_DB_USER\n value: wordpress\n ports:\n - containerPort: 80\n name: wordpress\n volumeMounts:\n - name: wordpress-persistent-storage\n mountPath: /var/www/html\n volumes:\n - name: wordpress-persistent-storage\n persistentVolumeClaim:\n claimName: wp-pv-claim\n +2025-11-28 10:49:16,214 ; 127.0.0.1 ; /api/generate-helmfile-diagram ; format=png;helmfile=;extraArgs=;withoutNamespace=False +2025-11-28 10:50:49,741 ; 127.0.0.1 ; /api/generate-helmfile-diagram ; format=png;helmfile=repositories:\n - name: prometheus-community\n url: https://prometheus-community.github.io/helm-charts\n - name: grafana\n url: https://grafana.github.io/helm-charts\n\nreleases:\n - name: prometheus\n namespace: monitoring\n chart: prometheus-community/prometheus\n version: 15.0.0\n values:\n - server:\n persistentVolume:\n enabled: true\n size: 8Gi\n\n - name: grafana\n namespace: monitoring\n chart: grafana/grafana\n version: 6.50.0\n values:\n - adminPassword: changeme\n persistence:\n enabled: true\n size: 5Gi\n\n;extraArgs=;withoutNamespace=False +2025-11-28 10:51:45,236 ; 127.0.0.1 ; /api/generate-helmfile-diagram ; format=dot_json;helmfile=repositories:\n - name: prometheus-community\n url: https://prometheus-community.github.io/helm-charts\n - name: grafana\n url: https://grafana.github.io/helm-charts\n\nreleases:\n - name: prometheus\n namespace: monitoring\n chart: prometheus-community/prometheus\n version: 15.0.0\n values:\n - server:\n persistentVolume:\n enabled: true\n size: 8Gi\n\n - name: grafana\n namespace: monitoring\n chart: grafana/grafana\n version: 6.50.0\n values:\n - adminPassword: changeme\n persistence:\n enabled: true\n size: 5Gi\n\n;extraArgs=;withoutNamespace=False +2025-11-28 10:52:07,872 ; 127.0.0.1 ; /api/generate-helmfile-diagram ; format=pdf;helmfile=repositories:\n - name: prometheus-community\n url: https://prometheus-community.github.io/helm-charts\n - name: grafana\n url: https://grafana.github.io/helm-charts\n\nreleases:\n - name: prometheus\n namespace: monitoring\n chart: prometheus-community/prometheus\n version: 15.0.0\n values:\n - server:\n persistentVolume:\n enabled: true\n size: 8Gi\n\n - name: grafana\n namespace: monitoring\n chart: grafana/grafana\n version: 6.50.0\n values:\n - adminPassword: changeme\n persistence:\n enabled: true\n size: 5Gi\n\n;extraArgs=;withoutNamespace=False +2025-11-28 10:53:16,735 ; 127.0.0.1 ; /api/generate-helmfile-diagram ; format=dot_json;helmfile=repositories:\n - name: prometheus-community\n url: https://prometheus-community.github.io/helm-charts\n - name: grafana\n url: https://grafana.github.io/helm-charts\n\nreleases:\n - name: prometheus\n namespace: monitoring\n chart: prometheus-community/prometheus\n version: 15.0.0\n values:\n - server:\n persistentVolume:\n enabled: true\n size: 8Gi\n\n - name: grafana\n namespace: monitoring\n chart: grafana/grafana\n version: 6.50.0\n values:\n - adminPassword: changeme\n persistence:\n enabled: true\n size: 5Gi\n\n;extraArgs=;withoutNamespace=False +2025-11-28 10:55:33,948 ; 127.0.0.1 ; /api/generate-diagram ; format=dot_json;extraArgs=;withoutNamespace=False;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd +2025-11-28 10:59:52,363 ; 127.0.0.1 ; /api/generate-diagram ; format=dot_json;extraArgs=;withoutNamespace=False;manifest=apiVersion: v1\nkind: Service\nmetadata:\n name: redis-service\nspec:\n clusterIP: None\n selector:\n app: redis\n ports:\n - port: 6379\n targetPort: 6379\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: redis\nspec:\n serviceName: redis-service\n replicas: 3\n selector:\n matchLabels:\n app: redis\n template:\n metadata:\n labels:\n app: redis\n spec:\n containers:\n - name: redis\n image: redis:7.0\n ports:\n - containerPort: 6379\n volumeMounts:\n - name: data\n mountPath: /data\n volumeClaimTemplates:\n - metadata:\n name: data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n resources:\n requests:\n storage: 1Gi\n\n +2025-11-28 11:09:43,294 ; 127.0.0.1 ; /api/generate-diagram ; format=png;extraArgs=;withoutNamespace=False;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command: \n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd\n +2025-11-28 11:09:58,166 ; 127.0.0.1 ; /api/generate-helmfile-diagram ; format=png;helmfile=repositories:\n- name: prometheus-community\n url: https://prometheus-community.github.io/helm-charts\n\nreleases:\n- name: prom-norbac-ubuntu\n namespace: prometheus\n chart: prometheus-community/prometheus\n set:\n - name: rbac.create\n value: false\n;extraArgs=;withoutNamespace=False +2025-11-28 11:21:48,531 ; 127.0.0.1 ; /api/generate-diagram ; format=png;extraArgs=;withoutNamespace=False;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd +2025-11-28 11:21:54,810 ; 127.0.0.1 ; /api/generate-diagram ; format=jpg;extraArgs=;withoutNamespace=False;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd +2025-11-28 11:21:57,647 ; 127.0.0.1 ; /api/generate-diagram ; format=jpeg;extraArgs=;withoutNamespace=False;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd +2025-11-28 11:21:59,337 ; 127.0.0.1 ; /api/generate-diagram ; format=svg;extraArgs=;withoutNamespace=False;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd +2025-11-28 11:22:03,383 ; 127.0.0.1 ; /api/generate-diagram ; format=pdf;extraArgs=;withoutNamespace=False;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd +2025-11-28 11:22:11,671 ; 127.0.0.1 ; /api/generate-diagram ; format=dot;extraArgs=;withoutNamespace=False;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd +2025-11-28 11:22:14,625 ; 127.0.0.1 ; /api/generate-diagram ; format=dot_json;extraArgs=;withoutNamespace=False;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd +2025-11-28 11:22:25,918 ; 127.0.0.1 ; /api/generate-diagram ; format=svg;extraArgs=;withoutNamespace=False;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd +2025-11-28 11:22:31,130 ; 127.0.0.1 ; /api/generate-diagram ; format=dot_json;extraArgs=;withoutNamespace=False;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd +2025-11-28 11:22:51,772 ; 127.0.0.1 ; /api/generate-helm-diagram ; format=png;chartUrl=https://charts.jetstack.io/cert-manager;extraArgs= +2025-11-28 11:22:55,039 ; 127.0.0.1 ; /api/generate-helm-diagram ; format=jpg;chartUrl=https://charts.jetstack.io/cert-manager;extraArgs= +2025-11-28 11:22:57,897 ; 127.0.0.1 ; /api/generate-helm-diagram ; format=jpeg;chartUrl=https://charts.jetstack.io/cert-manager;extraArgs= +2025-11-28 11:23:00,135 ; 127.0.0.1 ; /api/generate-helm-diagram ; format=svg;chartUrl=https://charts.jetstack.io/cert-manager;extraArgs= +2025-11-28 11:23:02,246 ; 127.0.0.1 ; /api/generate-helm-diagram ; format=pdf;chartUrl=https://charts.jetstack.io/cert-manager;extraArgs= +2025-11-28 11:23:05,591 ; 127.0.0.1 ; /api/generate-helm-diagram ; format=dot;chartUrl=https://charts.jetstack.io/cert-manager;extraArgs= +2025-11-28 11:23:08,547 ; 127.0.0.1 ; /api/generate-helm-diagram ; format=dot_json;chartUrl=https://charts.jetstack.io/cert-manager;extraArgs= +2025-11-28 11:23:33,878 ; 127.0.0.1 ; /api/generate-helmfile-diagram ; format=png;helmfile=repositories:\n - name: prometheus-community\n url: https://prometheus-community.github.io/helm-charts\n - name: grafana\n url: https://grafana.github.io/helm-charts\n\nreleases:\n - name: prometheus\n namespace: monitoring\n chart: prometheus-community/prometheus\n version: 15.0.0\n values:\n - server:\n persistentVolume:\n enabled: true\n size: 8Gi\n\n - name: grafana\n namespace: monitoring\n chart: grafana/grafana\n version: 6.50.0\n values:\n - adminPassword: changeme\n persistence:\n enabled: true\n size: 5Gi\n\n;extraArgs=;withoutNamespace=False +2025-11-28 11:23:38,138 ; 127.0.0.1 ; /api/generate-helmfile-diagram ; format=dot_json;helmfile=repositories:\n - name: prometheus-community\n url: https://prometheus-community.github.io/helm-charts\n - name: grafana\n url: https://grafana.github.io/helm-charts\n\nreleases:\n - name: prometheus\n namespace: monitoring\n chart: prometheus-community/prometheus\n version: 15.0.0\n values:\n - server:\n persistentVolume:\n enabled: true\n size: 8Gi\n\n - name: grafana\n namespace: monitoring\n chart: grafana/grafana\n version: 6.50.0\n values:\n - adminPassword: changeme\n persistence:\n enabled: true\n size: 5Gi\n\n;extraArgs=;withoutNamespace=False +2025-11-28 11:23:41,873 ; 127.0.0.1 ; /api/generate-helmfile-diagram ; format=dot;helmfile=repositories:\n - name: prometheus-community\n url: https://prometheus-community.github.io/helm-charts\n - name: grafana\n url: https://grafana.github.io/helm-charts\n\nreleases:\n - name: prometheus\n namespace: monitoring\n chart: prometheus-community/prometheus\n version: 15.0.0\n values:\n - server:\n persistentVolume:\n enabled: true\n size: 8Gi\n\n - name: grafana\n namespace: monitoring\n chart: grafana/grafana\n version: 6.50.0\n values:\n - adminPassword: changeme\n persistence:\n enabled: true\n size: 5Gi\n\n;extraArgs=;withoutNamespace=False +2025-11-28 11:23:45,957 ; 127.0.0.1 ; /api/generate-helmfile-diagram ; format=pdf;helmfile=repositories:\n - name: prometheus-community\n url: https://prometheus-community.github.io/helm-charts\n - name: grafana\n url: https://grafana.github.io/helm-charts\n\nreleases:\n - name: prometheus\n namespace: monitoring\n chart: prometheus-community/prometheus\n version: 15.0.0\n values:\n - server:\n persistentVolume:\n enabled: true\n size: 8Gi\n\n - name: grafana\n namespace: monitoring\n chart: grafana/grafana\n version: 6.50.0\n values:\n - adminPassword: changeme\n persistence:\n enabled: true\n size: 5Gi\n\n;extraArgs=;withoutNamespace=False +2025-11-28 11:23:49,736 ; 127.0.0.1 ; /api/generate-helmfile-diagram ; format=svg;helmfile=repositories:\n - name: prometheus-community\n url: https://prometheus-community.github.io/helm-charts\n - name: grafana\n url: https://grafana.github.io/helm-charts\n\nreleases:\n - name: prometheus\n namespace: monitoring\n chart: prometheus-community/prometheus\n version: 15.0.0\n values:\n - server:\n persistentVolume:\n enabled: true\n size: 8Gi\n\n - name: grafana\n namespace: monitoring\n chart: grafana/grafana\n version: 6.50.0\n values:\n - adminPassword: changeme\n persistence:\n enabled: true\n size: 5Gi\n\n;extraArgs=;withoutNamespace=False +2025-11-28 11:23:53,516 ; 127.0.0.1 ; /api/generate-helmfile-diagram ; format=jpeg;helmfile=repositories:\n - name: prometheus-community\n url: https://prometheus-community.github.io/helm-charts\n - name: grafana\n url: https://grafana.github.io/helm-charts\n\nreleases:\n - name: prometheus\n namespace: monitoring\n chart: prometheus-community/prometheus\n version: 15.0.0\n values:\n - server:\n persistentVolume:\n enabled: true\n size: 8Gi\n\n - name: grafana\n namespace: monitoring\n chart: grafana/grafana\n version: 6.50.0\n values:\n - adminPassword: changeme\n persistence:\n enabled: true\n size: 5Gi\n\n;extraArgs=;withoutNamespace=False +2025-11-28 11:23:58,608 ; 127.0.0.1 ; /api/generate-helmfile-diagram ; format=jpg;helmfile=repositories:\n - name: prometheus-community\n url: https://prometheus-community.github.io/helm-charts\n - name: grafana\n url: https://grafana.github.io/helm-charts\n\nreleases:\n - name: prometheus\n namespace: monitoring\n chart: prometheus-community/prometheus\n version: 15.0.0\n values:\n - server:\n persistentVolume:\n enabled: true\n size: 8Gi\n\n - name: grafana\n namespace: monitoring\n chart: grafana/grafana\n version: 6.50.0\n values:\n - adminPassword: changeme\n persistence:\n enabled: true\n size: 5Gi\n\n;extraArgs=;withoutNamespace=False +2025-11-28 11:33:17,932 ; 127.0.0.1 ; /api/generate-diagram ; format=png;extraArgs=;withoutNamespace=False;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd +2025-11-28 11:33:28,865 ; 127.0.0.1 ; /api/generate-diagram ; format=png;extraArgs=;withoutNamespace=True;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd +2025-11-28 11:33:39,735 ; 127.0.0.1 ; /api/generate-diagram ; format=svg;extraArgs=;withoutNamespace=True;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd +2025-11-28 11:33:51,170 ; 127.0.0.1 ; /api/generate-helm-diagram ; format=png;chartUrl=https://charts.jetstack.io/cert-manager;extraArgs= +2025-11-28 11:34:15,179 ; 127.0.0.1 ; /api/generate-helm-diagram ; format=svg;chartUrl=https://charts.jetstack.io/cert-manager;extraArgs= +2025-11-28 11:34:40,529 ; 127.0.0.1 ; /api/generate-diagram ; format=jpeg;extraArgs=;withoutNamespace=False;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd +2025-11-28 11:37:44,041 ; 127.0.0.1 ; /api/generate-diagram ; format=png;extraArgs=;withoutNamespace=False;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd +2025-11-28 11:37:48,813 ; 127.0.0.1 ; /api/generate-diagram ; format=png;extraArgs=;withoutNamespace=False;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd +2025-11-28 11:37:51,380 ; 127.0.0.1 ; /api/generate-diagram ; format=png;extraArgs=;withoutNamespace=False;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd +2025-11-28 11:37:52,775 ; 127.0.0.1 ; /api/generate-diagram ; format=png;extraArgs=;withoutNamespace=False;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd +2025-11-28 11:38:03,632 ; 127.0.0.1 ; /api/generate-diagram ; format=png;extraArgs=;withoutNamespace=False;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd +2025-11-28 11:38:45,637 ; 127.0.0.1 ; /api/generate-helm-diagram ; format=png;chartUrl=https://grafana.github.io/helm-charts/grafana;extraArgs= +2025-11-28 11:40:55,865 ; 127.0.0.1 ; /api/generate-helm-diagram ; format=png;chartUrl=https://grafana.github.io/helm-charts/grafana;extraArgs= +2025-11-28 11:41:04,816 ; 127.0.0.1 ; /api/generate-helm-diagram ; format=svg;chartUrl=https://grafana.github.io/helm-charts/grafana;extraArgs= +2025-11-28 11:41:07,810 ; 127.0.0.1 ; /api/generate-helm-diagram ; format=dot;chartUrl=https://grafana.github.io/helm-charts/grafana;extraArgs= +2025-11-28 11:41:10,493 ; 127.0.0.1 ; /api/generate-helm-diagram ; format=dot_json;chartUrl=https://grafana.github.io/helm-charts/grafana;extraArgs= +2025-11-28 11:41:23,669 ; 127.0.0.1 ; /api/generate-diagram ; format=png;extraArgs=;withoutNamespace=False;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd +2025-11-28 11:41:27,014 ; 127.0.0.1 ; /api/generate-diagram ; format=png;extraArgs=;withoutNamespace=True;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd +2025-11-28 11:41:28,875 ; 127.0.0.1 ; /api/generate-diagram ; format=jpg;extraArgs=;withoutNamespace=True;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd +2025-11-28 11:41:31,995 ; 127.0.0.1 ; /api/generate-diagram ; format=svg;extraArgs=;withoutNamespace=True;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd +2025-11-28 11:41:34,858 ; 127.0.0.1 ; /api/generate-diagram ; format=pdf;extraArgs=;withoutNamespace=True;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd +2025-11-28 11:41:37,788 ; 127.0.0.1 ; /api/generate-diagram ; format=dot_json;extraArgs=;withoutNamespace=True;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd +2025-11-28 12:43:34,942 ; 127.0.0.1 ; /api/generate-diagram ; format=png;extraArgs=;withoutNamespace=False;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd +2025-11-28 12:47:33,411 ; 127.0.0.1 ; /api/generate-diagram ; format=png;extraArgs=;withoutNamespace=False;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd +2025-11-28 12:47:37,639 ; 127.0.0.1 ; /api/generate-diagram ; format=png;extraArgs=;withoutNamespace=True;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd +2025-11-28 12:47:52,325 ; 127.0.0.1 ; /api/generate-diagram ; format=png;extraArgs=--without-namespace;withoutNamespace=False;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd +2025-11-28 12:48:02,889 ; 127.0.0.1 ; /api/generate-diagram ; format=png;extraArgs=;withoutNamespace=False;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd +2025-11-28 12:48:05,278 ; 127.0.0.1 ; /api/generate-diagram ; format=png;extraArgs=;withoutNamespace=True;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd +2025-11-28 12:54:15,841 ; 127.0.0.1 ; /api/generate-diagram ; format=png;extraArgs=;withoutNamespace=False;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd +2025-11-28 12:54:17,956 ; 127.0.0.1 ; /api/generate-diagram ; format=png;extraArgs=;withoutNamespace=True;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd +2025-11-28 12:54:20,984 ; 127.0.0.1 ; /api/generate-diagram ; format=png;extraArgs=;withoutNamespace=False;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd +2025-11-28 12:57:21,847 ; 127.0.0.1 ; /api/generate-diagram ; format=png;extraArgs=;withoutNamespace=False;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd +2025-11-28 13:05:52,802 ; 127.0.0.1 ; /api/generate-helm-diagram ; format=png;chartUrl=https://kubernetes.github.io/ingress-nginx/ingress-nginx;extraArgs= +2025-11-28 13:17:41,041 ; 127.0.0.1 ; /api/generate-diagram ; format=png;extraArgs=;withoutNamespace=False;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd +2025-11-28 13:17:46,245 ; 127.0.0.1 ; /api/generate-diagram ; format=jpg;extraArgs=;withoutNamespace=False;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd +2025-11-28 13:17:48,736 ; 127.0.0.1 ; /api/generate-diagram ; format=jpeg;extraArgs=;withoutNamespace=False;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd +2025-11-28 13:17:51,523 ; 127.0.0.1 ; /api/generate-diagram ; format=svg;extraArgs=;withoutNamespace=False;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd +2025-11-28 13:17:53,849 ; 127.0.0.1 ; /api/generate-diagram ; format=pdf;extraArgs=;withoutNamespace=False;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd +2025-11-28 13:17:56,179 ; 127.0.0.1 ; /api/generate-diagram ; format=dot;extraArgs=;withoutNamespace=False;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd +2025-11-28 13:17:59,449 ; 127.0.0.1 ; /api/generate-diagram ; format=dot_json;extraArgs=;withoutNamespace=False;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd +2025-11-28 13:18:42,089 ; 127.0.0.1 ; /api/generate-diagram ; format=png;extraArgs=;withoutNamespace=False;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd +2025-11-28 13:21:19,948 ; 127.0.0.1 ; /api/generate-helm-diagram ; format=png;chartUrl=https://prometheus-community.github.io/helm-charts/prometheus;extraArgs= +2025-11-28 13:21:25,710 ; 127.0.0.1 ; /api/generate-helm-diagram ; format=dot_json;chartUrl=https://prometheus-community.github.io/helm-charts/prometheus;extraArgs= +2025-11-28 13:23:12,597 ; 127.0.0.1 ; /api/generate-diagram ; format=png;extraArgs=;withoutNamespace=False;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd +2025-11-28 13:25:15,031 ; 127.0.0.1 ; /api/generate-diagram ; format=png;extraArgs=;withoutNamespace=False;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd +2025-11-28 13:25:36,797 ; 127.0.0.1 ; /api/generate-helmfile-diagram ; format=png;helmfile=repositories:\n- name: prometheus-community\n url: https://prometheus-community.github.io/helm-charts\n\nreleases:\n- name: prom-norbac-ubuntu\n namespace: prometheus\n chart: prometheus-community/prometheus\n set:\n - name: rbac.create\n value: false\n;extraArgs=;withoutNamespace=False +2025-11-28 13:28:33,079 ; 127.0.0.1 ; /api/generate-diagram ; format=png;extraArgs=;withoutNamespace=False;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd +2025-11-28 13:28:39,944 ; 127.0.0.1 ; /api/generate-diagram ; format=png;extraArgs=;withoutNamespace=True;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd +2025-11-28 13:32:50,201 ; 127.0.0.1 ; /api/generate-diagram ; format=png;extraArgs=;withoutNamespace=False;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd +2025-11-28 13:32:51,293 ; 127.0.0.1 ; /api/generate-diagram ; format=png;extraArgs=;withoutNamespace=False;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd +2025-11-28 13:32:52,066 ; 127.0.0.1 ; /api/generate-diagram ; format=png;extraArgs=;withoutNamespace=False;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd +2025-11-28 13:32:55,939 ; 127.0.0.1 ; /api/generate-diagram ; format=png;extraArgs=;withoutNamespace=False;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd +2025-11-28 13:32:56,817 ; 127.0.0.1 ; /api/generate-diagram ; format=png;extraArgs=;withoutNamespace=False;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd +2025-11-28 13:32:57,550 ; 127.0.0.1 ; /api/generate-diagram ; format=png;extraArgs=;withoutNamespace=False;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd +2025-11-28 13:32:58,191 ; 127.0.0.1 ; /api/generate-diagram ; format=png;extraArgs=;withoutNamespace=False;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd +2025-11-28 13:32:58,861 ; 127.0.0.1 ; /api/generate-diagram ; format=png;extraArgs=;withoutNamespace=False;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd +2025-11-28 13:32:59,611 ; 127.0.0.1 ; /api/generate-diagram ; format=png;extraArgs=;withoutNamespace=False;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd +2025-11-28 13:33:00,258 ; 127.0.0.1 ; /api/generate-diagram ; format=png;extraArgs=;withoutNamespace=False;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd +2025-11-28 13:33:05,422 ; 127.0.0.1 ; /api/generate-helm-diagram ; format=png;chartUrl=https://prometheus-community.github.io/helm-charts/prometheus;extraArgs= +2025-11-28 13:33:16,300 ; 127.0.0.1 ; /api/generate-helmfile-diagram ; format=png;helmfile=repositories:\n - name: prometheus-community\n url: https://prometheus-community.github.io/helm-charts\n - name: grafana\n url: https://grafana.github.io/helm-charts\n\nreleases:\n - name: prometheus\n namespace: monitoring\n chart: prometheus-community/prometheus\n version: 15.0.0\n values:\n - server:\n persistentVolume:\n enabled: true\n size: 8Gi\n\n - name: grafana\n namespace: monitoring\n chart: grafana/grafana\n version: 6.50.0\n values:\n - adminPassword: changeme\n persistence:\n enabled: true\n size: 5Gi\n\n;extraArgs=;withoutNamespace=False +2025-11-28 13:37:25,304 ; 127.0.0.1 ; /api/generate-diagram ; format=png;extraArgs=;withoutNamespace=False;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd +2025-11-28 13:37:28,741 ; 127.0.0.1 ; /api/generate-diagram ; format=jpeg;extraArgs=;withoutNamespace=False;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd +2025-11-28 13:37:31,008 ; 127.0.0.1 ; /api/generate-diagram ; format=svg;extraArgs=;withoutNamespace=False;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd +2025-11-28 13:37:35,563 ; 127.0.0.1 ; /api/generate-helmfile-diagram ; format=jpeg;helmfile=;extraArgs=;withoutNamespace=False +2025-11-28 13:37:36,927 ; 127.0.0.1 ; /api/generate-helmfile-diagram ; format=jpeg;helmfile=;extraArgs=;withoutNamespace=False +2025-11-28 13:37:39,362 ; 127.0.0.1 ; /api/generate-helmfile-diagram ; format=jpeg;helmfile=repositories:\n - name: prometheus-community\n url: https://prometheus-community.github.io/helm-charts\n - name: grafana\n url: https://grafana.github.io/helm-charts\n\nreleases:\n - name: prometheus\n namespace: monitoring\n chart: prometheus-community/prometheus\n version: 15.0.0\n values:\n - server:\n persistentVolume:\n enabled: true\n size: 8Gi\n\n - name: grafana\n namespace: monitoring\n chart: grafana/grafana\n version: 6.50.0\n values:\n - adminPassword: changeme\n persistence:\n enabled: true\n size: 5Gi\n\n;extraArgs=;withoutNamespace=False +2025-11-28 13:38:17,604 ; 127.0.0.1 ; /api/generate-helmfile-diagram ; format=jpeg;helmfile=repositories:\n - name: prometheus-community\n url: https://prometheus-community.github.io/helm-charts\n - name: grafana\n url: https://grafana.github.io/helm-charts\n\nreleases:\n - name: prometheus\n namespace: monitoring\n chart: prometheus-community/prometheus\n version: 15.0.0\n values:\n - server:\n persistentVolume:\n enabled: true\n size: 8Gi\n\n - name: grafana\n namespace: monitoring\n chart: grafana/grafana\n version: 6.50.0\n values:\n - adminPassword: changeme\n persistence:\n enabled: true\n size: 5Gi\n\n;extraArgs=;withoutNamespace=False +2025-11-28 13:49:00,253 ; 127.0.0.1 ; /api/generate-diagram ; format=png;extraArgs=;withoutNamespace=False;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd +2025-11-28 13:58:25,432 ; 127.0.0.1 ; /api/generate-diagram ; format=png;extraArgs=;withoutNamespace=False;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd +2025-11-28 14:00:16,585 ; 127.0.0.1 ; /api/generate-helm-diagram ; format=png;chartUrl=https://charts.bitnami.com/bitnami/redis;extraArgs= +2025-11-28 14:00:47,887 ; 127.0.0.1 ; /api/generate-diagram ; format=png;extraArgs=;withoutNamespace=False;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd +2025-11-28 14:00:58,825 ; 127.0.0.1 ; /api/generate-helm-diagram ; format=png;chartUrl=https://kubernetes.github.io/ingress-nginx/ingress-nginx;extraArgs= +2025-11-28 14:03:51,098 ; 127.0.0.1 ; /api/generate-helm-diagram ; format=png;chartUrl=oci://ghcr.io/argoproj/argo-helm/argo-cd;extraArgs= +2025-11-28 14:04:06,132 ; 127.0.0.1 ; /api/generate-diagram ; format=png;extraArgs=;withoutNamespace=False;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd +2025-11-28 16:40:40,648 ; 127.0.0.1 ; /api/generate-helm-diagram ; format=png;chartUrl=http://charts.gitlab.io/gitlab;extraArgs=--set certmanager-issuer.email=someone@acme.com +2025-11-28 16:42:10,189 ; 127.0.0.1 ; /api/generate-helmfile-diagram ; format=png;helmfile=repositories:\n - name: prometheus-community\n url: https://prometheus-community.github.io/helm-charts\n - name: grafana\n url: https://grafana.github.io/helm-charts\n\nreleases:\n - name: prometheus\n namespace: monitoring\n chart: prometheus-community/prometheus\n version: 15.0.0\n values:\n - server:\n persistentVolume:\n enabled: true\n size: 8Gi\n\n - name: grafana\n namespace: monitoring\n chart: grafana/grafana\n version: 6.50.0\n values:\n - adminPassword: changeme\n persistence:\n enabled: true\n size: 5Gi\n\n;extraArgs=;withoutNamespace=False +2025-11-28 16:42:50,574 ; 127.0.0.1 ; /api/generate-helm-diagram ; format=png;chartUrl=https://kubernetes.github.io/dashboard/kubernetes-dashboard;extraArgs= +2025-11-28 16:43:07,083 ; 127.0.0.1 ; /api/generate-diagram ; format=png;extraArgs=;withoutNamespace=False;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd +2025-11-28 17:14:26,329 ; 127.0.0.1 ; /api/generate-diagram ; format=png;extraArgs=;withoutNamespace=True;manifest=apiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: cassandra\n name: cassandra\nspec:\n clusterIP: None\n ports:\n - port: 9042\n selector:\n app: cassandra\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: cassandra\n labels:\n app: cassandra\nspec:\n serviceName: cassandra\n replicas: 3\n selector:\n matchLabels:\n app: cassandra\n template:\n metadata:\n labels:\n app: cassandra\n spec:\n terminationGracePeriodSeconds: 500\n containers:\n - name: cassandra\n image: gcr.io/google-samples/cassandra:v13\n imagePullPolicy: Always\n ports:\n - containerPort: 7000\n name: intra-node\n - containerPort: 7001\n name: tls-intra-node\n - containerPort: 7199\n name: jmx\n - containerPort: 9042\n name: cql\n resources:\n limits:\n cpu: "500m"\n memory: 1Gi\n requests:\n cpu: "500m"\n memory: 512M #CHANGED: instead of 1Gi\n securityContext:\n capabilities:\n add:\n - IPC_LOCK\n lifecycle:\n preStop:\n exec:\n command:\n - /bin/sh\n - -c\n - nodetool drain\n env:\n - name: MAX_HEAP_SIZE\n value: 512M\n - name: HEAP_NEWSIZE\n value: 100M\n - name: CASSANDRA_SEEDS\n value: "cassandra-0.cassandra.default.svc.cluster.local"\n - name: CASSANDRA_CLUSTER_NAME\n value: "K8Demo"\n - name: CASSANDRA_DC\n value: "DC1-K8Demo"\n - name: CASSANDRA_RACK\n value: "Rack1-K8Demo"\n - name: POD_IP\n valueFrom:\n fieldRef:\n fieldPath: status.podIP\n readinessProbe:\n exec:\n command:\n - /bin/bash\n - -c\n - echo OK #ERROR: if /ready-probe.sh\n initialDelaySeconds: 15\n timeoutSeconds: 5\n # These volume mounts are persistent. They are like inline claims,\n # but not exactly because the names need to match exactly one of\n # the stateful pod volumes.\n volumeMounts:\n - name: cassandra-data\n mountPath: /cassandra_data\n # These are converted to volume claims by the controller\n # and mounted at the paths mentioned above.\n # do not use these in production until ssd GCEPersistentDisk or other ssd pd\n volumeClaimTemplates:\n - metadata:\n name: cassandra-data\n spec:\n accessModes: [ "ReadWriteOnce" ]\n storageClassName: fast\n resources:\n requests:\n storage: 512M #CHANGED: instead of 1Gi\n---\nkind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n name: fast\nprovisioner: k8s.io/minikube-hostpath\nparameters:\n type: pd-ssd diff --git a/webapp/backend/logs/test.png b/webapp/backend/logs/test.png new file mode 100644 index 0000000..e13d35c --- /dev/null +++ b/webapp/backend/logs/test.png @@ -0,0 +1,9 @@ +{ + "command": "kube-diagrams /tmp/tmphc8047hw.yaml -o /tmp/tmphc8047hw.png", + "diagram": "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", + "filename": "tmphc8047hw.png", + "message": "Diagram successfully generated.", + "mimeType": "image/png", + "stderr": "", + "stdout": "Load /tmp/tmphc8047hw.yaml...\n/tmp/tmphc8047hw.png generated.\n" +} diff --git a/webapp/backend/requirements.txt b/webapp/backend/requirements.txt new file mode 100644 index 0000000..a7dcbc1 --- /dev/null +++ b/webapp/backend/requirements.txt @@ -0,0 +1,21 @@ +blinker==1.9.0 +cfgv==3.4.0 +click==8.2.1 +diagrams==0.24.4 +distlib==0.4.0 +filelock==3.19.1 +Flask==3.1.2 +flask-cors==6.0.1 +graphviz==0.20.3 +gunicorn==23.0.0 +identify==2.6.14 +itsdangerous==2.2.0 +Jinja2==3.1.6 +KubeDiagrams==0.6.0 +MarkupSafe==3.0.2 +nodeenv==1.9.1 +platformdirs==4.4.0 +pre_commit==4.3.0 +PyYAML==6.0.2 +virtualenv==20.34.0 +Werkzeug==3.1.3 diff --git a/webapp/backend/routes/__init__.py b/webapp/backend/routes/__init__.py new file mode 100644 index 0000000..99bf10f --- /dev/null +++ b/webapp/backend/routes/__init__.py @@ -0,0 +1,7 @@ +"""Routes Package Init File""" +from .manifest import manifest_bp +from .helm import helm_bp +from .helmfile import helmfile_bp +from .submit import submit_bp + +__all__ = ['manifest_bp', 'helm_bp', 'helmfile_bp', 'submit_bp'] \ No newline at end of file diff --git a/webapp/backend/routes/__pycache__/__init__.cpython-312.pyc b/webapp/backend/routes/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..7d0d16d Binary files /dev/null and b/webapp/backend/routes/__pycache__/__init__.cpython-312.pyc differ diff --git a/webapp/backend/routes/__pycache__/helm.cpython-312.pyc b/webapp/backend/routes/__pycache__/helm.cpython-312.pyc new file mode 100644 index 0000000..8c7ab15 Binary files /dev/null and b/webapp/backend/routes/__pycache__/helm.cpython-312.pyc differ diff --git a/webapp/backend/routes/__pycache__/helmfile.cpython-312.pyc b/webapp/backend/routes/__pycache__/helmfile.cpython-312.pyc new file mode 100644 index 0000000..d3dcc1a Binary files /dev/null and b/webapp/backend/routes/__pycache__/helmfile.cpython-312.pyc differ diff --git a/webapp/backend/routes/__pycache__/manifest.cpython-312.pyc b/webapp/backend/routes/__pycache__/manifest.cpython-312.pyc new file mode 100644 index 0000000..d635941 Binary files /dev/null and b/webapp/backend/routes/__pycache__/manifest.cpython-312.pyc differ diff --git a/webapp/backend/routes/__pycache__/submit.cpython-312.pyc b/webapp/backend/routes/__pycache__/submit.cpython-312.pyc new file mode 100644 index 0000000..7f93cec Binary files /dev/null and b/webapp/backend/routes/__pycache__/submit.cpython-312.pyc differ diff --git a/webapp/backend/routes/__pycache__/utils.cpython-312.pyc b/webapp/backend/routes/__pycache__/utils.cpython-312.pyc new file mode 100644 index 0000000..4e248a5 Binary files /dev/null and b/webapp/backend/routes/__pycache__/utils.cpython-312.pyc differ diff --git a/webapp/backend/routes/helm.py b/webapp/backend/routes/helm.py new file mode 100644 index 0000000..52b79df --- /dev/null +++ b/webapp/backend/routes/helm.py @@ -0,0 +1,49 @@ +"""Routes for generating diagrams from Helm charts.""" +from flask import Blueprint, request + +from .utils import log_to_csv +from services import generate_from_helm +from utils import InputValidator, ResponseBuilder + +helm_bp = Blueprint('helm', __name__) + +@helm_bp.route('/api/generate-helm-diagram', methods=['POST']) +def generate_helm_diagram(): + """Generate a diagram from a Helm chart.""" + data = request.get_json() + chart_url = data.get("chart", "") + output_format = (data.get("outputFormat") or "png").lower() + extra_args = data.get('extraArgs', '') + + # Log to CSV only + client_ip = request.remote_addr + route = request.path + params = f"format={output_format};chartUrl={chart_url};extraArgs={extra_args}" + log_to_csv(client_ip, route, params) + + # Chart URL validation + is_valid, error_msg = InputValidator.validate_helm_chart_url(chart_url) + if not is_valid: + return ResponseBuilder.validation_error("chart", error_msg) + + # Output format validation + is_valid, error_msg = InputValidator.validate_output_format(output_format) + if not is_valid: + return ResponseBuilder.validation_error("outputFormat", error_msg) + + # Extra arguments validation + is_valid, error_msg = InputValidator.validate_extra_args(extra_args) + if not is_valid: + return ResponseBuilder.validation_error("extraArgs", error_msg) + + # Diagram generation through service + result = generate_from_helm( + chart_url=chart_url, + output_format=output_format, + extra_args=extra_args + ) + + # Returning the response + return ResponseBuilder.from_diagram_result(result) + + diff --git a/webapp/backend/routes/helmfile.py b/webapp/backend/routes/helmfile.py new file mode 100644 index 0000000..04b2312 --- /dev/null +++ b/webapp/backend/routes/helmfile.py @@ -0,0 +1,57 @@ +"""Route for diagram generation from Helmfiles.""" +from flask import Blueprint, request + +from routes.utils import log_to_csv +from services import generate_from_helmfile +from utils import InputValidator, ResponseBuilder +from .utils import compact_for_log + +helmfile_bp = Blueprint('helmfile', __name__) + +@helmfile_bp.route('/api/generate-helmfile-diagram', methods=['POST']) +def generate_helmfile_diagram(): + """Diagram generation from a Helmfile.""" + data = request.get_json() + helmfile_content = data.get("content", "") + output_format = (data.get("outputFormat") or "png").lower() + extra_args = data.get("extraArgs", "").strip() + without_namespace = data.get("withoutNamespace", False) + + # Log to CSV only + client_ip = request.remote_addr + route = request.path + params = f"format={output_format};helmfile={compact_for_log(helmfile_content)};extraArgs={compact_for_log(extra_args)};withoutNamespace={without_namespace}" + log_to_csv(client_ip, route, params) + + # Helmfile validation + is_valid, error_msg = InputValidator.validate_helmfile(helmfile_content) + if not is_valid: + return ResponseBuilder.validation_error("content", error_msg) + + # Verify it doesn't look like a Manifest + if InputValidator.looks_like_manifest(helmfile_content): + return ResponseBuilder.error( + "This looks like a Manifest. Please use the Manifest tab for this content." + ) + + # Output format validation + is_valid, error_msg = InputValidator.validate_output_format(output_format) + if not is_valid: + return ResponseBuilder.validation_error("outputFormat", error_msg) + + # Extra arguments validation + is_valid, error_msg = InputValidator.validate_extra_args(extra_args) + if not is_valid: + return ResponseBuilder.validation_error("extraArgs", error_msg) + + # Diagram generation through service + result = generate_from_helmfile( + helmfile_content=helmfile_content, + output_format=output_format, + extra_args=extra_args, + without_namespace=without_namespace + ) + + # Returning the response + return ResponseBuilder.from_diagram_result(result) + diff --git a/webapp/backend/routes/manifest.py b/webapp/backend/routes/manifest.py new file mode 100644 index 0000000..bc21290 --- /dev/null +++ b/webapp/backend/routes/manifest.py @@ -0,0 +1,62 @@ +"""Route for diagram generation from Kubernetes manifests.""" +from flask import Blueprint, request + +from services import generate_from_manifest +from utils import InputValidator, ResponseBuilder +from .utils import compact_for_log, log_to_csv + +manifest_bp = Blueprint('manifest', __name__) + +@manifest_bp.route('/api/generate-diagram', methods=['POST']) +def generate_diagram(): + """Diagram generation from a Kubernetes manifest.""" + data = request.get_json() + manifest_content = data.get('manifest', '') + output_format = (data.get('outputFormat') or 'png').lower() + extra_args = data.get('extraArgs', '') + without_namespace = data.get('withoutNamespace', False) + + # Log to CSV only + client_ip = request.remote_addr + route = request.path + params = ( + f"format={output_format};" + f"extraArgs={compact_for_log(extra_args)};" + f"withoutNamespace={without_namespace};" + f"manifest={compact_for_log(manifest_content)}" + ) + log_to_csv(client_ip, route, params) + + # Manifest validation + is_valid, error_msg = InputValidator.validate_manifest(manifest_content) + if not is_valid: + return ResponseBuilder.validation_error("manifest", error_msg) + + # Verify it doesn't look like a Helmfile + if InputValidator.looks_like_helmfile(manifest_content): + return ResponseBuilder.error( + "This looks like a Helmfile. Please use the HelmFile tab for this content." + ) + + # Output format validation + is_valid, error_msg = InputValidator.validate_output_format(output_format) + if not is_valid: + return ResponseBuilder.validation_error("outputFormat", error_msg) + + # Extra arguments validation + is_valid, error_msg = InputValidator.validate_extra_args(extra_args) + if not is_valid: + return ResponseBuilder.validation_error("extraArgs", error_msg) + + # Generate diagram through service + result = generate_from_manifest( + manifest_content=manifest_content, + output_format=output_format, + extra_args=extra_args, + without_namespace=without_namespace + ) + + # Returning the response + return ResponseBuilder.from_diagram_result(result) + + diff --git a/webapp/backend/routes/submit.py b/webapp/backend/routes/submit.py new file mode 100644 index 0000000..c8144b4 --- /dev/null +++ b/webapp/backend/routes/submit.py @@ -0,0 +1,43 @@ +"""Route for submitting user feedback.""" +from flask import request, Blueprint +import logging + +from config import Config +from utils import ResponseBuilder, logger +from .utils import log_to_csv + +submit_bp = Blueprint('submit', __name__) + +@submit_bp.route('/api/submit-feedback', methods=['POST']) +def submit_feedback(): + """Submit user feedback.""" + data = request.get_json() + note = data.get("note", "") + comment = data.get("comment", "") + diagram_type = data.get("diagramType", "unknown") + + # Log to CSV only + client_ip = request.remote_addr + log_to_csv(client_ip, "Feedback", f"type={diagram_type};note={note}") + + if note or comment: + try: + with open(Config.FEEDBACK_FILE, "a", encoding="utf-8") as f: + f.write(f"[{diagram_type.upper()}]\nNote: {note}\nCommentaire: {comment}\n\n") + + return ResponseBuilder.success( + message="Feedback submitted successfully. Thank you!" + ) + except Exception as e: + # Log l'erreur dans le CSV + csv_logger = logging.getLogger(Config.LOGGER_NAME) + csv_logger.error(f"Error writing feedback: {e}") + return ResponseBuilder.error( + "Failed to save feedback. Please try again.", + status_code=500 + ) + + return ResponseBuilder.validation_error( + "feedback", + "Feedback must contain at least a note or a comment." + ) diff --git a/webapp/backend/routes/utils.py b/webapp/backend/routes/utils.py new file mode 100644 index 0000000..85c2a98 --- /dev/null +++ b/webapp/backend/routes/utils.py @@ -0,0 +1,37 @@ +"""Functions utils for routes.""" +from constants import MAX_LOG_LENGTH + + +def compact_for_log(value: str, maxlen: int = MAX_LOG_LENGTH) -> str: + """ + Compress a string for the logs (escape n/ r). + + Args: + value: Value to be compressed + maxlen: Maximum length + + Returns: + str: Compacted String + """ + if value is None: + return "" + s = str(value).replace("\n", "\\n").replace("\r", "\\r") + if len(s) <= maxlen: + return s + return s[:maxlen] + f"...<+{len(s)-maxlen} chars>" + + +def log_to_csv(client_ip: str, route: str, params: str): + """ + Legacy function kept for backward compatibility. + + Note: Logging is now handled centrally in app.py using Apache Combined Log Format + which is compatible with GoAccess and other log analysis tools. + + Args: + client_ip: Not used (IP obtained from request) + route: Not used (obtained from request) + params: Not used (logging is done in after_request) + """ + # No-op: logging is handled by app.py after_request in Apache Combined Log Format + pass diff --git a/webapp/backend/services/__init__.py b/webapp/backend/services/__init__.py new file mode 100644 index 0000000..602bc37 --- /dev/null +++ b/webapp/backend/services/__init__.py @@ -0,0 +1,15 @@ +"""Package contains all services for diagram generation.""" +from .models import DiagramResult +from .file_manager import FileManager +from .manifestService import generate_from_manifest +from .helmService import generate_from_helm +from .helmfileService import generate_from_helmfile + +__all__ = [ + 'DiagramResult', + 'FileManager', + 'generate_from_manifest', + 'generate_from_helm', + 'generate_from_helmfile' +] + diff --git a/webapp/backend/services/__pycache__/__init__.cpython-312.pyc b/webapp/backend/services/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..5a207b4 Binary files /dev/null and b/webapp/backend/services/__pycache__/__init__.cpython-312.pyc differ diff --git a/webapp/backend/services/__pycache__/file_manager.cpython-312.pyc b/webapp/backend/services/__pycache__/file_manager.cpython-312.pyc new file mode 100644 index 0000000..a981edf Binary files /dev/null and b/webapp/backend/services/__pycache__/file_manager.cpython-312.pyc differ diff --git a/webapp/backend/services/__pycache__/helmService.cpython-312.pyc b/webapp/backend/services/__pycache__/helmService.cpython-312.pyc new file mode 100644 index 0000000..7bb1e1b Binary files /dev/null and b/webapp/backend/services/__pycache__/helmService.cpython-312.pyc differ diff --git a/webapp/backend/services/__pycache__/helmfileService.cpython-312.pyc b/webapp/backend/services/__pycache__/helmfileService.cpython-312.pyc new file mode 100644 index 0000000..a78a40e Binary files /dev/null and b/webapp/backend/services/__pycache__/helmfileService.cpython-312.pyc differ diff --git a/webapp/backend/services/__pycache__/manifestService.cpython-312.pyc b/webapp/backend/services/__pycache__/manifestService.cpython-312.pyc new file mode 100644 index 0000000..47e8b42 Binary files /dev/null and b/webapp/backend/services/__pycache__/manifestService.cpython-312.pyc differ diff --git a/webapp/backend/services/__pycache__/models.cpython-312.pyc b/webapp/backend/services/__pycache__/models.cpython-312.pyc new file mode 100644 index 0000000..4f613d4 Binary files /dev/null and b/webapp/backend/services/__pycache__/models.cpython-312.pyc differ diff --git a/webapp/backend/services/__pycache__/utils.cpython-312.pyc b/webapp/backend/services/__pycache__/utils.cpython-312.pyc new file mode 100644 index 0000000..746feab Binary files /dev/null and b/webapp/backend/services/__pycache__/utils.cpython-312.pyc differ diff --git a/webapp/backend/services/file_manager.py b/webapp/backend/services/file_manager.py new file mode 100644 index 0000000..d4a1353 --- /dev/null +++ b/webapp/backend/services/file_manager.py @@ -0,0 +1,100 @@ +"""Temporary file management.""" +import os +import tempfile +from typing import Optional +from contextlib import contextmanager +class FileManager: + """Temporary file manager.""" + @staticmethod + @contextmanager + def create_temp_file(content: str, suffix: str = '.yaml', mode: str = 'w'): + """ + Create a temporary file with the given content. + Args: + content: Content to be written in the file + suffix: File suffix (extension) + mode: File opening mode + Yields: + str: Temporary file path created + """ + temp_file = None + try: + with tempfile.NamedTemporaryFile(mode=mode, delete=False, suffix=suffix) as f: + if mode == 'w': + f.write(content) + else: + f.write(content.encode('utf-8')) + temp_file = f.name + yield temp_file + finally: + if temp_file and os.path.exists(temp_file): + try: + os.remove(temp_file) + except OSError: + pass + @staticmethod + def cleanup_files(*file_paths: str) -> None: + """ + Safely deletes the specified files. + Args: + *file_paths: Paths of the files to be deleted + """ + for file_path in file_paths: + if file_path and os.path.exists(file_path): + try: + os.remove(file_path) + except OSError: + pass + @staticmethod + def get_output_paths(base_path: str, output_format: str) -> tuple[str, str]: + """ + Generates the output paths for a given format. + Args: + base_path: Base path (without extension) + output_format: Requested output format + Returns: + tuple: (requested format_path, fallback png_path) + """ + base_without_ext = os.path.splitext(base_path)[0] + requested_output = f"{base_without_ext}.{output_format}" + png_output = f"{base_without_ext}.png" + return requested_output, png_output + @staticmethod + def find_output_file(requested_path: str, fallback_path: str) -> Optional[tuple[str, str]]: + """ + Find the generated output file. + Args: + requested_path: File path in the requested format + fallback_path: Fallback file path (PNG) + Returns: + tuple: (file_path, format) or None if no file found + """ + if os.path.exists(requested_path): + format_ext = os.path.splitext(requested_path)[1].lstrip('.') + return requested_path, format_ext + elif os.path.exists(fallback_path): + return fallback_path, "png" + return None + @staticmethod + def read_file_content(file_path: str, binary: bool = True) -> bytes | str: + """ + Reads the contents of a file. + Args: + file_path: Path of the file to read + binary: If True, reads in binary mode + Returns: + File content (bytes or str) + """ + mode = "rb" if binary else "r" + with open(file_path, mode) as f: + return f.read() + @staticmethod + def get_base_name_from_path(file_path: str) -> str: + """ + Extract the base name of a file path (without extension). + Args: + file_path: File path + Returns: + Base name without extension + """ + return os.path.splitext(os.path.basename(file_path))[0] diff --git a/webapp/backend/services/helmService.py b/webapp/backend/services/helmService.py new file mode 100644 index 0000000..5766b15 --- /dev/null +++ b/webapp/backend/services/helmService.py @@ -0,0 +1,137 @@ +"""Service for generating diagrams from Helm charts.""" +import subprocess +import os +from urllib.parse import urlparse + +from constants import MIME_TYPES +from .models import DiagramResult +from .file_manager import FileManager +from .utils import parse_extra_args, has_fatal_error, encode_content + + +def generate_from_helm( + chart_url: str, + output_format: str = "png", + extra_args: str = "" +) -> DiagramResult: + """ + Generate a diagram from a Helm chart. + + Args: + chart_url: Helm chart URL + output_format: Output format + extra_args: Additional arguments + + Returns: + DiagramResult: Result of the generation + """ + # Extraction du nom de base + parsed = urlparse(chart_url) + base_name = os.path.basename(parsed.path).replace(".tgz", "").replace(".tar.gz", "") + + # Pour les URLs OCI + if chart_url.startswith('oci://'): + base_name = chart_url.rstrip('/').split('/')[-1] + + requested_output = os.path.abspath(f"{base_name}.{output_format}") + png_output = os.path.abspath(f"{base_name}.png") + + try: + # Command uses helm-diagrams instead of helm + cmd = ["helm-diagrams", chart_url, "-o", f"{base_name}.{output_format}"] + if extra_args.strip(): + cmd.extend(parse_extra_args(extra_args)) + + # Execution + proc = subprocess.run(cmd, check=False, capture_output=True, text=True) + stdout_output = proc.stdout or "" + stderr_output = proc.stderr or "" + + # First we verify if there was an error before file exist + has_error = proc.returncode != 0 or has_fatal_error(stdout_output, stderr_output) + + # Second we verify if there was an error in the stderr output + if "Error:" in stderr_output or "execution error" in stderr_output.lower(): + has_error = True + + if has_error: + FileManager.cleanup_files(requested_output, png_output) + + # logs for all errors + error_details = [] + if "not found" in stderr_output.lower() or "404" in stderr_output: + error_details.append("Chart not found. Please verify the repository URL and chart name.") + if "could not download" in stderr_output.lower(): + error_details.append("Could not download the chart. Check the URL and your network connection.") + if "authentication" in stderr_output.lower() or "unauthorized" in stderr_output.lower(): + error_details.append("Authentication required or access denied.") + if "is not a valid chart repository" in stderr_output.lower(): + error_details.append("The URL is not a valid Helm chart repository.") + if "repo not found" in stderr_output.lower(): + error_details.append("Repository not found. Make sure the URL points to a valid Helm repository.") + if "execution error" in stderr_output.lower(): + error_details.append("Helm chart has configuration errors. Check the stderr output below for details.") + if "you must provide" in stderr_output.lower(): + error_details.append("Missing required configuration values in the chart.") + + main_error = " ".join(error_details) if error_details else "helm-diagrams failed to generate the diagram." + + return DiagramResult( + success=False, + error=main_error, + command=" ".join(cmd), + stdout=stdout_output, + stderr=stderr_output + ) + + # Search for the output file + output_info = FileManager.find_output_file(requested_output, png_output) + if not output_info: + return DiagramResult( + success=False, + error=f"Output file not found (looked for {os.path.basename(requested_output)} and {os.path.basename(png_output)}).", + command=" ".join(cmd), + stdout=stdout_output, + stderr=stderr_output + ) + + output_file, produced_format = output_info + + note = "" + if produced_format == "png" and output_format != "png": + note = f"Requested format '{output_format}' is not available from helm-diagrams. Returned PNG instead." + + content = FileManager.read_file_content(output_file, binary=True) + encoded = encode_content(content, produced_format) + + # Cleaning + FileManager.cleanup_files(requested_output, png_output) + + message = (note + " " if note else "") + "Helm diagram successfully generated." + + return DiagramResult( + success=True, + diagram=encoded, + mime_type=MIME_TYPES.get(produced_format, "application/octet-stream"), + filename=f"{base_name}.{produced_format}", + message=message.strip(), + command=" ".join(cmd), + stdout=stdout_output, + stderr=stderr_output + ) + + except ValueError as e: + FileManager.cleanup_files(requested_output, png_output) + return DiagramResult( + success=False, + error=str(e), + command=" ".join(cmd) if 'cmd' in locals() else None + ) + except Exception as e: + FileManager.cleanup_files(requested_output, png_output) + return DiagramResult( + success=False, + error=f"Internal error: {e}", + command=" ".join(cmd) if 'cmd' in locals() else None + ) + diff --git a/webapp/backend/services/helmfileService.py b/webapp/backend/services/helmfileService.py new file mode 100644 index 0000000..adc5168 --- /dev/null +++ b/webapp/backend/services/helmfileService.py @@ -0,0 +1,121 @@ +"""Service for generating diagrams from Helmfiles.""" +import subprocess +import os + +from constants import MIME_TYPES +from .models import DiagramResult +from .file_manager import FileManager +from .utils import parse_extra_args, has_fatal_error, encode_content + + +def generate_from_helmfile( + helmfile_content: str, + output_format: str = "png", + extra_args: str = "", + without_namespace: bool = False +) -> DiagramResult: + """ + Generate a diagram from a Helmfile. + + Args: + helmfile_content: Contents of the Helmfile + output_format: Output format + extra_args: Additional arguments + without_namespace: Hide namespaces + + Returns: + DiagramResult: Result of the generation + """ + with FileManager.create_temp_file(helmfile_content, suffix=".yaml", mode='wb') as temp_helmfile_path: + output_path = temp_helmfile_path + f".{output_format}" + + try: + # Command helmfile template + template_cmd = ["helmfile", "template", "-f", temp_helmfile_path] + + template_proc = subprocess.Popen( + template_cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + helm_output, helm_err = template_proc.communicate() + + if template_proc.returncode != 0 or has_fatal_error("", helm_err): + FileManager.cleanup_files(output_path) + return DiagramResult( + success=False, + error="Helmfile template failed. See command output below.", + command=" ".join(template_cmd), + stdout="", + stderr=helm_err or "" + ) + + # Command kube-diagrams + cmd = ["kube-diagrams", "-", "-o", output_path] + if without_namespace: + cmd.append("--without-namespace") + if extra_args.strip(): + cmd.extend(parse_extra_args(extra_args)) + + kube_proc = subprocess.run( + cmd, + input=helm_output, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + + stdout_output = kube_proc.stdout or "" + stderr_output = kube_proc.stderr or "" + + if kube_proc.returncode != 0 or has_fatal_error(stdout_output, stderr_output): + FileManager.cleanup_files(output_path) + return DiagramResult( + success=False, + error="kube-diagrams failed", + command=f"{' '.join(template_cmd)} | {' '.join(cmd)}", + stdout=stdout_output, + stderr=stderr_output + ) + + if not os.path.exists(output_path): + return DiagramResult( + success=False, + error=f"Output file not found: {output_path}", + command=f"{' '.join(template_cmd)} | {' '.join(cmd)}", + stdout=stdout_output, + stderr=stderr_output + ) + + content = FileManager.read_file_content(output_path, binary=True) + encoded = encode_content(content, output_format) + + # Cleaning + FileManager.cleanup_files(output_path) + + return DiagramResult( + success=True, + diagram=encoded, + mime_type=MIME_TYPES.get(output_format, "application/octet-stream"), + filename=f"helmfile-diagram.{output_format}", + message="Helmfile diagram successfully generated.", + command=f"{' '.join(template_cmd)} | {' '.join(cmd)}", + stdout=stdout_output, + stderr=stderr_output + ) + + except ValueError as e: + FileManager.cleanup_files(output_path) + return DiagramResult( + success=False, + error=str(e), + command=" ".join(cmd) if 'cmd' in locals() else None + ) + except Exception as e: + FileManager.cleanup_files(output_path) + return DiagramResult( + success=False, + error=str(e), + command=" ".join(cmd) if 'cmd' in locals() else None + ) \ No newline at end of file diff --git a/webapp/backend/services/manifestService.py b/webapp/backend/services/manifestService.py new file mode 100644 index 0000000..8786ca9 --- /dev/null +++ b/webapp/backend/services/manifestService.py @@ -0,0 +1,99 @@ +"""Service for generating diagrams from Kubernetes manifests.""" +import subprocess + +from constants import MIME_TYPES +from .models import DiagramResult +from .file_manager import FileManager +from .utils import parse_extra_args, has_fatal_error, encode_content + + +def generate_from_manifest( + manifest_content: str, + output_format: str = "png", + extra_args: str = "", + without_namespace: bool = False +) -> DiagramResult: + """ + Generate a diagram from a Kubernetes manifest. + + Args: + manifest_content: Content of the manifest + output_format: Output format (png, svg, etc.) + extra_args: Additional arguments for kube-diagrams + without_namespace: Hide namespaces + + Returns: + DiagramResult: Result of the generation + """ + with FileManager.create_temp_file(manifest_content, suffix='.yaml') as tmp_manifest: + base_name = FileManager.get_base_name_from_path(tmp_manifest) + requested_output, png_output = FileManager.get_output_paths(tmp_manifest, output_format) + + try: + # Command + cmd = ["kube-diagrams", tmp_manifest, "-o", requested_output] + if without_namespace: + cmd.append("--without-namespace") + if extra_args.strip(): + cmd.extend(parse_extra_args(extra_args)) + + # Execution + proc = subprocess.run(cmd, check=False, capture_output=True, text=True) + stdout_output = proc.stdout or "" + stderr_output = proc.stderr or "" + + # Error verification + if proc.returncode != 0 or has_fatal_error(stdout_output, stderr_output): + FileManager.cleanup_files(requested_output, png_output) + return DiagramResult( + success=False, + error="KubeDiagrams failed. See command output below.", + command=" ".join(cmd), + stdout=stdout_output, + stderr=stderr_output + ) + + # Output file verification + output_info = FileManager.find_output_file(requested_output, png_output) + if not output_info: + return DiagramResult( + success=False, + error=f"Output file not found (looked for {requested_output} and {png_output}).", + command=" ".join(cmd), + stdout=stdout_output, + stderr=stderr_output + ) + + output_file, produced_format = output_info + content = FileManager.read_file_content(output_file, binary=True) + encoded = encode_content(content, produced_format) + + # Cleaning + FileManager.cleanup_files(requested_output, png_output) + + return DiagramResult( + success=True, + diagram=encoded, + mime_type=MIME_TYPES.get(produced_format, "application/octet-stream"), + filename=f"{base_name}.{produced_format}", + message="Diagram successfully generated.", + command=" ".join(cmd), + stdout=stdout_output, + stderr=stderr_output + ) + + except ValueError as e: + FileManager.cleanup_files(requested_output, png_output) + return DiagramResult( + success=False, + error=str(e), + command=" ".join(cmd) if 'cmd' in locals() else None + ) + except Exception as e: + FileManager.cleanup_files(requested_output, png_output) + return DiagramResult( + success=False, + error=f"Internal error: {e}", + command=" ".join(cmd) if 'cmd' in locals() else None + ) + diff --git a/webapp/backend/services/models.py b/webapp/backend/services/models.py new file mode 100644 index 0000000..0b378a9 --- /dev/null +++ b/webapp/backend/services/models.py @@ -0,0 +1,43 @@ +"""Service Data Model""" +from typing import Optional +class DiagramResult: + """Data model for the diagram result.""" + def __init__( + self, + success: bool, + diagram: Optional[str] = None, + mime_type: Optional[str] = None, + filename: Optional[str] = None, + message: Optional[str] = None, + error: Optional[str] = None, + command: Optional[str] = None, + stdout: Optional[str] = None, + stderr: Optional[str] = None + ): + self.success = success + self.diagram = diagram + self.mime_type = mime_type + self.filename = filename + self.message = message + self.error = error + self.command = command + self.stdout = stdout + self.stderr = stderr + + def to_dict(self) -> dict: + """Converts the DiagramResult to a dictionary.""" + result = { + "command": self.command, + "stdout": self.stdout or "", + "stderr": self.stderr or "" + } + if self.success: + result.update({ + "diagram": self.diagram, + "mimeType": self.mime_type, + "filename": self.filename, + "message": self.message + }) + else: + result["error"] = self.error + return result diff --git a/webapp/backend/services/utils.py b/webapp/backend/services/utils.py new file mode 100644 index 0000000..3e298c6 --- /dev/null +++ b/webapp/backend/services/utils.py @@ -0,0 +1,57 @@ +"""Utilitaires pour les services de génération de diagrammes.""" +import base64 +import shlex + +from constants import TEXT_FORMATS + + +def has_fatal_error(stdout_txt: str, stderr_txt: str) -> bool: + """ + Vérifie si la sortie contient une erreur fatale. + + Args: + stdout_txt: Sortie standard + stderr_txt: Sortie d'erreur + + Returns: + bool: True si erreur fatale détectée + """ + return ("error:" in (stdout_txt or "").lower()) or ("error:" in (stderr_txt or "").lower()) + + +def parse_extra_args(extra_args: str) -> list[str]: + """ + Parse les arguments supplémentaires. + + Args: + extra_args: Arguments supplémentaires en string + + Returns: + list[str]: Liste des arguments parsés + + Raises: + ValueError: Si les arguments sont invalides + """ + if not extra_args or not extra_args.strip(): + return [] + try: + return shlex.split(extra_args.strip()) + except Exception as e: + raise ValueError(f"Invalid extraArgs: {e}") + + +def encode_content(content: bytes, output_format: str) -> str: + """ + Encode le contenu en base64 ou UTF-8 selon le format. + + Args: + content: Contenu à encoder + output_format: Format de sortie + + Returns: + str: Contenu encodé + """ + if output_format in TEXT_FORMATS: + return content.decode("utf-8") + return base64.b64encode(content).decode("utf-8") + diff --git a/webapp/backend/utils/__init__.py b/webapp/backend/utils/__init__.py new file mode 100644 index 0000000..61563e3 --- /dev/null +++ b/webapp/backend/utils/__init__.py @@ -0,0 +1,17 @@ +"""Package utils.""" +from .logger import AppLogger, get_app_logger +from .validators import InputValidator, ValidationError +from .response_builder import ResponseBuilder +from .access_logger import get_real_ip, log_request, log_request_compact + +__all__ = [ + 'AppLogger', + 'get_app_logger', + 'InputValidator', + 'ValidationError', + 'ResponseBuilder', + 'get_real_ip', + 'log_request', + 'log_request_compact' +] + diff --git a/webapp/backend/utils/__pycache__/__init__.cpython-312.pyc b/webapp/backend/utils/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..96d0265 Binary files /dev/null and b/webapp/backend/utils/__pycache__/__init__.cpython-312.pyc differ diff --git a/webapp/backend/utils/__pycache__/access_logger.cpython-312.pyc b/webapp/backend/utils/__pycache__/access_logger.cpython-312.pyc new file mode 100644 index 0000000..8055a17 Binary files /dev/null and b/webapp/backend/utils/__pycache__/access_logger.cpython-312.pyc differ diff --git a/webapp/backend/utils/__pycache__/logger.cpython-312.pyc b/webapp/backend/utils/__pycache__/logger.cpython-312.pyc new file mode 100644 index 0000000..c4379b3 Binary files /dev/null and b/webapp/backend/utils/__pycache__/logger.cpython-312.pyc differ diff --git a/webapp/backend/utils/__pycache__/response_builder.cpython-312.pyc b/webapp/backend/utils/__pycache__/response_builder.cpython-312.pyc new file mode 100644 index 0000000..13e7dd0 Binary files /dev/null and b/webapp/backend/utils/__pycache__/response_builder.cpython-312.pyc differ diff --git a/webapp/backend/utils/__pycache__/validators.cpython-312.pyc b/webapp/backend/utils/__pycache__/validators.cpython-312.pyc new file mode 100644 index 0000000..d4631f5 Binary files /dev/null and b/webapp/backend/utils/__pycache__/validators.cpython-312.pyc differ diff --git a/webapp/backend/utils/access_logger.py b/webapp/backend/utils/access_logger.py new file mode 100644 index 0000000..c3d23d1 --- /dev/null +++ b/webapp/backend/utils/access_logger.py @@ -0,0 +1,283 @@ +"""access logging for Flask application.""" +import logging +from datetime import datetime +from flask import request, g +from typing import Optional +from config import Config + + +def is_valid_ip(ip: str) -> bool: + """ + Check if the IP address is valid and not a placeholder. + + Args: + ip: IP address string to validate + + Returns: + bool: True if IP is valid, False otherwise + """ + if not ip: + return False + + ip = ip.strip() + + # List of invalid placeholder values (not real IPs) + invalid_values = [ + '', + '-', + '(null)', + 'null', + 'None', + 'unknown', + ] + + if ip.lower() in [v.lower() for v in invalid_values]: + return False + + return True + + +def normalize_ip(ip: str) -> str: + """ + Normalize IP address to IPv4 format when possible. + Converts IPv6 localhost (::1) to IPv4 (127.0.0.1). + + Args: + ip: IP address string + + Returns: + str: Normalized IP address + """ + if not ip: + return '-' + + ip = ip.strip() + + # Convert IPv6 localhost to IPv4 + if ip == '::1': + return '127.0.0.1' + + # Convert IPv6-mapped IPv4 addresses (::ffff:192.168.1.1 -> 192.168.1.1) + if ip.startswith('::ffff:'): + return ip[7:] + + return ip + + +def get_real_ip() -> str: + """ + Get the real IP address of the client, handling proxies and load balancers. + + When using ProxyFix middleware (Config.BEHIND_PROXY=True), request.remote_addr + is automatically set to the real client IP from X-Forwarded-For headers. + + For additional safety, we also check common proxy headers manually: + 1. X-Forwarded-For (most common with reverse proxies) + 2. X-Real-IP (nginx) + 3. CF-Connecting-IP (Cloudflare) + 4. True-Client-IP (Akamai and some CDNs) + 5. request.remote_addr (fallback / already fixed by ProxyFix) + + Returns: + str: The real IP address of the client (normalized to IPv4 when possible) + """ + # X-Forwarded-For can contain multiple IPs: "client, proxy1, proxy2" + # The first one is the original client + x_forwarded_for = request.headers.get('X-Forwarded-For') + if x_forwarded_for: + # Take the first IP in the chain (the original client) + ip = x_forwarded_for.split(',')[0].strip() + if is_valid_ip(ip): + return normalize_ip(ip) + + # X-Real-IP is used by nginx + x_real_ip = request.headers.get('X-Real-IP') + if x_real_ip and is_valid_ip(x_real_ip): + return normalize_ip(x_real_ip.strip()) + + # Cloudflare specific header + cf_connecting_ip = request.headers.get('CF-Connecting-IP') + if cf_connecting_ip and is_valid_ip(cf_connecting_ip): + return normalize_ip(cf_connecting_ip.strip()) + + # Akamai and some CDNs + true_client_ip = request.headers.get('True-Client-IP') + if true_client_ip and is_valid_ip(true_client_ip): + return normalize_ip(true_client_ip.strip()) + + # X-Client-IP (some load balancers) + x_client_ip = request.headers.get('X-Client-IP') + if x_client_ip and is_valid_ip(x_client_ip): + return normalize_ip(x_client_ip.strip()) + + # Fallback to remote_addr (will be correct if ProxyFix is enabled) + remote_addr = request.remote_addr + if remote_addr and is_valid_ip(remote_addr): + return normalize_ip(remote_addr) + + # Ultimate fallback + return '-' + + +def get_all_ip_headers() -> dict: + """ + Get all IP-related headers for debugging purposes. + + Returns: + dict: Dictionary of all IP-related headers and their values + """ + headers = { + 'remote_addr': request.remote_addr, + 'X-Forwarded-For': request.headers.get('X-Forwarded-For'), + 'X-Real-IP': request.headers.get('X-Real-IP'), + 'CF-Connecting-IP': request.headers.get('CF-Connecting-IP'), + 'True-Client-IP': request.headers.get('True-Client-IP'), + 'X-Client-IP': request.headers.get('X-Client-IP'), + 'X-Forwarded-Proto': request.headers.get('X-Forwarded-Proto'), + 'X-Forwarded-Host': request.headers.get('X-Forwarded-Host'), + } + return {k: v for k, v in headers.items() if v is not None} + + +def get_user_agent() -> str: + """ + Get the User-Agent header. + + Returns: + str: User-Agent string or '-' if not present + """ + return request.headers.get('User-Agent', '-') + + +def get_referer() -> str: + """ + Get the Referer header. + + Returns: + str: Referer URL or '-' if not present + """ + return request.headers.get('Referer', '-') + + +def format_apache_log( + ip: str, + method: str, + path: str, + protocol: str, + status_code: int, + response_size: int, + referer: str, + user_agent: str, + request_time: Optional[float] = None +) -> str: + """ + Format log entry in Apache Combined Log Format with additional info. + + Apache Combined Log Format: + IP - - [datetime] "METHOD PATH PROTOCOL" STATUS SIZE "REFERER" "USER-AGENT" + + We add request time at the end for performance monitoring. + + Args: + ip: Client IP address + method: HTTP method (GET, POST, etc.) + path: Request path + protocol: HTTP protocol version + status_code: HTTP status code + response_size: Response size in bytes + referer: Referer header + user_agent: User-Agent header + request_time: Request processing time in milliseconds (optional) + + Returns: + str: Formatted log entry + """ + # Format datetime in Apache format: [10/Oct/2000:13:55:36 +0000] + now = datetime.now() + timestamp = now.strftime('[%d/%b/%Y:%H:%M:%S +0000]') + + # Escape quotes in referer and user-agent + referer = referer.replace('"', '\\"') + user_agent = user_agent.replace('"', '\\"') + + # Format: IP - - [datetime] "METHOD PATH PROTOCOL" STATUS SIZE "REFERER" "USER-AGENT" + log_line = ( + f'{ip} - - {timestamp} ' + f'"{method} {path} {protocol}" ' + f'{status_code} {response_size} ' + f'"{referer}" "{user_agent}"' + ) + + # Add request time if available + if request_time is not None: + log_line += f' {request_time:.2f}ms' + + return log_line + + +def log_request(status_code: int, response_size: int = 0): + """ + Log an HTTP request in Apache Combined Log Format. + + Args: + status_code: HTTP status code + response_size: Response size in bytes (default: 0) + """ + try: + ip = get_real_ip() + method = request.method + path = request.path + protocol = request.environ.get('SERVER_PROTOCOL', 'HTTP/1.1') + referer = get_referer() + user_agent = get_user_agent() + + # Calculate request time if available + request_time = None + if hasattr(g, 'request_start_time'): + from time import time + request_time = (time() - g.request_start_time) * 1000 # Convert to ms + + # Format and log + log_line = format_apache_log( + ip=ip, + method=method, + path=path, + protocol=protocol, + status_code=status_code, + response_size=response_size, + referer=referer, + user_agent=user_agent, + request_time=request_time + ) + + logger = logging.getLogger(Config.LOGGER_NAME) + logger.info(log_line) + + except Exception as e: + # Don't let logging errors break the application + error_logger = logging.getLogger('app') + error_logger.error(f"Error logging request: {e}") + + +def log_request_compact(status_code: int, params_summary: str = ""): + """ + Log an HTTP request with a compact parameter summary (for backward compatibility). + This can be used for detailed parameter logging alongside the Apache-style log. + + Args: + status_code: HTTP status code + params_summary: Compact summary of request parameters + """ + try: + ip = get_real_ip() + path = request.path + + # Compact format: IP ; PATH ; PARAMS + log_line = f"{ip} ; {path} ; {params_summary}" + + logger = logging.getLogger(Config.LOGGER_NAME) + logger.info(log_line) + + except Exception as e: + error_logger = logging.getLogger('app') + error_logger.error(f"Error logging request: {e}") diff --git a/webapp/backend/utils/logger.py b/webapp/backend/utils/logger.py new file mode 100644 index 0000000..88f8823 --- /dev/null +++ b/webapp/backend/utils/logger.py @@ -0,0 +1,108 @@ +"""Configuration for application logging.""" +import logging +import sys +from logging.handlers import RotatingFileHandler +from pathlib import Path + + +class AppLogger: + """Logging manager for the application.""" + + _loggers = {} + + @classmethod + def setup_logger( + cls, + name: str, + log_file: str = None, + level: int = logging.INFO, + max_bytes: int = 10 * 1024 * 1024, # 10MB + backup_count: int = 5 + ) -> logging.Logger: + """ + Configure and return a logger. + + Args: + name: Name of the logger + log_file: Log File Path (optional) + level: Logging level + max_bytes: Maximum log file size before rotation + backup_count: Number of backup files to keep + + Returns: + logging.Logger: Logger configured + """ + # To not recreate logger if already exist + if name in cls._loggers: + return cls._loggers[name] + + logger = logging.getLogger(name) + logger.setLevel(level) + + # Avoid adding multiple handlers to the same logger + if logger.handlers: + return logger + + formatter = logging.Formatter( + '%(asctime)s - %(name)s - %(levelname)s - %(message)s', + datefmt='%Y-%m-%d %H:%M:%S' + ) + + # Console handler + console_handler = logging.StreamHandler(sys.stdout) + console_handler.setLevel(level) + console_handler.setFormatter(formatter) + logger.addHandler(console_handler) + + # Handler file if needed + if log_file: + # Create directory if needed + log_path = Path(log_file) + log_path.parent.mkdir(parents=True, exist_ok=True) + + file_handler = RotatingFileHandler( + log_file, + maxBytes=max_bytes, + backupCount=backup_count, + encoding='utf-8' + ) + file_handler.setLevel(level) + file_handler.setFormatter(formatter) + logger.addHandler(file_handler) + + cls._loggers[name] = logger + return logger + + @classmethod + def get_logger(cls, name: str) -> logging.Logger: + """ + Retrieve an existing logger or create a new one. + + Args: + name: Name of the logger + + Returns: + logging.Logger: Logger + """ + if name in cls._loggers: + return cls._loggers[name] + return cls.setup_logger(name) + + +def get_app_logger(name: str = None) -> logging.Logger: + """ + Helper function to obtain a logger. + + Args: + name: Name of the logger (uses the name of the calling module by default) + + Returns: + logging.Logger: Logger configured + """ + if name is None: + import inspect + frame = inspect.currentframe().f_back + name = frame.f_globals.get('__name__', 'app') + + return AppLogger.get_logger(name) + diff --git a/webapp/backend/utils/response_builder.py b/webapp/backend/utils/response_builder.py new file mode 100644 index 0000000..cbaee48 --- /dev/null +++ b/webapp/backend/utils/response_builder.py @@ -0,0 +1,172 @@ +"""Http response builder.""" +from typing import Any, Dict, Optional +from flask import jsonify, Response + + +class ResponseBuilder: + """Http response builder.""" + + @staticmethod + def success( + data: Any = None, + message: str = "Success", + status_code: int = 200 + ) -> tuple[Response, int]: + """ + Create a standardized success response. + + Args: + data: Data to be returned + message: Success message + status_code: HTTP status code + + Returns: + tuple[Response, int]: Flask response and status code + """ + response = { + "success": True, + "message": message + } + + if data is not None: + if isinstance(data, dict): + response.update(data) + else: + response["data"] = data + + return jsonify(response), status_code + + @staticmethod + def error( + error: str, + details: Optional[Dict[str, Any]] = None, + status_code: int = 400 + ) -> tuple[Response, int]: + """ + Creates a standard error response. + + Args: + error: Error message + details: Additional details + status_code: HTTP status code + + Returns: + tuple[Response, int]: Flask response and status code + """ + response = { + "success": False, + "error": error + } + + if details: + response.update(details) + + return jsonify(response), status_code + + @staticmethod + def validation_error( + field: str, + message: str, + status_code: int = 400 + ) -> tuple[Response, int]: + """ + Creates a validation error response. + + Args: + field: Field in error + message: Error message + status_code: HTTP status code + + Returns: + tuple[Response, int]: Flask response and status code + """ + return ResponseBuilder.error( + error=message, + details={"field": field, "type": "validation_error"}, + status_code=status_code + ) + + @staticmethod + def diagram_success( + diagram: str, + mime_type: str, + filename: str, + message: str = "Diagram successfully generated.", + command: str = None, + stdout: str = "", + stderr: str = "" + ) -> tuple[Response, int]: + """ + Creates a success response for the diagram generation. + + Args: + diagram: Content of the diagram (encoded) + mime_type: MIME type of the diagram + filename: File name + message: Success message + command: Command executed + stdout: Standard output + stderr: Error output + + Returns: + tuple[Response, int]: Flask response and status code + """ + response_data = { + "diagram": diagram, + "mimeType": mime_type, + "filename": filename, + "message": message, + "command": command, + "stdout": stdout or "", + "stderr": stderr or "" + } + + return ResponseBuilder.success(data=response_data, message=message, status_code=200) + + @staticmethod + def diagram_error( + error: str, + command: str = None, + stdout: str = "", + stderr: str = "", + status_code: int = 400 + ) -> tuple[Response, int]: + """ + Creates an error response for diagram generation. + + Args: + error: Error message + command: Command executed + stdout: Standard output + stderr: Error output + status_code: HTTP status code + + Returns: + tuple[Response, int]: Flask response and status code + """ + details = { + "command": command, + "stdout": stdout or "", + "stderr": stderr or "" + } + + return ResponseBuilder.error( + error=error, + details=details, + status_code=status_code + ) + + @staticmethod + def from_diagram_result(result) -> tuple[Response, int]: + """ + Create a response from a DiagramResult. + + Args: + result: Instance of DiagramResult + + Returns: + tuple[Response, int]: Flask response and status code + """ + status_code = 200 if result.success else 400 + return jsonify(result.to_dict()), status_code + diff --git a/webapp/backend/utils/validators.py b/webapp/backend/utils/validators.py new file mode 100644 index 0000000..d75835f --- /dev/null +++ b/webapp/backend/utils/validators.py @@ -0,0 +1,183 @@ +"""Validation of user inputs.""" +import re +from typing import Optional, Tuple +from constants import MANIFEST_RE, KIND_RE + + +class ValidationError(Exception): + """Exception error validation.""" + pass + + +class InputValidator: + """Validator for user inputs.""" + + SUPPORTED_FORMATS = ['png', 'jpg', 'jpeg', 'gif', 'svg', 'pdf', 'dot', 'dot_json'] + + # Valid Pattern for url + HELM_URL_PATTERN = re.compile( + r'^(https?://|oci://|file://|[a-zA-Z0-9\-_]+/[a-zA-Z0-9\-_]+)' + ) + + @classmethod + def validate_manifest(cls, content: str) -> Tuple[bool, Optional[str]]: + """ + Validate a Kubernetes manifest content. + + Args: + content: Manifest content + + Returns: + Tuple[bool, Optional[str]]: (is_valid, error_message) + """ + if not content or not content.strip(): + return False, "Manifest content cannot be empty." + + # Verify minimum length + if len(content.strip()) < 10: + return False, "Manifest content is too short." + + # Verify it looks like a manifest K8s + if not cls.looks_like_manifest(content): + return False, "Content does not appear to be a valid Kubernetes manifest (missing apiVersion or kind)." + + return True, None + + @classmethod + def validate_helmfile(cls, content: str) -> Tuple[bool, Optional[str]]: + """ + Validate a Helmfile content. + + Args: + content: Content of a Helmfile + + Returns: + Tuple[bool, Optional[str]]: (is_valid, error_message) + """ + if not content or not content.strip(): + return False, "Helmfile content cannot be empty." + + if len(content.strip()) < 10: + return False, "Helmfile content is too short." + + # Verify it looks like a Helmfile + if not cls.looks_like_helmfile(content): + return False, "Content does not appear to be a valid Helmfile (missing typical Helmfile keys)." + + return True, None + + @classmethod + def validate_helm_chart_url(cls, url: str) -> Tuple[bool, Optional[str]]: + """ + Validate a Helm chart URL. + + Args: + url: URL of the Helm chart + + Returns: + Tuple[bool, Optional[str]]: (est_valide, message_erreur) + """ + if not url or not url.strip(): + return False, "Chart URL cannot be empty." + + url = url.strip() + + # Verify the helm url pattern + if not cls.HELM_URL_PATTERN.match(url): + return False, "Invalid Helm chart URL format. Must start with http://, https://, oci://, file://, or be a chart reference." + + return True, None + + @classmethod + def validate_output_format(cls, format_str: str) -> Tuple[bool, Optional[str]]: + """ + Validate output format. + + Args: + format_str: output format + + Returns: + Tuple[bool, Optional[str]]: (is_valid, error_message) + """ + if not format_str: + return False, "Output format cannot be empty." + + format_str = format_str.lower().strip() + + if format_str not in cls.SUPPORTED_FORMATS: + return False, f"Unsupported output format '{format_str}'. Supported formats: {', '.join(cls.SUPPORTED_FORMATS)}" + + return True, None + + @classmethod + def validate_extra_args(cls, args: str) -> Tuple[bool, Optional[str]]: + """ + Validate extra args. + + Args: + args: extra_args + + Returns: + Tuple[bool, Optional[str]]: (is_valid, error_message) + """ + if not args or not args.strip(): + return True, None # Les args vides sont valides + + dangerous_chars = [';', '&', '|', '`', '$', '(', ')'] + for char in dangerous_chars: + if char in args: + return False, f"Extra args contain dangerous character '{char}'" + + return True, None + + @staticmethod + def looks_like_manifest(text: str) -> bool: + """ + Heuristic to detect a Kubernetes manifest. + + Args: + text: content that will be checked + + Returns: + bool: True if it looks like a manifest + """ + if not text: + return False + t = text.strip() + return bool(MANIFEST_RE.search(t) and KIND_RE.search(t)) + + @staticmethod + def looks_like_helmfile(text: str) -> bool: + """ + Heuristic to detect a HelmFile. + + Args: + text: Content that will be checked. + + Returns: + bool: True if it looks like a Helmfile + """ + if not text: + return False + t = text.lower() + helmfile_keys = ["\nreleases:", "\nrepositories:", "\nhelmdefaults:", "\nenvironments:", "\ntemplates:"] + return any(key in t for key in helmfile_keys) + + # TODO: Not used yet + @classmethod + def sanitize_filename(cls, filename: str) -> str: + """ + Clean a filename to make it safe. + + Args: + filename: Filename to clean + + Returns: + str: Filename cleaned + """ + safe_filename = re.sub(r'[^a-zA-Z0-9._-]', '_', filename) + # Length limit + if len(safe_filename) > 255: + safe_filename = safe_filename[:255] + return safe_filename + diff --git a/webapp/backend/venv/bin/Activate.ps1 b/webapp/backend/venv/bin/Activate.ps1 new file mode 100644 index 0000000..eeea358 --- /dev/null +++ b/webapp/backend/venv/bin/Activate.ps1 @@ -0,0 +1,247 @@ +<# +.Synopsis +Activate a Python virtual environment for the current PowerShell session. + +.Description +Pushes the python executable for a virtual environment to the front of the +$Env:PATH environment variable and sets the prompt to signify that you are +in a Python virtual environment. Makes use of the command line switches as +well as the `pyvenv.cfg` file values present in the virtual environment. + +.Parameter VenvDir +Path to the directory that contains the virtual environment to activate. The +default value for this is the parent of the directory that the Activate.ps1 +script is located within. + +.Parameter Prompt +The prompt prefix to display when this virtual environment is activated. By +default, this prompt is the name of the virtual environment folder (VenvDir) +surrounded by parentheses and followed by a single space (ie. '(.venv) '). + +.Example +Activate.ps1 +Activates the Python virtual environment that contains the Activate.ps1 script. + +.Example +Activate.ps1 -Verbose +Activates the Python virtual environment that contains the Activate.ps1 script, +and shows extra information about the activation as it executes. + +.Example +Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv +Activates the Python virtual environment located in the specified location. + +.Example +Activate.ps1 -Prompt "MyPython" +Activates the Python virtual environment that contains the Activate.ps1 script, +and prefixes the current prompt with the specified string (surrounded in +parentheses) while the virtual environment is active. + +.Notes +On Windows, it may be required to enable this Activate.ps1 script by setting the +execution policy for the user. You can do this by issuing the following PowerShell +command: + +PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser + +For more information on Execution Policies: +https://go.microsoft.com/fwlink/?LinkID=135170 + +#> +Param( + [Parameter(Mandatory = $false)] + [String] + $VenvDir, + [Parameter(Mandatory = $false)] + [String] + $Prompt +) + +<# Function declarations --------------------------------------------------- #> + +<# +.Synopsis +Remove all shell session elements added by the Activate script, including the +addition of the virtual environment's Python executable from the beginning of +the PATH variable. + +.Parameter NonDestructive +If present, do not remove this function from the global namespace for the +session. + +#> +function global:deactivate ([switch]$NonDestructive) { + # Revert to original values + + # The prior prompt: + if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) { + Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt + Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT + } + + # The prior PYTHONHOME: + if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) { + Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME + Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME + } + + # The prior PATH: + if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) { + Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH + Remove-Item -Path Env:_OLD_VIRTUAL_PATH + } + + # Just remove the VIRTUAL_ENV altogether: + if (Test-Path -Path Env:VIRTUAL_ENV) { + Remove-Item -Path env:VIRTUAL_ENV + } + + # Just remove VIRTUAL_ENV_PROMPT altogether. + if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) { + Remove-Item -Path env:VIRTUAL_ENV_PROMPT + } + + # Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether: + if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) { + Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force + } + + # Leave deactivate function in the global namespace if requested: + if (-not $NonDestructive) { + Remove-Item -Path function:deactivate + } +} + +<# +.Description +Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the +given folder, and returns them in a map. + +For each line in the pyvenv.cfg file, if that line can be parsed into exactly +two strings separated by `=` (with any amount of whitespace surrounding the =) +then it is considered a `key = value` line. The left hand string is the key, +the right hand is the value. + +If the value starts with a `'` or a `"` then the first and last character is +stripped from the value before being captured. + +.Parameter ConfigDir +Path to the directory that contains the `pyvenv.cfg` file. +#> +function Get-PyVenvConfig( + [String] + $ConfigDir +) { + Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg" + + # Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue). + $pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue + + # An empty map will be returned if no config file is found. + $pyvenvConfig = @{ } + + if ($pyvenvConfigPath) { + + Write-Verbose "File exists, parse `key = value` lines" + $pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath + + $pyvenvConfigContent | ForEach-Object { + $keyval = $PSItem -split "\s*=\s*", 2 + if ($keyval[0] -and $keyval[1]) { + $val = $keyval[1] + + # Remove extraneous quotations around a string value. + if ("'""".Contains($val.Substring(0, 1))) { + $val = $val.Substring(1, $val.Length - 2) + } + + $pyvenvConfig[$keyval[0]] = $val + Write-Verbose "Adding Key: '$($keyval[0])'='$val'" + } + } + } + return $pyvenvConfig +} + + +<# Begin Activate script --------------------------------------------------- #> + +# Determine the containing directory of this script +$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition +$VenvExecDir = Get-Item -Path $VenvExecPath + +Write-Verbose "Activation script is located in path: '$VenvExecPath'" +Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)" +Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)" + +# Set values required in priority: CmdLine, ConfigFile, Default +# First, get the location of the virtual environment, it might not be +# VenvExecDir if specified on the command line. +if ($VenvDir) { + Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values" +} +else { + Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir." + $VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/") + Write-Verbose "VenvDir=$VenvDir" +} + +# Next, read the `pyvenv.cfg` file to determine any required value such +# as `prompt`. +$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir + +# Next, set the prompt from the command line, or the config file, or +# just use the name of the virtual environment folder. +if ($Prompt) { + Write-Verbose "Prompt specified as argument, using '$Prompt'" +} +else { + Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value" + if ($pyvenvCfg -and $pyvenvCfg['prompt']) { + Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'" + $Prompt = $pyvenvCfg['prompt']; + } + else { + Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virtual environment)" + Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'" + $Prompt = Split-Path -Path $venvDir -Leaf + } +} + +Write-Verbose "Prompt = '$Prompt'" +Write-Verbose "VenvDir='$VenvDir'" + +# Deactivate any currently active virtual environment, but leave the +# deactivate function in place. +deactivate -nondestructive + +# Now set the environment variable VIRTUAL_ENV, used by many tools to determine +# that there is an activated venv. +$env:VIRTUAL_ENV = $VenvDir + +if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) { + + Write-Verbose "Setting prompt to '$Prompt'" + + # Set the prompt to include the env name + # Make sure _OLD_VIRTUAL_PROMPT is global + function global:_OLD_VIRTUAL_PROMPT { "" } + Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT + New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt + + function global:prompt { + Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) " + _OLD_VIRTUAL_PROMPT + } + $env:VIRTUAL_ENV_PROMPT = $Prompt +} + +# Clear PYTHONHOME +if (Test-Path -Path Env:PYTHONHOME) { + Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME + Remove-Item -Path Env:PYTHONHOME +} + +# Add the venv to the PATH +Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH +$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH" diff --git a/webapp/backend/venv/bin/activate b/webapp/backend/venv/bin/activate new file mode 100644 index 0000000..411bbab --- /dev/null +++ b/webapp/backend/venv/bin/activate @@ -0,0 +1,70 @@ +# This file must be used with "source bin/activate" *from bash* +# You cannot run it directly + +deactivate () { + # reset old environment variables + if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then + PATH="${_OLD_VIRTUAL_PATH:-}" + export PATH + unset _OLD_VIRTUAL_PATH + fi + if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then + PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}" + export PYTHONHOME + unset _OLD_VIRTUAL_PYTHONHOME + fi + + # Call hash to forget past commands. Without forgetting + # past commands the $PATH changes we made may not be respected + hash -r 2> /dev/null + + if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then + PS1="${_OLD_VIRTUAL_PS1:-}" + export PS1 + unset _OLD_VIRTUAL_PS1 + fi + + unset VIRTUAL_ENV + unset VIRTUAL_ENV_PROMPT + if [ ! "${1:-}" = "nondestructive" ] ; then + # Self destruct! + unset -f deactivate + fi +} + +# unset irrelevant variables +deactivate nondestructive + +# on Windows, a path can contain colons and backslashes and has to be converted: +if [ "${OSTYPE:-}" = "cygwin" ] || [ "${OSTYPE:-}" = "msys" ] ; then + # transform D:\path\to\venv to /d/path/to/venv on MSYS + # and to /cygdrive/d/path/to/venv on Cygwin + export VIRTUAL_ENV=$(cygpath /home/spirals/Documents/Workspace/Inria/kubediagramswebui/backend/venv) +else + # use the path as-is + export VIRTUAL_ENV=/home/spirals/Documents/Workspace/Inria/kubediagramswebui/backend/venv +fi + +_OLD_VIRTUAL_PATH="$PATH" +PATH="$VIRTUAL_ENV/"bin":$PATH" +export PATH + +# unset PYTHONHOME if set +# this will fail if PYTHONHOME is set to the empty string (which is bad anyway) +# could use `if (set -u; : $PYTHONHOME) ;` in bash +if [ -n "${PYTHONHOME:-}" ] ; then + _OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}" + unset PYTHONHOME +fi + +if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then + _OLD_VIRTUAL_PS1="${PS1:-}" + PS1='(venv) '"${PS1:-}" + export PS1 + VIRTUAL_ENV_PROMPT='(venv) ' + export VIRTUAL_ENV_PROMPT +fi + +# Call hash to forget past commands. Without forgetting +# past commands the $PATH changes we made may not be respected +hash -r 2> /dev/null diff --git a/webapp/backend/venv/bin/activate.csh b/webapp/backend/venv/bin/activate.csh new file mode 100644 index 0000000..333e6da --- /dev/null +++ b/webapp/backend/venv/bin/activate.csh @@ -0,0 +1,27 @@ +# This file must be used with "source bin/activate.csh" *from csh*. +# You cannot run it directly. + +# Created by Davide Di Blasi . +# Ported to Python 3.3 venv by Andrew Svetlov + +alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate' + +# Unset irrelevant variables. +deactivate nondestructive + +setenv VIRTUAL_ENV /home/spirals/Documents/Workspace/Inria/kubediagramswebui/backend/venv + +set _OLD_VIRTUAL_PATH="$PATH" +setenv PATH "$VIRTUAL_ENV/"bin":$PATH" + + +set _OLD_VIRTUAL_PROMPT="$prompt" + +if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then + set prompt = '(venv) '"$prompt" + setenv VIRTUAL_ENV_PROMPT '(venv) ' +endif + +alias pydoc python -m pydoc + +rehash diff --git a/webapp/backend/venv/bin/activate.fish b/webapp/backend/venv/bin/activate.fish new file mode 100644 index 0000000..ee9c44a --- /dev/null +++ b/webapp/backend/venv/bin/activate.fish @@ -0,0 +1,69 @@ +# This file must be used with "source /bin/activate.fish" *from fish* +# (https://fishshell.com/). You cannot run it directly. + +function deactivate -d "Exit virtual environment and return to normal shell environment" + # reset old environment variables + if test -n "$_OLD_VIRTUAL_PATH" + set -gx PATH $_OLD_VIRTUAL_PATH + set -e _OLD_VIRTUAL_PATH + end + if test -n "$_OLD_VIRTUAL_PYTHONHOME" + set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME + set -e _OLD_VIRTUAL_PYTHONHOME + end + + if test -n "$_OLD_FISH_PROMPT_OVERRIDE" + set -e _OLD_FISH_PROMPT_OVERRIDE + # prevents error when using nested fish instances (Issue #93858) + if functions -q _old_fish_prompt + functions -e fish_prompt + functions -c _old_fish_prompt fish_prompt + functions -e _old_fish_prompt + end + end + + set -e VIRTUAL_ENV + set -e VIRTUAL_ENV_PROMPT + if test "$argv[1]" != "nondestructive" + # Self-destruct! + functions -e deactivate + end +end + +# Unset irrelevant variables. +deactivate nondestructive + +set -gx VIRTUAL_ENV /home/spirals/Documents/Workspace/Inria/kubediagramswebui/backend/venv + +set -gx _OLD_VIRTUAL_PATH $PATH +set -gx PATH "$VIRTUAL_ENV/"bin $PATH + +# Unset PYTHONHOME if set. +if set -q PYTHONHOME + set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME + set -e PYTHONHOME +end + +if test -z "$VIRTUAL_ENV_DISABLE_PROMPT" + # fish uses a function instead of an env var to generate the prompt. + + # Save the current fish_prompt function as the function _old_fish_prompt. + functions -c fish_prompt _old_fish_prompt + + # With the original prompt function renamed, we can override with our own. + function fish_prompt + # Save the return status of the last command. + set -l old_status $status + + # Output the venv prompt; color taken from the blue of the Python logo. + printf "%s%s%s" (set_color 4B8BBE) '(venv) ' (set_color normal) + + # Restore the return status of the previous command. + echo "exit $old_status" | . + # Output the original/"old" prompt. + _old_fish_prompt + end + + set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV" + set -gx VIRTUAL_ENV_PROMPT '(venv) ' +end diff --git a/webapp/backend/venv/bin/diagrams b/webapp/backend/venv/bin/diagrams new file mode 100755 index 0000000..06fcd39 --- /dev/null +++ b/webapp/backend/venv/bin/diagrams @@ -0,0 +1,8 @@ +#!/home/spirals/Documents/Workspace/Inria/kubediagramswebui/backend/venv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from diagrams.cli import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/webapp/backend/venv/bin/flask b/webapp/backend/venv/bin/flask new file mode 100755 index 0000000..b3f8989 --- /dev/null +++ b/webapp/backend/venv/bin/flask @@ -0,0 +1,8 @@ +#!/home/spirals/Documents/Workspace/Inria/kubediagramswebui/backend/venv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from flask.cli import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/webapp/backend/venv/bin/gunicorn b/webapp/backend/venv/bin/gunicorn new file mode 100755 index 0000000..4db2193 --- /dev/null +++ b/webapp/backend/venv/bin/gunicorn @@ -0,0 +1,8 @@ +#!/home/spirals/Documents/Workspace/Inria/kubediagramswebui/backend/venv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from gunicorn.app.wsgiapp import run +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(run()) diff --git a/webapp/backend/venv/bin/helm-diagrams b/webapp/backend/venv/bin/helm-diagrams new file mode 100755 index 0000000..1567153 --- /dev/null +++ b/webapp/backend/venv/bin/helm-diagrams @@ -0,0 +1,136 @@ +#!/bin/bash + +# helm-diagrams: A script to generate a diagram of an Helm chart +# using kube-diagrams from the output of 'helm template ' + +# Check if kube-diagrams is installed +if ! command -v kube-diagrams &>/dev/null; then + echo "Error: kube-diagrams is not installed. Please install it first." + exit 1 +fi + +# Check if helm is installed +if ! command -v helm &>/dev/null; then + echo "Error: helm is not installed. Please install it first." + exit 1 +fi + +# Display help message +show_help() { + echo "Usage: helm-diagrams [OPTIONS] [FLAGS]" + echo "" + echo "A script to generate a diagram of an Helm chart using kube-diagrams." + echo "" + echo "Options:" + echo " -o, --output Specify the output file for the diagram" + echo " -f, --format Specify the output format (e.g., png, svg)" + echo " --embed-all-icons Embed all icons into svg or dot_json output diagrams" + echo " -c, --config Specify the custom kube-diagrams configuration file" + echo " -h, --help Display this help message" + echo "" + echo "Any flag supported by helm template, e.g.:" + echo " -g, --generate-name Generate the name (and omit the NAME parameter)" + echo " --include-crds Include CRDs in the templated output" + echo " -l, --labels stringToString Labels that would be added to release metadata. Should be divided by comma. (default [])" + echo " --name-template string Specify template used to name the release" + echo " --set stringArray Set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)" + echo " --set-file stringArray Set values from respective files specified via the command line (can specify multiple or separate values with commas: key1=path1,key2=path2)" + echo " --set-json stringArray Set JSON values on the command line (can specify multiple or separate values with commas: key1=jsonval1,key2=jsonval2)" + echo " --set-literal stringArray Set a literal STRING value on the command line" + echo " --set-string stringArray Set STRING values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)" + echo " -f, --values strings Specify values in a YAML file or a URL (can specify multiple)" + echo " --version string Specify a version constraint for the chart version to use. This constraint can be a specific tag (e.g. 1.1.1) or it may reference a valid range (e.g. ^2.0.0). If this is not specified, the latest version is used" + echo "" + echo "Examples:" + echo " helm-diagrams https://charts.jetstack.io/cert-manager -o diagram.png" + echo " helm-diagrams https://charts.jetstack.io/cert-manager --set crds.enabled=true -o cert-manager.png" + echo " helm-diagrams oci://ghcr.io/argoproj/argo-helm/argo-cd -f svg" + echo " helm-diagrams --help" + exit 0 +} + +# Initialize variables +HELM_CHART_URL="" +HELM_ARGS=() +KUBE_DIAGRAMS_ARGS=("-" "--without-namespace") # Default to stdin indicator + +# Use the KubeDiagrams custom configuration if exist. +KD_CC="KubeDiagrams.yaml" +if [ -e $KD_CC ] +then + KUBE_DIAGRAMS_ARGS+=("--config" "$KD_CC") +fi + +# Parse arguments +while [[ $# -gt 0 ]]; do + case "$1" in + -h | --help) + show_help + ;; + -o | --output | -f | --format | -c | --config) + KUBE_DIAGRAMS_ARGS+=("$1") + if [[ -n "$2" ]]; then + KUBE_DIAGRAMS_ARGS+=("$2") + shift 2 + else + echo "Error: $1 requires a value!" + exit 64 + fi + ;; + --embed-all-icons) + KUBE_DIAGRAMS_ARGS+=("$1") + shift + ;; + *) + if [ -z "${HELM_CHART_URL}" ]; then + HELM_CHART_URL=$1 + repository=$(dirname ${HELM_CHART_URL}) + name=$(basename ${HELM_CHART_URL}) + KUBE_DIAGRAMS_ARGS+=("--output" "${name}") + else + HELM_ARGS+=("$1") + fi + shift + ;; + esac +done + +# Check if any Helm chart url was specified +if [ -z "${HELM_CHART_URL}" ]; then + echo "Error: At least one Helm chart URL must be specified!" + exit 64 +fi + +echo Download ${HELM_CHART_URL}... + +if [[ ${repository} == oci* ]]; then + # Deal with OCI registries. + # Execute helm and pipe to kube-diagrams with arguments + helm template ${name} ${HELM_CHART_URL} "${HELM_ARGS[@]}" | kube-diagrams "${KUBE_DIAGRAMS_ARGS[@]}" + EXIT_CODE=$? +elif [[ ${repository} == http* ]]; then + # Deal with HTTP registries. + + # Add a Helm repository. + rid=helm-diagrams-repo + helm repo add $rid $repository --force-update >/dev/null + + # Execute helm and pipe to kube-diagrams with arguments + helm template ${name} $rid/${name} "${HELM_ARGS[@]}" | kube-diagrams "${KUBE_DIAGRAMS_ARGS[@]}" + EXIT_CODE=$? + + # Remove the Helm repository. + helm repo remove $rid >/dev/null +else + # Deal with local charts + # Execute helm and pipe to kube-diagrams with arguments + helm template ${name} ${repository}/${name} "${HELM_ARGS[@]}" | kube-diagrams -o local-$name "${KUBE_DIAGRAMS_ARGS[@]}" + EXIT_CODE=$? +fi + +if [[ $EXIT_CODE -eq 0 ]]; then + echo "Diagram generated successfully" +else + echo "Error: Failed to generate diagram!" + exit 1 +fi diff --git a/webapp/backend/venv/bin/icons/apiservice.png b/webapp/backend/venv/bin/icons/apiservice.png new file mode 100644 index 0000000..88cba9b Binary files /dev/null and b/webapp/backend/venv/bin/icons/apiservice.png differ diff --git a/webapp/backend/venv/bin/icons/container.png b/webapp/backend/venv/bin/icons/container.png new file mode 100644 index 0000000..f0743a8 Binary files /dev/null and b/webapp/backend/venv/bin/icons/container.png differ diff --git a/webapp/backend/venv/bin/icons/csidriver.png b/webapp/backend/venv/bin/icons/csidriver.png new file mode 100644 index 0000000..3bea724 Binary files /dev/null and b/webapp/backend/venv/bin/icons/csidriver.png differ diff --git a/webapp/backend/venv/bin/icons/csinode.png b/webapp/backend/venv/bin/icons/csinode.png new file mode 100644 index 0000000..720860f Binary files /dev/null and b/webapp/backend/venv/bin/icons/csinode.png differ diff --git a/webapp/backend/venv/bin/icons/csisc.png b/webapp/backend/venv/bin/icons/csisc.png new file mode 100644 index 0000000..92d235b Binary files /dev/null and b/webapp/backend/venv/bin/icons/csisc.png differ diff --git a/webapp/backend/venv/bin/icons/eps.png b/webapp/backend/venv/bin/icons/eps.png new file mode 100644 index 0000000..68fc18c Binary files /dev/null and b/webapp/backend/venv/bin/icons/eps.png differ diff --git a/webapp/backend/venv/bin/icons/ic.png b/webapp/backend/venv/bin/icons/ic.png new file mode 100644 index 0000000..d0db94b Binary files /dev/null and b/webapp/backend/venv/bin/icons/ic.png differ diff --git a/webapp/backend/venv/bin/icons/lease.png b/webapp/backend/venv/bin/icons/lease.png new file mode 100644 index 0000000..d7ecd75 Binary files /dev/null and b/webapp/backend/venv/bin/icons/lease.png differ diff --git a/webapp/backend/venv/bin/icons/mwc.png b/webapp/backend/venv/bin/icons/mwc.png new file mode 100644 index 0000000..4604777 Binary files /dev/null and b/webapp/backend/venv/bin/icons/mwc.png differ diff --git a/webapp/backend/venv/bin/icons/pc.png b/webapp/backend/venv/bin/icons/pc.png new file mode 100644 index 0000000..b8a4146 Binary files /dev/null and b/webapp/backend/venv/bin/icons/pc.png differ diff --git a/webapp/backend/venv/bin/icons/pdb.png b/webapp/backend/venv/bin/icons/pdb.png new file mode 100644 index 0000000..fdd52fc Binary files /dev/null and b/webapp/backend/venv/bin/icons/pdb.png differ diff --git a/webapp/backend/venv/bin/icons/podtemplate.png b/webapp/backend/venv/bin/icons/podtemplate.png new file mode 100644 index 0000000..d66b91d Binary files /dev/null and b/webapp/backend/venv/bin/icons/podtemplate.png differ diff --git a/webapp/backend/venv/bin/icons/rc.png b/webapp/backend/venv/bin/icons/rc.png new file mode 100644 index 0000000..4f15c2b Binary files /dev/null and b/webapp/backend/venv/bin/icons/rc.png differ diff --git a/webapp/backend/venv/bin/icons/runtimeclass.png b/webapp/backend/venv/bin/icons/runtimeclass.png new file mode 100644 index 0000000..c0a4a42 Binary files /dev/null and b/webapp/backend/venv/bin/icons/runtimeclass.png differ diff --git a/webapp/backend/venv/bin/icons/vpa.png b/webapp/backend/venv/bin/icons/vpa.png new file mode 100644 index 0000000..17b39b8 Binary files /dev/null and b/webapp/backend/venv/bin/icons/vpa.png differ diff --git a/webapp/backend/venv/bin/icons/vwc.png b/webapp/backend/venv/bin/icons/vwc.png new file mode 100644 index 0000000..7d3118d Binary files /dev/null and b/webapp/backend/venv/bin/icons/vwc.png differ diff --git a/webapp/backend/venv/bin/identify-cli b/webapp/backend/venv/bin/identify-cli new file mode 100755 index 0000000..af8d59f --- /dev/null +++ b/webapp/backend/venv/bin/identify-cli @@ -0,0 +1,8 @@ +#!/home/spirals/Documents/Workspace/Inria/kubediagramswebui/backend/venv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from identify.cli import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/webapp/backend/venv/bin/kube-diagrams b/webapp/backend/venv/bin/kube-diagrams new file mode 100755 index 0000000..86af02b --- /dev/null +++ b/webapp/backend/venv/bin/kube-diagrams @@ -0,0 +1,1477 @@ +#!/home/spirals/Documents/Workspace/Inria/kubediagramswebui/backend/venv/bin/python3 +""" + KubeDiagrams main script. +""" + +import argparse +import copy +import importlib +import json +import os +import traceback +from pprint import pprint +import sys +import yaml +import diagrams +from diagrams import Edge, Cluster +from diagrams.aws.enablement import ManagedServices +from diagrams.custom import Custom +from diagrams.k8s.group import Namespace + +# According to https://github.com/yaml/pyyaml/issues/89, PyYAML raises +# yaml.constructor.ConstructorError: could not determine a constructor for +# the tag 'tag:yaml.org,2002:value' +# when loading a string equals to the unquoted value '='. +# A proposed fix is +yaml.SafeLoader.yaml_implicit_resolvers.pop("=") + +# +# Extensions of diagrams framework +# + +# All dot output formats are listed in https://graphviz.org/docs/outputs/ +# If you need a format not listed below, just add it below. +SUPPORTED_OUTPUT_FORMATS = ( + "dot", "dot_json", "gif", "jp2", "jpe", "jpeg", "jpg", "pdf", "png", + "svg", "tif", "tiff" +) +class Diagram(diagrams.Diagram): + """ + Enhancement of the Diagram class to add new output formats. + """ + # pylint: disable-next=unused-private-member + __outformats = SUPPORTED_OUTPUT_FORMATS + +# Inspired from https://github.com/mingrammer/diagrams/pull/853 +def icon(node: object, label: str, size=64): + """ + Function adds a Diagrams-compatible icon + + :param node: Diagrams object, like VPC or Docker + :param label: Label text, like "subnet-a" + :param size: Icon size in px. + :returns: "Label prefixed with a specified icon" + """ + # pylint: disable-next=too-few-public-methods + class Node(node): + """ + Overloading Node class. + """ + def __init__(self): + """ + Initialisation. + """ + # pass # do nothing! + + # pylint: disable-next=protected-access + icon_path = Node()._load_icon() + return '<
' \ + + label + '
>' + +# Maximum length for diagram node labels +MAX_NODE_LABEL_LENGTH = 16 + +def query_path(data, path, default=None): + """ + Query YAML data. + """ + paths = path.split(".") + for p in paths[:-1]: + data = data.get(p) + if data is None: + return default + data = data.get(paths[-1]) + if data is None: + return default + return data + +# Directory where this script is. +DIRNAME = os.path.dirname(__file__) + +# Load configuration. +config = {} +with open(DIRNAME + "/kube-diagrams.yaml", encoding="utf-8") as f: + config = yaml.safe_load(f) # load YAML config file + +# Get edge config. +def get_edge_config(edge_kind): + """ + Get the configuration for a diagram edge kind. + """ + edge_config = config.get("edges", {}).get(edge_kind) + if edge_config is None: + print(f"Error: {edge_kind} edge configuration not found!") + return edge_config if edge_config else {} + +__REPORT_REENTRANCE__ = True + +REPORT_COLORS = { + "Info": "\33[0m", + "Warning": "\33[33m", + "Error": "\33[31m" +} + +def report(kind, resource, path, msg, end): + """ + Report. + """ + # pylint: disable=global-statement + global __REPORT_REENTRANCE__ + if not __REPORT_REENTRANCE__: + return + __REPORT_REENTRANCE__ = False + rm = f"{REPORT_COLORS[kind]}[{kind}] " \ + + query_path(resource, "kind", "NO-KIND") + ":" \ + + get_name(resource) + if path is not None: + rm += f":{path}" + rm += f" - {msg}{end}\33[0m" + # GOOD: it is fine to log data that is not sensitive + print(rm) + __REPORT_REENTRANCE__ = True + +def info(resource, path, msg): + """ + Report an information message. + """ + report("Info", resource, path, msg, '.') + +def warning(resource, path, msg): + """ + Report a warning message. + """ + report("Warning", resource, path, msg, '!') + +def error(resource, path, msg): + """ + Report an error message. + """ + report("Error", resource, path, msg, '!') + +def get_node_config_of_resource_type(resource_type): + """ + Get the configuration for a Kubernetes resource type. + """ + node_config = config.get("nodes", {}).get(resource_type) + while isinstance(node_config, str): + node_config = config.get("nodes", {}).get(node_config) + return node_config + +# Get config associated to a resource. +already_warned_node_configs = set() +def get_node_config(resource): + """ + Get the configuration for a Kubernetes resource. + """ + resource_type = get_type(resource) + node_config = config.get("nodes", {}).get(resource_type) + while isinstance(node_config, str): + if node_config not in already_warned_node_configs: + warning(resource, None, f"{resource_type} used instead of {node_config}") + already_warned_node_configs.add(node_config) + node_config = config.get("nodes", {}).get(node_config) + if node_config is None: + if resource_type not in already_warned_node_configs: + warning(resource, None, f"{resource_type} node configuration undefined") + already_warned_node_configs.add(resource_type) + return node_config if node_config else {} + +class Resource(dict): + """ + Kubernetes resource. + """ + def __init__(self, data, fn): + """ + Initialize a Kubernetes resource. + """ + dict.__init__(self, data) + self.filename = fn + + def get_metadata_annotation(self, key): + """ + Get a metadata annotation. + """ + return query_path(self, "metadata.annotations", {}).get(key) + +def get_type(resource): + """ + Get the type of a Kubernetes resource, i.e., "kind/apiVersion". + """ + return query_path(resource, "kind", "kind-NOT-SET") + "/" \ + + query_path(resource, "apiVersion", "apiVersion-NOT-SET") + +def get_name(resource): + """ + Get the name of a Kubernetes resource. + """ + name = query_path(resource, "metadata.name") \ + or query_path(resource, "metadata.generateName") + if name is None: + warning(resource, "metadata.name", "Not set or set to null") + name = "NO-NAME" + if "metadata" in resource: + resource["metadata"]["name"] = name + else: + resource["metadata"] = { "name": name } + return name + +def get_namespace(resource): + """ + Get the namespace of a Kubernetes resource. + If not set then return the default namespace. + """ + return query_path(resource, "metadata.namespace", config.get("default_namespace", "default")) + +# Separators in node labels +NODE_LABEL_SEPARATORS = [" ", ":", "-", "."] + +def split_node_label(node_label): + """ + Split node labels into multi-lines. + """ + result = "" + last_pos = 0 + max_pos = len(node_label) - MAX_NODE_LABEL_LENGTH + while last_pos < max_pos: + part = node_label[last_pos:last_pos+MAX_NODE_LABEL_LENGTH] + idx = MAX_NODE_LABEL_LENGTH - 1 + while idx > 0: + if part[idx] in NODE_LABEL_SEPARATORS: + part = part[:idx] + break + idx -= 1 + result += part + result += "\n" + last_pos += len(part) + result += node_label[last_pos:] + return result + +def create_diagram_node(resource): + """ + Create a diagram node from a Kubernetes resource. + """ + # Format node label + node_label = split_node_label(get_name(resource)) + # Format node tooltip + tooltip = f"kind: {resource.get('kind')}\n" \ + + f"apiVersion: {resource.get('apiVersion')}\n" \ + + "metadata:\n" \ + + f" name: {get_name(resource)}\n" \ + + "..." + # Search diagram node class + diagram_node_class = ManagedServices # default diagram node class + node_config = get_node_config(resource) + if node_config is not None: # node config found + custom_icon = node_config.get("custom_icon") + if custom_icon is not None: # custom_icon defined + return Custom( + node_label, + os.path.abspath(custom_icon.replace("$KD", DIRNAME)), + tooltip=tooltip + ) + # Get diagram node class name + diagram_node_classname = node_config.get("diagram_node_classname") + if diagram_node_classname is not None: # classname defined + # Import Diagrams node class module + idx = diagram_node_classname.rfind('.') + if idx != -1: + module = importlib.import_module(diagram_node_classname[:idx]) + # Get diagram node class + diagram_node_class = getattr(module, diagram_node_classname[idx+1:]) + # Create a diagram node + return diagram_node_class(node_label, tooltip=tooltip) + +class ResourceCluster: + """ + Hierarchical clustering of Kubernetes resources. + """ + def __init__(self, name): + """ + Initialize a ResourceCluster instance. + """ + self.name = name + self.resources = {} # dict + self.clusters = {} # dict + self.graph_attr = { "tooltip": name } + + def get_or_create_cluster(self, name): + """ + Get or create a sub cluster. + """ + cluster = self.clusters.get(name) + if cluster is None: + cluster = ResourceCluster(name) + self.clusters[name] = cluster + return cluster + + def display(self, ident=0): + """ + Display sub clusters and Kubernetes resources recursively. + """ + for rid, _ in self.resources.items(): + print(" " * ident, f"- Resource {rid}") + for cid, sub_cluster in self.clusters.items(): + print(" " * ident, f"- {cid}") + sub_cluster.display(ident + 1) + +class EdgesContext(list): + """ + Context provided to edges configuration scripts. + """ + def __init__(self, rid, resource): + """ + Initialize an edge context. + """ + list.__init__(self) + self.rid = rid + self.resource = resource + self.namespace = get_namespace(resource) + + def info(self, path, msg): + """ + Report an information message. + """ + info(self.resource, path, msg) + + def warning(self, path, msg): + """ + Report a warning message. + """ + warning(self.resource, path, msg) + + def error(self, path, msg): + """ + Report an error message. + """ + error(self.resource, path, msg) + + def add_edge(self, path, edge, data=None): + """ + Add an edge. + """ + edge_kind = edge[-1] + if isinstance(edge_kind, str): + edge_kind = dict(get_edge_config(edge_kind)) + if "tooltip" not in edge_kind: + if data is None and path is not None: + data = query_path(self.resource, path) + if data is None: + tooltip = path + else: + tooltip = yaml.dump({path: data}, default_flow_style=False)[:-1] + if len(tooltip) > 16384: # dot parsing limit! + tooltip = tooltip[:16380] + "\n..." + edge_kind["tooltip"] = tooltip + edge[-1] = edge_kind + self.append(edge) + + def add_edge_to_rid(self, path, rid, edge_kind, data=None): + """ + Add an edge to a resource identifier. + """ + if rid in resources: + self.add_edge(path, [rid, edge_kind], data) + else: + if rid in config["cluster-resources"]: + self.info(path, f"'{rid}' provided by Kubernetes cluster") + else: + self.warning(path, f"'{rid}' undefined") + + def add_edge_to(self, path, name, namespace, kind, api_version, edge_kind, data=None): + """ + Add an edge to a resource. + """ + if name == ".": + name = query_path(self.resource, path) + if name is None: + return + if name == "": + self.warning(path, "Set to \"\"") + return + if namespace is not None: + rid = f"{name}/{namespace}/{kind}/{api_version}" + else: + rid = f"{name}/{kind}/{api_version}" + if not (get_node_config_of_resource_type(f"{kind}/{api_version}") or {}) \ + .get("show", True): + self.info(path, f"{kind} '{name}' hidden") + else: + if rid in resources: + self.add_edge(path, [rid, edge_kind], data) + elif rid in config["cluster-resources"]: + self.info(path, f"{kind} '{name}' provided by Kubernetes cluster") + else: + self.warning(path, f"{kind} '{name}' undefined") + + def add_resource(self, path): + """ + Add a reference edge to a resource. + """ + target = query_path(self.resource, path) + self.add_edge_to( + path, + target['name'], + self.namespace, + target['kind'], + target['apiVersion'], + "REFERENCE-UP" + ) + + def add_resources(self, path, name_path, namespace_path, kind, api_version): + """ + Add reference edges to a set of resources. + """ + for ridx, resource in enumerate(query_path(self.resource, path, [])): + self.add_edge_to( + f"{path}[{ridx}]", + resource[name_path], + resource.get(namespace_path, self.namespace), + kind, + api_version, + "REFERENCE" + ) + + def add_all_resources_matching_labels( + self, + kind, + path, + match_labels=None, + data=None, + resource_labels_path="metadata.labels", + edge_kind="SELECTOR", + tooltip_data=None + ): + """ + Add selector egdes to all resources matching given labels. + """ + if data is None: + data = self.resource + if match_labels is None: + match_labels = query_path(data, path) + if match_labels is None: + return False + resource_found = False + for rid, resource in resources.items(): + if resource.get("kind") != kind: + continue + labels = query_path(resource, resource_labels_path, {}) + if not isinstance(labels, dict): + continue # skip these labels because this is not a dictionary. + found = True + for sk, sv in match_labels.items(): + if labels.get(sk) != sv: + found = False + break + if found: + self.add_edge_to_rid(path, rid, edge_kind, tooltip_data) + resource_found = True + return resource_found + + def add_all_volume_resources(self, path): + """ + All edges for resources referenced into volumes. + """ + def add_volumes(volumes, path): + for idx, volume in enumerate(volumes): + if "configMap" in volume: + config_map = volume["configMap"] + if config_map is None: + continue # skip this config map + config_name = config_map.get("name") + config_map_id = f"{config_name}/{self.namespace}/ConfigMap/v1" + if config_map.get("optional") is True: + if config_map_id not in resources: + self.info( + f"{path}[{idx}].configMap", + f"ConfigMap '{config_name}' undefined but optional" + ) + continue # skip it + self.add_edge_to( + f"{path}[{idx}].configMap", + config_name, + self.namespace, + "ConfigMap", + "v1", + "REFERENCE", + data=config_map + ) + elif "secret" in volume: + secret = volume["secret"] + if secret is None: + continue # skip this secret + secret_name = secret.get("secretName") + if secret_name is None: + # Warning: jenkins chart uses 'name' instead of 'secretName'! + secret_name = secret.get("name") + secret_id = f"{secret_name}/{self.namespace}/Secret/v1" + if secret.get("optional") is True: + if secret_id not in resources: + self.info( + f"{path}[{idx}].secret", + f"Secret '{secret_name}' undefined but optional" + ) + continue # skip it + self.add_edge_to( + f"{path}[{idx}].secret", + secret_name, + self.namespace, + "Secret", + "v1", + "REFERENCE", + data=secret + ) + elif "persistentVolumeClaim" in volume: + self.add_edge_to( + f"{path}[{idx}].persistentVolumeClaim", + query_path(volume, "persistentVolumeClaim.claimName"), + self.namespace, + "PersistentVolumeClaim", + "v1", + "REFERENCE", + data=volume["persistentVolumeClaim"] + ) + elif "projected" in volume: + volumes = query_path(volume, "projected.sources") + if volumes is not None: + add_volumes(volumes, path + "projected.sources") + + volumes = query_path(self.resource, path) + if volumes is not None: + add_volumes(volumes, path) + + def add_containers_env_value_from_and_env_from(self, path): + """ + Add edges to resources referenced from containers.env.valueFrom and envFrom. + """ + containers = query_path(self.resource, path) + if containers is None: + return + + target_resources = set() + def process_optional_resource( + path, + context, + kind, + name_path, + optional_path + ): + resource_name = query_path(context, name_path) + if resource_name is not None: + resource_id = f"{resource_name}/{self.namespace}/{kind}/v1" + if query_path(context, optional_path) is True: + if resource_id not in resources: + self.info( + f"{path}.{name_path}", + f"{kind} '{resource_name}' undefined but optional" + ) + return + target_resources.add( + resource_id + ) + + for cidx, container in enumerate(containers): + container_env = query_path(container, "env") + if isinstance(container_env, list): + for eidx, env in enumerate(container_env): + process_optional_resource( + f"{path}[{cidx}].env[{eidx}]", + env, + "ConfigMap", + "valueFrom.configMapKeyRef.name", + "valueFrom.configMapKeyRef.optional" + ) + process_optional_resource( + f"{path}[{cidx}].env[{eidx}]", + env, + "Secret", + "valueFrom.secretKeyRef.name", + "valueFrom.secretKeyRef.optional" + ) + container_env_from = query_path(container, "envFrom") + if isinstance(container_env_from, list): + for eidx, env_from in enumerate(container_env_from): + process_optional_resource( + f"{path}[{cidx}].envFrom[{eidx}]", + env_from, + "ConfigMap", + "configMapRef.name", + "configMapRef.optional" + ) + process_optional_resource( + f"{path}[{cidx}].envFrom[{eidx}]", + env_from, + "Secret", + "secretRef.name", + "secretRef.optional" + ) + + for target_resource in target_resources: + self.add_edge_to_rid(path, target_resource, "REFERENCE") + + def add_volume_claim_templates(self, path): + """ + Add reference edges to volume claim templates. + """ + for idx, vct in enumerate(query_path(self.resource, path, [])): + self.add_edge_to( + f"{path}[{idx}]", + vct['metadata']['name'], + self.namespace, + "PersistentVolumeClaim", + "v1", + "REFERENCE" + ) + + def add_wait_for_services(self, path): + """ + Add dependence edges between worload resources. + """ + for _, ic in enumerate(query_path(self.resource, path, [])): + if ic.get("name") == "wait-for-services": + for arg in query_path(ic, "args", []): + if arg.startswith("-service="): + sn = arg[len("-service="):] + self.add_edge_to( + path, + sn, + self.namespace, + "Service", + "v1", + "DEPENDENCE" + ) + + def add_service_account(self, path): + """ + Add a reference edge to a service account resource. + """ + pod_spec = query_path(self.resource, path) + if pod_spec is None: + return + service_account_name = pod_spec.get("serviceAccountName") + if service_account_name is None: + service_account_name = pod_spec.get("serviceAccount") + if service_account_name is not None: + self.warning(f"{path}.serviceAccount", "Deprecated") + # If << serviceAccountName: "" >> then no service account defined + if service_account_name == "": + return + # else add an edge to the ServiceAccount resource + self.add_edge_to( + f"{path}.serviceAccountName", + service_account_name, + self.namespace, + "ServiceAccount", + "v1", + "REFERENCE" + ) + + def add_role(self, path): + """ + Add a reference edge to a role resource. + """ + role_ref = self.resource.get(path) + if role_ref is None: + return + api_group = query_path(role_ref, "apiGroup", "rbac.authorization.k8s.io") + if api_group == "": + self.warning(path, "Set to \"\"") + api_group = "rbac.authorization.k8s.io" + if role_ref.get("kind") == "Role": + namespace = self.namespace + else: + namespace = None + self.add_edge_to( + path, + role_ref["name"], + namespace, + role_ref["kind"], + f"{api_group}/v1", + "REFERENCE", + data=role_ref + ) + + def add_subjects(self): + """ + Add reference edges for subject resources. + """ + for idx, subject in enumerate(query_path(self.resource, "subjects", [])): + namespace = subject.get("namespace") + if namespace is None and subject.get("kind") == "ServiceAccount": + namespace = get_namespace(self.resource) + if namespace is not None: + api_version = query_path(subject, "apiGroup", "v1") + if api_version == "": + api_version = "v1" + else: + api_group = query_path(subject, "apiGroup", "rbac.authorization.k8s.io") + if api_group == "": + self.warning(f"subjects[{idx}].apiGroup", "Set to \"\"") + api_group = "rbac.authorization.k8s.io" + api_version = f"{api_group}/v1" + self.add_edge_to( + f"subjects[{idx}]", + subject['name'], + namespace, + subject['kind'], + api_version, + "REFERENCE-UP", + data=subject + ) + + def add_service(self, path, data=None, name=None): + """ + Add a reference edge to a service resource. + """ + if data is None: + data = self.resource + if name is None: + name = query_path(data, path) + self.add_edge_to( + path, + name, + self.namespace, + "Service", + "v1", + "REFERENCE", + data=name + ) + + def get_owned_resources(self, owner_resource): + """ + Get owned resources. + """ + result = [] + uid = query_path(owner_resource, "metadata.uid") + for _, resource in resources.items(): + for owner_reference in query_path(resource, "metadata.ownerReferences", []): + if owner_reference.get("uid") == uid: + result.append(resource) + return result + + def add_owned_resources(self): + """ + Add owner edges to owned resources. + """ + for resource in self.get_owned_resources(self.resource): + if get_node_config(resource).get("scope") == "Namespaced": + namespace = get_namespace(resource) + else: + namespace = None + self.add_edge_to( + "owns", + resource['metadata']['name'], + namespace, + resource['kind'], + resource['apiVersion'], + "OWNER" + ) + + def add_ingress_and_egress_rules(self): + """ + Add selector edges for ingress and egress rules. + """ + selected_nodes = [e[0] for e in self] + for ridx, ingress_rule in enumerate(query_path(self.resource, "spec.ingress", [])): + for fidx, ingress_from in enumerate(query_path(ingress_rule, "from", [])): + if "podSelector" not in ingress_from: + continue # skip this ingress_from item + current_index = len(self) + self.add_all_workload_resources( + f"spec.ingress[{ridx}].from[{fidx}]", + query_path(ingress_from, "podSelector.matchLabels", {}), + edge_kind="INVISIBLE" + ) + ports = [ + f"{str(port.get('port'))}/{str(port.get('protocol'))}" \ + for port in query_path(ingress_rule, "ports", []) + ] + edge = { + **get_edge_config("COMMUNICATION"), + "headlabel": f"ingress\n{', '.join(ports)}", + "fontsize": "10" + } + for rid_from in [e[0] for e in self[current_index:]]: + for rid_to in selected_nodes: + self.add_edge("spec.ingress", [rid_from, rid_to, edge]) + + for egress_rule in query_path(self.resource, "spec.egress", []): + for egress_to in query_path(egress_rule, "to", []): + if "podSelector" not in egress_to: + continue # skip this egress_to item + current_index = len(self) + self.add_all_workload_resources( + "spec.egress", + query_path(egress_to, "podSelector.matchLabels", {}), + edge_kind="INVISIBLE" + ) + ports = [ + f"{str(port.get('port'))}/{str(port.get('protocol'))}" \ + for port in query_path(egress_rule, "ports", []) + ] + edge = { + **get_edge_config("COMMUNICATION"), + "taillabel": f"egress\n{', '.join(ports)}", + "fontsize": "10" + } + for rid_to in [e[0] for e in self[current_index:]]: + for rid_from in selected_nodes: + self.add_edge("spec.egress", [rid_from, rid_to, edge]) + + def add_webhooks(self): + """ + Add reference edges for webhooks. + """ + for idx, webhook in enumerate(query_path(self.resource, "webhooks", [])): + service = query_path(webhook, "clientConfig.service") + if service is not None: + self.add_edge_to( + f"webhooks[{idx}].clientConfig.service", + service['name'], + service['namespace'], + "Service", + "v1", + "REFERENCE", + data=service + ) + + def add_all_workload_resources( + self, + path, + selector=None, + default_selector=None, + edge_kind="SELECTOR" + ): + """ + Add selector edges to all workload resources. + """ + if selector is None: + selector = query_path(self.resource, path, default_selector) + if selector is None: + return + if query_path(self.resource, "metadata.uid") is not None: + # If the resource is deployed then binds it to pods only. + build_in_workload_resources = { + "Pod": "metadata.labels", + } + else: + # If the resource is not deployed then binds it to workloads or pods. + build_in_workload_resources = { + "Deployment": "spec.template.metadata.labels", + "ReplicaSet": "spec.template.metadata.labels", + "ReplicationController": "spec.template.metadata.labels", + "StatefulSet": "spec.template.metadata.labels", + "DaemonSet": "spec.template.metadata.labels", + "Job": "spec.template.metadata.labels", + "PodTemplate": "template.metadata.labels", + "Pod": "metadata.labels", + } + resource_not_found = True + for kind, label in build_in_workload_resources.items(): + if self.add_all_resources_matching_labels( + kind, + path, + selector, + resource_labels_path=label, + edge_kind=edge_kind): + resource_not_found = False + if default_selector is None: + break + if resource_not_found: + self.warning(path, f"No workload resource matches metadata labels {selector}") + + def add_edges_for_service(self): + """ + Add edges from a service resource to workload resources, + endpoint slices and endpoints. + """ + labels = [] + for service_port in query_path(self.resource, "spec.ports", []): + port = service_port.get('port') + target_port = service_port.get('targetPort') + protocol = service_port.get('protocol', 'TCP') + if target_port is None or target_port == port: + label = f"{port}/{protocol}" + else: + label = f"{port}->{target_port}/{protocol}" + labels.append(label) + edge = { + **get_edge_config("SELECTOR"), + "xlabel": '\n'.join(labels), + "fontsize": "10" + } + self.add_all_workload_resources("spec.selector", edge_kind=edge) + self.add_all_resources_matching_labels( + "EndpointSlice", + "endpoint_slice", + { + "kubernetes.io/service-name": + query_path(self.resource, "metadata.name") + } + ) + rid = f"{query_path(self.resource, 'metadata.name')}" \ + f"/{get_namespace(self.resource)}/Endpoints/v1" + if rid in resources: + self.add_edge("owns", [rid, "OWNER"]) + + def add_networks(self, path): + """ + Add edges from a workload resource to its referenced + NetworkAttachmentDefinition resources. + """ + annotations = query_path(self.resource, path) + if annotations is not None: + networks = annotations.get("k8s.v1.cni.cncf.io/networks") + if networks is not None: + try: + networks = json.loads(networks) + except json.decoder.JSONDecodeError: + self.warning(f"{path}:k8s.v1.cni.cncf.io/networks", + f"Invalid JSON '{networks}'") + return + for nidx, network in enumerate(networks): + self.add_edge_to( + f"{path}:k8s.v1.cni.cncf.io/networks[{nidx}]", + network['name'], + get_namespace(self.resource), + "NetworkAttachmentDefinition", + "k8s.cni.cncf.io/v1", + "REFERENCE", + data=network + ) + + def add_priority_class(self, path): + """ + Add a reference edge to a priority class resource. + """ + # If << priorityClassName: "" >> then no priority class defined + if query_path(self.resource, path) == "": + return + # else add an edge to the PriorityClass resource + self.add_edge_to( + path, + ".", + None, + "PriorityClass", + "scheduling.k8s.io/v1", + "REFERENCE" + ) + + def add_rules_resource_names(self): + """ + Add edges to rules resourceNames. + """ + for ridx, rule in enumerate(query_path(self.resource, "rules", [])): + if not isinstance(rule, dict): + continue # skip this rule as it is not a dict as expected + resource_names = query_path(rule, "resourceNames") + if resource_names is None: + continue # skip this rule as no resourceNames + api_groups = query_path(rule, "apiGroups") + if len(api_groups) != 1: + self.error( + f"rules[{ridx}]", + f"Field apiGroups ({api_groups}) should contain only one value" + ) + continue # skip it + api_group = api_groups[0] + if api_group == "": + api_version = "v1" + else: + api_version = f"{api_group}/v1" + for resource in query_path(rule, "resources", []): + if '/' in resource: + continue # skip subresources + kind = PLURAL2KINDS.get(resource, resource) + for rnidx, resource_name in enumerate(resource_names): + if not isinstance(resource_name, str): + continue # skip it as a string is expected + if '*' in resource_name: + continue # skip it + scope = config["nodes"].get(f"{kind}/{api_version}", {}).get("scope") + namespace = self.namespace if scope == "Namespaced" else None + self.add_edge_to( + f"rules[{ridx}].resourceNames[{rnidx}]", + resource_name, + namespace, + kind, + api_version, + "REFERENCE-UP" + ) + +PLURAL2KINDS = {} +for k, v in config["nodes"].items(): + if not isinstance(v, dict): + continue # skip this aliased node + node_kind = k.split("/")[0] + plural = v.get("plural") + if plural is None: + plural = node_kind.lower() + "s" + PLURAL2KINDS[plural] = node_kind + +def create_node_for_role_rules_resource_names(role, nodes): + """ + Create nodes for rules resourceNames. + """ + for ridx, rule in enumerate(query_path(role, "rules", [])): + if not isinstance(rule, dict): + continue # skip this rule as it is not a dict as expected + resource_names = query_path(rule, "resourceNames") + if resource_names is None: + continue # skip this rule as no resourceNames + api_groups = query_path(rule, "apiGroups") + if len(api_groups) != 1: + error( + role, + f"rules[{ridx}]", + f"Field apiGroups ({api_groups}) should contain only one value" + ) + continue # skip it + api_group = api_groups[0] + if api_group == "": + api_version = "v1" + else: + api_version = f"{api_group}/v1" + for resource in query_path(rule, "resources", []): + if '/' in resource: + continue # skip subresources + resource_kind = PLURAL2KINDS.get(resource) + if resource_kind is None: + continue # skip it + for rnidx, resource_name in enumerate(resource_names): + if not isinstance(resource_name, str): + continue # skip it as a string is expected + if '*' in resource_name: + continue # skip it + resource_id = f"{resource_name}/{get_namespace(role)}/{resource_kind}/{api_version}" + if resource_id in resources: + continue # skip it + for rule1 in query_path(role, "rules", []): + if api_group in rule1.get("apiGroups", []) \ + and resource in rule1.get("resources", []) \ + and "create" in rule1.get("verbs", []): + new_node = { + "kind": resource_kind, + "apiVersion": api_version, + "metadata": { + "name": resource_name, + "namespace": get_namespace(role), + "labels": query_path(role, "metadata.labels") + } + } + warning(role, f"rules[{ridx}].resourceNames[{rnidx}]", f"Create {new_node}") + nodes.append(new_node) + break + +# Parse arguments +parser = argparse.ArgumentParser( + prog="kube-diagrams", + description="Generate Kubernetes architecture diagrams from Kubernetes manifest files") +parser.add_argument("filename", nargs='+', + help="the Kubernetes manifest filename to process") +parser.add_argument("-o", "--output", type=str, + help="output diagram filename") +parser.add_argument("-f", "--format", type=str, + help="output format, allowed formats are " \ + + ", ".join(SUPPORTED_OUTPUT_FORMATS) \ + + ", set to png by default", + default="png") +parser.add_argument("--embed-all-icons", + help="embed all icons into svg or dot_json output diagrams", + action="store_true", default=False) +parser.add_argument("-c", "--config", type=str, action="append", + help="custom kube-diagrams configuration file") +parser.add_argument("-n", "--namespace", type=str, + help="visualize only the resources inside a given namespace") +parser.add_argument("-v", "--verbose", + help="verbosity, set to false by default", + action="store_true", default=False) +parser.add_argument("--without-namespace", + help="disable namespace cluster generation", + action="store_true", default=False) +args = parser.parse_args() + +# Process arguments. +if args.output is None: + args.output = args.filename[0][:args.filename[0].rfind('.')] +else: + dot_idx = args.output.rfind('.') + if dot_idx != -1: + args.format = args.output[dot_idx+1:] + args.output = args.output[:dot_idx] + +if args.format not in SUPPORTED_OUTPUT_FORMATS: + SOF = "' or '".join(SUPPORTED_OUTPUT_FORMATS) + print(f"Error: '{args.format}' output format unsupported," + f" use '{SOF}' instead!", file=sys.stderr) + sys.exit(1) + +if args.embed_all_icons and args.format not in ('svg', 'dot_json'): + print("Warning: --embed-all-icons only works with svg or dot_json output format!") + +if args.config is not None: + for config_file in args.config: + with open(config_file, encoding="utf-8") as f: + custom_config = yaml.safe_load(f) # load custom config file + if custom_config: # not empty file + if "default_namespace" in custom_config: + config["default_namespace"] = custom_config["default_namespace"] + if custom_config.get("edges"): + config["edges"].update(custom_config["edges"]) + if custom_config.get("clusters"): + for cluster_custom_config in custom_config["clusters"]: + if "label" in cluster_custom_config: + cluster_label = cluster_custom_config["label"] + for config_cluster in config["clusters"]: + if config_cluster.get("label") == cluster_label: + config_cluster.update(cluster_custom_config) + cluster_label = None + break + if cluster_label is not None: + config["clusters"].append(cluster_custom_config) + elif "annotation" in cluster_custom_config: + cluster_annotation = cluster_custom_config["annotation"] + for config_cluster in config["clusters"]: + if config_cluster.get("annotation") == cluster_annotation: + config_cluster.update(cluster_custom_config) + cluster_annotation = None + break + if cluster_annotation is not None: + config["clusters"].append(cluster_custom_config) + else: + print("ISSUE on", cluster_custom_config) + if custom_config.get("nodes"): + for k, v in custom_config["nodes"].items(): + previous = config["nodes"].get(k) + if previous is None: + config["nodes"][k] = v + else: + previous.update(v) + if "diagram" in custom_config: + config["diagram"] = custom_config["diagram"] + +# Load the Kubernetes manifest file. +resources = {} # a map of all resources +resource_cluster = ResourceCluster("ROOT") # Clustering resources + +# pylint: disable-next=too-many-statements +def process_resource(resource): + """ + Process a resource. + """ + cluster = resource_cluster + name = get_name(resource) + resource_scope = get_node_config(resource).get("scope") + # Namespace filter + if args.namespace is not None and \ + ( resource_scope != "Namespaced" \ + or get_namespace(resource) != args.namespace): + return # skip this resource + + if resource_scope == "Outside": + rid = name + "/" + get_type(resource) + elif resource_scope == "Cluster": + rid = name + "/" + get_type(resource) +#TBR: commented to avoid to create a cluster +# if not(args.without_namespace) and resource.get("kind") == "Namespace": +# cluster = resource_cluster.get_or_create_cluster(f"Namespace: {name}") + else: # scope = Namespaced + rid = name + "/" + get_namespace(resource) + "/" + get_type(resource) + if not args.without_namespace: + cluster = resource_cluster.get_or_create_cluster( + f"Namespace: {get_namespace(resource)}" + ) + cluster.graph_attr.update({ + "style": "rounded,dashed", + "bgcolor": "white", + "pencolor": "black", + "label": icon(Namespace, get_namespace(resource)) + }) + + if rid not in resources: + resources[rid] = resource + else: + error(resource, None, f"Already declared in {resources[rid].filename}") + + def process_clusters(cluster, resource, cluster_configs): + """ + Process a resource. + """ + for cluster_config in cluster_configs: + if cluster_config.get("show", True) is False: + continue # skip this cluster + if "annotation" in cluster_config: + annotation = cluster_config["annotation"] + annotations = query_path(resource, "metadata.annotations") + if isinstance(annotations, dict) and annotation in annotations: + cluster_config_title = cluster_config['title'] + cluster_name = cluster_config_title.format(annotations[annotation]) + cluster = cluster.get_or_create_cluster(cluster_name) + cluster.graph_attr.update(cluster_config.get("graph_attr", {})) + cluster_configs.remove(cluster_config) + return process_clusters(cluster, resource, cluster_configs) + elif "label" in cluster_config: + label = cluster_config["label"] + labels = query_path(resource, "metadata.labels") + if isinstance(labels, dict) and label in labels: + cluster_config_title = cluster_config['title'] + cluster_name = cluster_config_title.format(labels[label]) + cluster = cluster.get_or_create_cluster(cluster_name) + cluster.graph_attr.update(cluster_config.get("graph_attr", {})) + cluster_configs.remove(cluster_config) + return process_clusters(cluster, resource, cluster_configs) + return cluster + + cluster = process_clusters( + cluster, + resource, + copy.deepcopy(config.get("clusters", [])) + ) + cluster.resources[rid] = resource + +for filename in args.filename: + try: + with open(0 if filename == '-' else filename, encoding="utf-8") as f: + print(f"Load {'from stdin' if filename == '-' else filename}...") + for yaml_data in yaml.safe_load_all(f): # load YAML file + if yaml_data is None: + continue # Skip empty YAML content + if yaml_data.get("kind", "NO-KIND").endswith("List") and "items" in yaml_data: + for r in yaml_data["items"]: + process_resource(Resource(r, filename)) + else: + process_resource(Resource(yaml_data, filename)) + except FileNotFoundError: + print(f"Error: file '{filename}' not found!") + if len(args.filename) == 1: + sys.exit(1) + except yaml.scanner.ScannerError as error: + print("Error: " + str(error).replace('\n', ' ') + "!") + except yaml.constructor.ConstructorError as error: + print("Error: " + str(error).replace('\n', ' ') + "!") + +# Print loaded Kubernetes resources +if args.verbose: + print("Loaded Kubernetes resources:") + resource_cluster.display() + +# Create new nodes +def create_new_nodes(): + """ + Create new nodes + """ + + def internal_create_new_nodes(resource): + nodes_script = get_node_config(resource).get("nodes") + if nodes_script is None: + return # skip it + nodes = [] + try: + # pylint: disable-next=exec-used + exec(nodes_script) + except Exception as exc: + print("Error:", type(exc), ":", exc.args) + traceback.print_exc() + print("Nodes script:\n", nodes_script) + print("Resource:") + pprint(resource) + raise + for node in nodes: + resource = Resource(node, resource.filename) + process_resource(resource) + internal_create_new_nodes(resource) + + for _, resource in dict(resources).items(): + internal_create_new_nodes(resource) + +create_new_nodes() + +def process_edges(): + """ + Process diagram edges. + """ + for resource_id, resource in resources.items(): + if not get_node_config(resource).get("show", True): + continue # Skip this hidden resource. + edges = EdgesContext(resource_id, resource) + # execute config code to generate edges + code_to_exec = get_node_config(resource).get("edges") + if code_to_exec: + try: + # pylint: disable-next=exec-used + exec(code_to_exec) + except Exception as exc: + print("Error:", type(exc), ":", exc.args) + traceback.print_exc() + print("Edges script:\n", code_to_exec) + print("Resource:") + pprint(resource) + raise + # generate diagram edges + for eidx, edge in enumerate(edges): + if len(edge) == 2: + edges[eidx] = [resource_id, edge[0], edge[1]] + for edge in edges: + edge_from = edge[0] + edge_to = edge[1] + edge_name = edge[2] + try: + if isinstance(edge_name, dict): + edge_configuration = edge_name + else: + edge_configuration = get_edge_config(edge_name) + if edge_configuration.get("direction") == "up": + _ = diagram_nodes[edge_to] \ + << Edge(**edge_configuration) \ + << diagram_nodes[edge_from] + else: + _ = diagram_nodes[edge_from] \ + >> Edge(**edge_configuration) \ + >> diagram_nodes[edge_to] + except KeyError as ke: + if edge_to in config["cluster-resources"]: + info(resource, None, f"Referenced {edge_to} resource is provided" + " by K8s clusters.") + continue # skip this edge as the resource is provided by K8s clusters. + error(resource, None, f"{ke} resource not found") + raise + +def create_nodes(cluster): + """ + Create diagram nodes and clusters recursively. + """ + for rid, resource in cluster.resources.items(): + if get_node_config(resource).get("show", True): + diagram_nodes[rid] = create_diagram_node(resource) + for cid, sub_cluster in cluster.clusters.items(): + with Cluster(cid, graph_attr=sub_cluster.graph_attr): + create_nodes(sub_cluster) + +def create_custom_node(node_id, node_def): + """ + Create a custom node. + """ + diagram_node_class = ManagedServices # default diagram node class + # Get diagram node class name + diagram_node_classname = node_def.get("type") + if diagram_node_classname is not None: # classname defined + # Import Diagrams node class module + idx = diagram_node_classname.rfind('.') + if idx != -1: + module = importlib.import_module(diagram_node_classname[:idx]) + # Get diagram node class + diagram_node_class = getattr(module, diagram_node_classname[idx+1:]) + node_label = split_node_label(node_def.get("name", "")) + diagram_nodes[node_id] = diagram_node_class(node_label, tooltip=node_label) + +def create_custom_cluster(cluster_id, cluster_def): + """ + Create a custom cluster. + """ + cluster_name = query_path(cluster_def, "name") + with Cluster(cluster_name, graph_attr={ + "tooltip": cluster_name, + **cluster_def.get("graph_attr", {}) + }): + create_custom_clusters_nodes(cluster_id, cluster_def) + +def create_custom_clusters_nodes(container_id, container_def): + """ + Create custom clusters and nodes. + """ + prefix_id = container_id + "." if container_id else "" + for cluster_id, cluster_def in query_path(container_def, "clusters", {}).items(): + create_custom_cluster(prefix_id + cluster_id, cluster_def) + for node_id, node_def in query_path(container_def, "nodes", {}).items(): + create_custom_node(prefix_id + node_id, node_def) + if container_id == generate_diagram_in_cluster: + create_nodes(resource_cluster) + +# Generate diagram +generate_diagram_in_cluster = query_path(config, "diagram.generate_diagram_in_cluster") +with Diagram("", filename=args.output, show=False, direction="TB", outformat=args.format): + # Generate diagram nodes + diagram_nodes = {} + create_custom_clusters_nodes(None, query_path(config, "diagram", {})) + + # Generate diagram edges + process_edges() + + # Create custom edges + for edge_idx, custom_edge in enumerate(query_path(config, "diagram.edges", [])): + custom_edge_from = custom_edge.get("from") + from_node = diagram_nodes.get(custom_edge_from) + if from_node is None: + print(f"Warning: diagram.edges[{edge_idx}].from:" + f" Node '{custom_edge_from}' undefined!") + custom_edge_to = custom_edge.get("to") + to_node = diagram_nodes.get(custom_edge_to) + if to_node is None: + print(f"Warning: diagram.edges[{edge_idx}].to:" + f" Node '{custom_edge_to}' undefined!") + if from_node is not None and to_node is not None: + edge_tooltip = f"from: {custom_edge_from}\nto: {custom_edge_to}" + _ = from_node >> Edge(**custom_edge, tooltip=edge_tooltip) >> to_node + +print(f"{args.output}.{args.format} generated.") + +if args.format in ("svg", "dot_json"): + filename = f"{args.output}.{args.format}" + print("Post-process paths of icons...") + # read all the lines of the generated file + with open(filename, "rt", encoding="utf-8") as fs: + lines = fs.readlines() + # compute absolute paths to be replaced by urls + from pathlib import Path + DIAGRAMS_PATH = str(Path(os.path.abspath(os.path.dirname(diagrams.__file__))).parent) + DIAGRAMS_URL = \ + "https://raw.githubusercontent.com/mingrammer/diagrams/refs/heads/master" + KUBEDIAGRAMS_PATH = str(Path(os.path.abspath(os.path.dirname(__file__))).parent) + KUBEDIAGRAMS_URL = \ + "https://raw.githubusercontent.com/philippemerle/KubeDiagrams/refs/heads/main" + if args.format == "svg": + what_to_search = [ + r'image xlink:href="([^"]+)"', + ] + elif args.format == "dot_json": + DIAGRAMS_PATH = DIAGRAMS_PATH.replace("/", "\\/") + KUBEDIAGRAMS_PATH = KUBEDIAGRAMS_PATH.replace("/", "\\/") + what_to_search = [ + r'"image": "([^"]+)"', + r'img src=\\"([^"]+)\\"', + ] + else: + what_to_search = [] + # rewrite all the lines of the generated file + with open(filename, "wt", encoding="utf-8") as fs: + for line in lines: + for wts in what_to_search: + import re + img_paths = re.findall(wts, line) + for img_path in img_paths: + if not args.embed_all_icons: + # replace absolute paths by urls + if DIAGRAMS_PATH in line: + line = line.replace(DIAGRAMS_PATH, DIAGRAMS_URL) + continue + if KUBEDIAGRAMS_PATH in line: + line = line.replace(KUBEDIAGRAMS_PATH, KUBEDIAGRAMS_URL) + continue + full_img_path = Path(img_path.replace("\\/", "/")) + if full_img_path.exists(): + # read the image + with open(full_img_path, 'rb') as img_file: + img_data = img_file.read() + # encode the image in base64 + import base64 + mime_type = 'image/png' + b64_data = base64.b64encode(img_data).decode('ascii') + data_uri = f"data:{mime_type};base64,{b64_data}" + # replace absolute path by image encoded in base64 + line = line.replace(img_path, data_uri) + else: + print(f"Warning: Image not found: {full_img_path}") + # write the line + fs.write(line) + print(f"{filename} saved.") diff --git a/webapp/backend/venv/bin/kube-diagrams.yaml b/webapp/backend/venv/bin/kube-diagrams.yaml new file mode 100644 index 0000000..f1f09fb --- /dev/null +++ b/webapp/backend/venv/bin/kube-diagrams.yaml @@ -0,0 +1,689 @@ +# Configuration of kube-diagrams +default_namespace: default +edges: + REFERENCE: + color: black + REFERENCE-UP: + color: black + direction: up + SELECTOR: + color: black + style: dashed + SELECTOR-UP: + color: black + style: dashed + direction: up + CONTROLLED_BY: + xlabel: controller + color: black + style: dotted + OWNER: + color: black + style: dotted + COMMUNICATION: + color: brown + DEPENDENCE: + color: darkgrey + INVISIBLE: + color: transparent + style: invisible + direction: up +clusters: + - label: app.kubernetes.io/instance + title: "K8s Instance: {}" + recommended: true + graph_attr: + bgcolor: "#E5F5FD" + - label: release + title: "Release: {}" + recommended: false + graph_attr: + bgcolor: "#E5F5FD" + - label: helm.sh/chart + title: "Helm Chart: {}" + recommended: true + graph_attr: + bgcolor: "#EBF3E7" + - label: chart + title: "Chart: {}" + recommended: false + graph_attr: + bgcolor: "#EBF3E7" + - label: app.kubernetes.io/name + title: "K8s Application: {}" + recommended: true + graph_attr: + bgcolor: "#ECE8F6" + - label: app + title: "Application: {}" + recommended: false + graph_attr: + bgcolor: "#ECE8F6" + - label: tier + title: "Tier: {}" + recommended: false + graph_attr: + bgcolor: "#ECE8F6" + - label: app.kubernetes.io/component + title: "K8s Component: {}" + recommended: true + graph_attr: + bgcolor: "#FDF7E3" + - label: component + title: "Component: {}" + recommended: false + graph_attr: + bgcolor: "#FDF7E3" + - label: service + title: "Microservice: {}" + recommended: false + graph_attr: + bgcolor: "#FDF7E3" + - label: rbac.authorization.k8s.io/aggregate-to-admin + title: "Admin ClusterRole Aggregation" + recommended: true + graph_attr: + bgcolor: transparent + - label: rbac.authorization.k8s.io/aggregate-to-edit + title: "Edit ClusterRole Aggregation" + recommended: true + graph_attr: + bgcolor: transparent + - label: rbac.authorization.k8s.io/aggregate-to-view + title: "View ClusterRole Aggregation" + recommended: true + graph_attr: + bgcolor: transparent + - annotation: helm.sh/hook + title: "{}" + recommended: true + graph_attr: + bgcolor: "#EBF3E7" + style: dotted,rounded +nodes: + APIService/apiregistration.k8s.io/v1: + scope: Cluster + custom_icon: $KD/icons/apiservice.png + edges: | + service = query_path(resource, "spec.service") + if service is not None: + edges.add_edge_to( + "spec.service", + service['name'], + service['namespace'], + "Service", + "v1", + "REFERENCE" + ) + APIService/apiregistration.k8s.io/v1beta1: APIService/apiregistration.k8s.io/v1 + ClusterRole/rbac.authorization.k8s.io/v1: + scope: Cluster + diagram_node_classname: diagrams.k8s.rbac.CRole + edges: | + for idx, clusterRoleSelector in enumerate(query_path(resource, "aggregationRule.clusterRoleSelectors", [])): + edges.add_all_resources_matching_labels( + "ClusterRole", + f"aggregationRule.clusterRoleSelectors[{idx}]", + match_labels=clusterRoleSelector.get("matchLabels"), + tooltip_data=clusterRoleSelector + ) + edges.add_rules_resource_names() + ClusterRole/rbac.authorization.k8s.io/v1beta1: ClusterRole/rbac.authorization.k8s.io/v1 + ClusterRoleBinding/rbac.authorization.k8s.io/v1: + scope: Cluster + diagram_node_classname: diagrams.k8s.rbac.CRB + edges: | + edges.add_role("roleRef") + edges.add_subjects() + ClusterRoleBinding/rbac.authorization.k8s.io/v1beta1: ClusterRoleBinding/rbac.authorization.k8s.io/v1 + ConfigMap/v1: + scope: Namespaced + diagram_node_classname: diagrams.k8s.podconfig.CM + # no edges + CronJob/batch/v1: + scope: Namespaced + diagram_node_classname: diagrams.k8s.compute.Cronjob + edges: | + edges.add_owned_resources() + edges.add_service_account("spec.jobTemplate.spec.template.spec") + edges.add_all_volume_resources("spec.jobTemplate.spec.template.spec.volumes") + edges.add_containers_env_value_from_and_env_from("spec.jobTemplate.spec.template.spec.containers") + edges.add_containers_env_value_from_and_env_from("spec.jobTemplate.spec.template.spec.initContainers") + edges.add_containers_env_value_from_and_env_from("spec.jobTemplate.spec.template.spec.ephemeralContainers") + edges.add_wait_for_services("spec.jobTemplate.spec.template.spec.initContainers") + edges.add_networks("spec.jobTemplate.spec.template.metadata.annotations") + edges.add_priority_class("spec.jobTemplate.spec.template.spec.priorityClassName") + edges.add_edge_to( + "spec.jobTemplate.spec.template.spec.runtimeClassName", + ".", + None, + "RuntimeClass", + "node.k8s.io/v1", + "REFERENCE" + ) + CronJob/batch/v1beta1: CronJob/batch/v1 + CustomResourceDefinition/apiextensions.k8s.io/v1: + scope: Cluster + diagram_node_classname: diagrams.k8s.others.CRD + # no edges + CustomResourceDefinition/apiextensions.k8s.io/v1beta1: CustomResourceDefinition/apiextensions.k8s.io/v1 + CSIDriver/storage.k8s.io/v1: + scope: Cluster + custom_icon: $KD/icons/csidriver.png + # no edges + CSIDriver/storage.k8s.io/v1beta1: CSIDriver/storage.k8s.io/v1 + CSINode/storage.k8s.io/v1: + scope: Cluster + custom_icon: $KD/icons/csinode.png + edges: | + for idx, driver in enumerate(query_path(resource, "spec.drivers", [])): + edges.add_edge_to( + f"spec.drivers[{idx}]", + driver['name'], + None, + "CSIDriver", + "storage.k8s.io/v1", + "REFERENCE", + data=driver + ) + CSIStorageCapacity/storage.k8s.io/v1: + scope: Namescaped + custom_icon: $KD/icons/csisc.png + edges: | + edges.add_edge_to( + "storageClassName", + ".", + None, + "StorageClass", + "storage.k8s.io/v1", + "REFERENCE" + ) + DaemonSet/apps/v1: + scope: Namespaced + diagram_node_classname: diagrams.k8s.compute.DS + edges: | + edges.add_owned_resources() + edges.add_service_account("spec.template.spec") + edges.add_all_volume_resources("spec.template.spec.volumes") + edges.add_containers_env_value_from_and_env_from("spec.template.spec.containers") + edges.add_containers_env_value_from_and_env_from("spec.template.spec.initContainers") + edges.add_containers_env_value_from_and_env_from("spec.template.spec.ephemeralContainers") + edges.add_wait_for_services("spec.template.spec.initContainers") + edges.add_networks("spec.template.metadata.annotations") + edges.add_priority_class("spec.template.spec.priorityClassName") + edges.add_edge_to( + "spec.template.spec.runtimeClassName", + ".", + None, + "RuntimeClass", + "node.k8s.io/v1", + "REFERENCE" + ) + DaemonSet/apps/v1beta2: DaemonSet/apps/v1 + DaemonSet/extensions/v1beta1: DaemonSet/apps/v1 + Deployment/apps/v1: + scope: Namespaced + diagram_node_classname: diagrams.k8s.compute.Deploy + edges: | + edges.add_owned_resources() + edges.add_service_account("spec.template.spec") + edges.add_all_volume_resources("spec.template.spec.volumes") + edges.add_containers_env_value_from_and_env_from("spec.template.spec.containers") + edges.add_containers_env_value_from_and_env_from("spec.template.spec.initContainers") + edges.add_containers_env_value_from_and_env_from("spec.template.spec.ephemeralContainers") + edges.add_wait_for_services("spec.template.spec.initContainers") + edges.add_networks("spec.template.metadata.annotations") + edges.add_priority_class("spec.template.spec.priorityClassName") + edges.add_edge_to( + "spec.template.spec.runtimeClassName", + ".", + None, + "RuntimeClass", + "node.k8s.io/v1", + "REFERENCE" + ) + Deployment/extensions/v1beta1: Deployment/apps/v1 + Deployment/apps/v1beta1: Deployment/apps/v1 + Deployment/apps/v1beta2: Deployment/apps/v1 + Endpoints/v1: + scope: Namespaced + plural: endpoints + diagram_node_classname: diagrams.k8s.network.Endpoint + edges: | + for sidx, subset in enumerate(resource.get("subsets", [])): + for aidx, address in enumerate(subset.get("addresses", [])): + if "targetRef" in address: + target = address["targetRef"] + edges.add_edge_to( + f"subsets[{sidx}].addresses[{aidx}].targetRef", + target["name"], + target.get("namespace"), + target["kind"], + "v1", + "REFERENCE", + data=target + ) + EndpointSlice/discovery.k8s.io/v1: + scope: Namespaced + custom_icon: $KD/icons/eps.png + edges: | + for idx, endpoint in enumerate(resource.get("endpoints", [])): + if "targetRef" in endpoint: + target = endpoint["targetRef"] + edges.add_edge_to( + f"endpoints[{idx}].targetRef", + target["name"], + target["namespace"], + target["kind"], + "v1", + "REFERENCE", + data=target + ) + Group/rbac.authorization.k8s.io/v1: + scope: Cluster + diagram_node_classname: diagrams.k8s.rbac.Group + # no edges + HorizontalPodAutoscaler/autoscaling/v1: + scope: Namespaced + diagram_node_classname: diagrams.k8s.clusterconfig.HPA + edges: | + edges.add_resource("spec.scaleTargetRef") + HorizontalPodAutoscaler/autoscaling/v2: + scope: Namespaced + diagram_node_classname: diagrams.k8s.clusterconfig.HPA + edges: | + edges.add_resource("spec.scaleTargetRef") + HorizontalPodAutoscaler/autoscaling/v2beta1: HorizontalPodAutoscaler/autoscaling/v2 + HorizontalPodAutoscaler/autoscaling/v2beta2: HorizontalPodAutoscaler/autoscaling/v2 + Ingress/networking.k8s.io/v1: + scope: Namespaced + diagram_node_classname: diagrams.k8s.network.Ing + edges: | + edges.add_service("spec.defaultBackend.service.name") + for ridx, rule in enumerate(query_path(resource, "spec.rules", [])): + for pidx, path in enumerate(query_path(rule, "http.paths", [])): + edges.add_service(f"spec.rules[{ridx}].http.paths[{pidx}].backend.service.name", name=query_path(path, "backend.service.name")) + edges.add_edge_to( + "spec.ingressClassName", + ".", + None, + "IngressClass", + "networking.k8s.io/v1", + "REFERENCE" + ) + for idx, tls in enumerate(query_path(resource, "spec.tls", [])): + edges.add_edge_to( + f"spec.tls[{idx}]", + tls.get("secretName"), + get_namespace(resource), + "Secret", + "v1", + "REFERENCE" + ) + Ingress/networking.k8s.io/v1beta1: Ingress/networking.k8s.io/v1 + Ingress/extensions/v1beta1: Ingress/networking.k8s.io/v1 + IngressClass/networking.k8s.io/v1: + scope: Cluster + custom_icon: $KD/icons/ic.png + edges: | + # TODO: Following works well with ingress-nginx but was not + # been tested with any other ingress implementations. + edges.add_all_resources_matching_labels("Pod", "metadata.labels", + edge_kind="CONTROLLED_BY") + IngressClass/networking.k8s.io/v1beta1: IngressClass/networking.k8s.io/v1 + Job/batch/v1: + scope: Namespaced + diagram_node_classname: diagrams.k8s.compute.Job + edges: | + edges.add_owned_resources() + edges.add_service_account("spec.template.spec") + edges.add_all_volume_resources("spec.template.spec.volumes") + edges.add_containers_env_value_from_and_env_from("spec.template.spec.containers") + edges.add_containers_env_value_from_and_env_from("spec.template.spec.initContainers") + edges.add_containers_env_value_from_and_env_from("spec.template.spec.ephemeralContainers") + edges.add_wait_for_services("spec.template.spec.initContainers") + edges.add_networks("spec.template.metadata.annotations") + edges.add_priority_class("spec.template.spec.priorityClassName") + edges.add_edge_to( + "spec.template.spec.runtimeClassName", + ".", + None, + "RuntimeClass", + "node.k8s.io/v1", + "REFERENCE" + ) + Job/batch/v1beta1: Job/batch/v1 + Lease/coordination.k8s.io/v1: + scope: Namespaced + custom_icon: $KD/icons/lease.png + # no edges + LimitRange/v1: + scope: Namespaced + diagram_node_classname: diagrams.k8s.clusterconfig.LimitRange + # no edges + MutatingWebhookConfiguration/admissionregistration.k8s.io/v1: + scope: Cluster + custom_icon: $KD/icons/mwc.png + edges: | + edges.add_webhooks() + MutatingWebhookConfiguration/admissionregistration.k8s.io/v1beta1: MutatingWebhookConfiguration/admissionregistration.k8s.io/v1 + Namespace/v1: + scope: Cluster + diagram_node_classname: diagrams.k8s.group.NS + # no edges + NetworkAttachmentDefinition/k8s.cni.cncf.io/v1: + scope: Namespaced + diagram_node_classname: diagrams.azure.network.NetworkInterfaces + # no edges + NetworkPolicy/networking.k8s.io/v1: + scope: Namespaced + diagram_node_classname: diagrams.k8s.network.NetworkPolicy + edges: | + edges.add_all_workload_resources( + "spec.podSelector.matchLabels", + default_selector={}, + edge_kind="SELECTOR-UP" + ) + edges.add_ingress_and_egress_rules() + Node/v1: + scope: Cluster + diagram_node_classname: diagrams.k8s.infra.Node + edges: | + edges.add_owned_resources() + PersistentVolume/v1: + scope: Cluster + diagram_node_classname: diagrams.k8s.storage.PV + edges: | + edges.add_edge_to( + "spec.storageClassName", + ".", + None, + "StorageClass", + "storage.k8s.io/v1", + "REFERENCE" + ) + PersistentVolumeClaim/v1: + scope: Namespaced + diagram_node_classname: diagrams.k8s.storage.PVC + edges: | + if query_path(resource, "spec.storageClassName") != "": + edges.add_edge_to( + "spec.storageClassName", + ".", + None, + "StorageClass", + "storage.k8s.io/v1", + "REFERENCE" + ) + edges.add_edge_to( + "spec.volumeName", + ".", + None, + "PersistentVolume", + "v1", + "REFERENCE" + ) + PriorityClass/scheduling.k8s.io/v1: + scope: Cluster + custom_icon: $KD/icons/pc.png + # no edges + PriorityClass/scheduling.k8s.io/v1beta1: PriorityClass/scheduling.k8s.io/v1 + Pod/v1: + scope: Namespaced + diagram_node_classname: diagrams.k8s.compute.Pod + edges: | + edges.add_service_account("spec") + edges.add_all_volume_resources("spec.volumes") + edges.add_containers_env_value_from_and_env_from("spec.containers") + edges.add_containers_env_value_from_and_env_from("spec.initContainers") + edges.add_containers_env_value_from_and_env_from("spec.ephemeralContainers") + edges.add_wait_for_services("spec.initContainers") + edges.add_edge_to( + "spec.nodeName", + ".", + None, + "Node", + "v1", + "REFERENCE" + ) + edges.add_networks("metadata.annotations") + edges.add_priority_class("spec.priorityClassName") + edges.add_edge_to( + "spec.runtimeClassName", + ".", + None, + "RuntimeClass", + "node.k8s.io/v1", + "REFERENCE" + ) + PodDisruptionBudget/policy/v1: + scope: Namespaced + custom_icon: $KD/icons/pdb.png + edges: | + edges.add_all_workload_resources( + "spec.selector.matchLabels", + edge_kind="SELECTOR-UP" + ) + PodDisruptionBudget/policy/v1beta1: PodDisruptionBudget/policy/v1 + PodSecurityPolicy/policy/v1: + scope: Namespaced + plural: podsecuritypolicies + diagram_node_classname: diagrams.k8s.others.PSP + # no edges + PodSecurityPolicy/policy/v1beta1: PodSecurityPolicy/policy/v1 + PodSecurityPolicy/extensions/v1beta1: PodSecurityPolicy/policy/v1 + PodTemplate/v1: + scope: Namespaced + custom_icon: $KD/icons/podtemplate.png + edges: | + edges.add_service_account("template.spec") + edges.add_all_volume_resources("template.spec.volumes") + edges.add_containers_env_value_from_and_env_from("template.spec.containers") + edges.add_containers_env_value_from_and_env_from("template.spec.initContainers") + edges.add_containers_env_value_from_and_env_from("template.spec.ephemeralContainers") + edges.add_wait_for_services("template.spec.initContainers") + edges.add_edge_to( + "template.spec.nodeName", + ".", + None, + "Node", + "v1", + "REFERENCE" + ) + edges.add_networks("template.metadata.annotations") + edges.add_priority_class("template.spec.priorityClassName") + edges.add_edge_to( + "template.spec.runtimeClassName", + ".", + None, + "RuntimeClass", + "node.k8s.io/v1", + "REFERENCE" + ) + ReplicaSet/apps/v1: + scope: Namespaced + diagram_node_classname: diagrams.k8s.compute.RS + edges: | + edges.add_owned_resources() + edges.add_service_account("spec.template.spec") + edges.add_all_volume_resources("spec.template.spec.volumes") + edges.add_containers_env_value_from_and_env_from("spec.template.spec.containers") + edges.add_containers_env_value_from_and_env_from("spec.template.spec.initContainers") + edges.add_containers_env_value_from_and_env_from("spec.template.spec.ephemeralContainers") + edges.add_wait_for_services("spec.template.spec.initContainers") + edges.add_networks("spec.template.metadata.annotations") + edges.add_priority_class("spec.template.spec.priorityClassName") + edges.add_edge_to( + "spec.template.spec.runtimeClassName", + ".", + None, + "RuntimeClass", + "node.k8s.io/v1", + "REFERENCE" + ) + ReplicationController/v1: + scope: Namespaced + custom_icon: $KD/icons/rc.png + edges: | + edges.add_owned_resources() + edges.add_service_account("spec.template.spec") + edges.add_all_volume_resources("spec.template.spec.volumes") + edges.add_containers_env_value_from_and_env_from("spec.template.spec.containers") + edges.add_containers_env_value_from_and_env_from("spec.template.spec.initContainers") + edges.add_containers_env_value_from_and_env_from("spec.template.spec.ephemeralContainers") + edges.add_wait_for_services("spec.template.spec.initContainers") + edges.add_networks("spec.template.metadata.annotations") + edges.add_priority_class("spec.template.spec.priorityClassName") + edges.add_edge_to( + "spec.template.spec.runtimeClassName", + ".", + None, + "RuntimeClass", + "node.k8s.io/v1", + "REFERENCE" + ) + ResourceQuota/v1: + scope: Namespaced + diagram_node_classname: diagrams.k8s.clusterconfig.Quota + # no edges + Role/rbac.authorization.k8s.io/v1: + scope: Namespaced + diagram_node_classname: diagrams.k8s.rbac.Role + nodes: | + create_node_for_role_rules_resource_names(resource, nodes) + edges: | + edges.add_rules_resource_names() + Role/rbac.authorization.k8s.io/v1beta1: Role/rbac.authorization.k8s.io/v1 + RoleBinding/rbac.authorization.k8s.io/v1: + scope: Namespaced + diagram_node_classname: diagrams.k8s.rbac.RB + edges: | + edges.add_role("roleRef") + edges.add_subjects() + RoleBinding/rbac.authorization.k8s.io/v1beta1: RoleBinding/rbac.authorization.k8s.io/v1 + RuntimeClass/node.k8s.io/v1: + scope: Cluster + custom_icon: $KD/icons/runtimeclass.png + Secret/v1: + scope: Namespaced + diagram_node_classname: diagrams.k8s.podconfig.Secret + # no edges + Service/v1: + scope: Namespaced + diagram_node_classname: diagrams.k8s.network.SVC + edges: | + edges.add_edges_for_service() + ServiceAccount/v1: + scope: Namespaced + diagram_node_classname: diagrams.k8s.rbac.SA + # no edges + StatefulSet/apps/v1: + scoped: Namespaced + diagram_node_classname: diagrams.k8s.compute.STS + nodes: | + namespace = resource["metadata"].get("namespace") + for vct in query_path(resource, "spec.volumeClaimTemplates", []): + pvc = { + "kind": "PersistentVolumeClaim", + "apiVersion": "v1", + **vct + } + pvc["metadata"]["name"] = f'{pvc["metadata"]["name"]}-{resource["metadata"]["name"]}' + if namespace is not None: + pvc["metadata"]["namespace"] = namespace + pvc["metadata"]["labels"] = { + **query_path(resource, "metadata.labels", {}), + **query_path(pvc, "metadata.labels", {}) + } + nodes.append(pvc) + edges: | + edges.add_owned_resources() + edges.add_service_account("spec.template.spec") + edges.add_all_volume_resources("spec.template.spec.volumes") + edges.add_containers_env_value_from_and_env_from("spec.template.spec.containers") + edges.add_containers_env_value_from_and_env_from("spec.template.spec.initContainers") + edges.add_containers_env_value_from_and_env_from("spec.template.spec.ephemeralContainers") + edges.add_wait_for_services("spec.template.spec.initContainers") + edges.add_networks("spec.template.metadata.annotations") + edges.add_priority_class("spec.template.spec.priorityClassName") + edges.add_edge_to( + "spec.template.spec.runtimeClassName", + ".", + None, + "RuntimeClass", + "node.k8s.io/v1", + "REFERENCE" + ) + edges.add_volume_claim_templates("spec.volumeClaimTemplates") + edges.add_service("spec.serviceName") + StatefulSet/apps/v1beta1: StatefulSet/apps/v1 + StatefulSet/apps/v1beta2: StatefulSet/apps/v1 + StorageClass/storage.k8s.io/v1: + scope: Cluster + diagram_node_classname: diagrams.k8s.storage.SC + edges: | + edges.add_edge_to( + "provisioner", + ".", + None, + "CSIDriver", + "storage.k8s.io/v1", + "REFERENCE" + ) + StorageClass/storage.k8s.io/v1beta1: StorageClass/storage.k8s.io/v1 + User/rbac.authorization.k8s.io/v1: + scope: Cluster + diagram_node_classname: diagrams.k8s.rbac.User + # no edges + ValidatingWebhookConfiguration/admissionregistration.k8s.io/v1: + scope: Cluster + custom_icon: $KD/icons/vwc.png + edges: | + edges.add_webhooks() + ValidatingWebhookConfiguration/admissionregistration.k8s.io/v1beta1: ValidatingWebhookConfiguration/admissionregistration.k8s.io/v1 + VerticalPodAutoscaler/autoscaling.k8s.io/v1: + scoped: Namespaced + custom_icon: $KD/icons/vpa.png + edges: | + edges.add_resource("spec.targetRef") + VolumeAttachment/storage.k8s.io/v1: + scope: Cluster + diagram_node_classname: diagrams.k8s.storage.Volume + edges: | + edges.add_edge_to( + "spec.nodeName", + ".", + None, + "Node", + "v1", + "REFERENCE" + ) +cluster-resources: + # default namespace + - default/default/ServiceAccount/v1 + - kube-root-ca.crt/default/ConfigMap/v1 + - kubernetes/default/Service/v1 + # kube-system namespace + - extension-apiserver-authentication-reader/kube-system/Role/rbac.authorization.k8s.io/v1 + - horizontal-pod-autoscaler/kube-system/ServiceAccount/v1 + # cluster scope + - admin/ClusterRole/rbac.authorization.k8s.io/v1 + - cluster-admin/ClusterRole/rbac.authorization.k8s.io/v1 + - edit/ClusterRole/rbac.authorization.k8s.io/v1 + - generic-garbage-collector/kube-system/ServiceAccount/v1 + - standard/StorageClass/storage.k8s.io/v1 + - system:anonymous/User/rbac.authorization.k8s.io/v1 + - system:auth-delegator/ClusterRole/rbac.authorization.k8s.io/v1 + - system:authenticated/Group/rbac.authorization.k8s.io/v1 + - system:coredns/ClusterRole/rbac.authorization.k8s.io/v1 + - system:discovery/ClusterRole/rbac.authorization.k8s.io/v1 + - system:node-proxier/ClusterRole/rbac.authorization.k8s.io/v1 + - system:nodes/Group/rbac.authorization.k8s.io/v1 + - system:unauthenticated/Group/rbac.authorization.k8s.io/v1 + - system:persistent-volume-provisioner/ClusterRole/rbac.authorization.k8s.io/v1 + - system:service-account-issuer-discovery/ClusterRole/rbac.authorization.k8s.io/v1 + - system:serviceaccounts/Group/rbac.authorization.k8s.io/v1 + - system:volume-scheduler/ClusterRole/rbac.authorization.k8s.io/v1 + - system-cluster-critical/PriorityClass/scheduling.k8s.io/v1 + - system-node-critical/PriorityClass/scheduling.k8s.io/v1 + - view/ClusterRole/rbac.authorization.k8s.io/v1 diff --git a/webapp/backend/venv/bin/kubectl-diagrams b/webapp/backend/venv/bin/kubectl-diagrams new file mode 100755 index 0000000..9388809 --- /dev/null +++ b/webapp/backend/venv/bin/kubectl-diagrams @@ -0,0 +1,110 @@ +#!/bin/bash + +# kubectl diagrams: A kubectl plugin to generate a diagram of Kubernetes resources +# using kube-diagrams from the output of 'kubectl get -o yaml' + +# Version +VERSION="0.1.0" + +# Check if kube-diagrams is installed +if ! command -v kube-diagrams &>/dev/null; then + echo "Error: kube-diagrams is not installed. Please install it first." + exit 1 +fi + +# Check if kubectl is installed +if ! command -v kubectl &>/dev/null; then + echo "Error: kubectl is not installed. Please install it first." + exit 1 +fi + +# Display help message +show_help() { + echo "Usage: kubectl diagrams [OPTIONS] " + echo "" + echo "A kubectl plugin to generate diagrams of Kubernetes resources using kube-diagrams." + echo "" + echo "Options:" + echo " -n, --namespace Specify the namespace to query resources from" + echo " -o, --output Specify the output file for the diagram" + echo " -f, --format Specify the output format (e.g., png, svg)" + echo " --embed-all-icons Embed all icons into svg or dot_json output diagrams" + echo " -c, --config Specify the custom kube-diagrams configuration file" + echo " --without-namespace Exclude namespace from the diagram" + echo " --version Display the version number" + echo " --help Display this help message" + echo "" + echo "Examples:" + echo " kubectl diagrams pod,service -n my-namespace -o diagram.png" + echo " kubectl diagrams deployment --without-namespace -f svg" + echo " kubectl diagrams --version" + exit 0 +} + +# Initialize variables +KUBE_DIAGRAMS_ARGS=("-") # Default to stdin indicator +KUBECTL_ARGS=() + +# Parse arguments +while [[ $# -gt 0 ]]; do + case "$1" in + --version) + echo "kubectl diagrams version $VERSION" + exit + ;; + --help) + show_help + ;; + -n | --namespace) + if [[ -n "$2" ]]; then + KUBECTL_ARGS+=("-n" "$2") + shift 2 + else + echo "Error: --namespace requires a value" + exit 64 + fi + ;; + -o | --output | -f | --format | -c | --config | --without-namespace) + KUBE_DIAGRAMS_ARGS+=("$1") + if [[ "$1" == "-o" || "$1" == "--output" || "$1" == "-f" || "$1" == "--format" || "$1" == "-c" || "$1" == "--config" ]]; then + if [[ -n "$2" ]]; then + KUBE_DIAGRAMS_ARGS+=("$2") + shift 2 + else + echo "Error: $1 requires a value" + exit 64 + fi + else + shift + fi + ;; + --embed-all-icons) + KUBE_DIAGRAMS_ARGS+=("$1") + shift + ;; + *) + KUBECTL_ARGS+=("$1") + shift + ;; + esac +done + +# Check if any resources were specified +if [[ ${#KUBECTL_ARGS[@]} -eq 0 ]]; then + echo "Error: At least one resource kind must be specified (e.g., pod, service, deployment)" + exit 64 +fi + +# Add default output format for kubectl +KUBECTL_ARGS+=("-o" "yaml") + +# Execute kubectl and pipe to kube-diagrams with arguments +kubectl get "${KUBECTL_ARGS[@]}" | kube-diagrams "${KUBE_DIAGRAMS_ARGS[@]}" +EXIT_CODE=$? + +if [[ $EXIT_CODE -eq 0 ]]; then + echo "Diagram generated successfully" +else + echo "Error: Failed to generate diagram" + exit 1 +fi diff --git a/webapp/backend/venv/bin/nodeenv b/webapp/backend/venv/bin/nodeenv new file mode 100755 index 0000000..8b9e052 --- /dev/null +++ b/webapp/backend/venv/bin/nodeenv @@ -0,0 +1,8 @@ +#!/home/spirals/Documents/Workspace/Inria/kubediagramswebui/backend/venv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from nodeenv import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/webapp/backend/venv/bin/normalizer b/webapp/backend/venv/bin/normalizer new file mode 100755 index 0000000..64dc82d --- /dev/null +++ b/webapp/backend/venv/bin/normalizer @@ -0,0 +1,8 @@ +#!/home/spirals/Documents/Workspace/Inria/kubediagramswebui/backend/venv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from charset_normalizer.cli import cli_detect +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(cli_detect()) diff --git a/webapp/backend/venv/bin/pip b/webapp/backend/venv/bin/pip new file mode 100755 index 0000000..76f6edf --- /dev/null +++ b/webapp/backend/venv/bin/pip @@ -0,0 +1,8 @@ +#!/home/spirals/Documents/Workspace/Inria/kubediagramswebui/backend/venv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from pip._internal.cli.main import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/webapp/backend/venv/bin/pip-compile b/webapp/backend/venv/bin/pip-compile new file mode 100755 index 0000000..d3d94ac --- /dev/null +++ b/webapp/backend/venv/bin/pip-compile @@ -0,0 +1,8 @@ +#!/home/spirals/Documents/Workspace/Inria/kubediagramswebui/backend/venv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from piptools.scripts.compile import cli +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(cli()) diff --git a/webapp/backend/venv/bin/pip-sync b/webapp/backend/venv/bin/pip-sync new file mode 100755 index 0000000..5d6ab8d --- /dev/null +++ b/webapp/backend/venv/bin/pip-sync @@ -0,0 +1,8 @@ +#!/home/spirals/Documents/Workspace/Inria/kubediagramswebui/backend/venv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from piptools.scripts.sync import cli +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(cli()) diff --git a/webapp/backend/venv/bin/pip3 b/webapp/backend/venv/bin/pip3 new file mode 100755 index 0000000..76f6edf --- /dev/null +++ b/webapp/backend/venv/bin/pip3 @@ -0,0 +1,8 @@ +#!/home/spirals/Documents/Workspace/Inria/kubediagramswebui/backend/venv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from pip._internal.cli.main import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/webapp/backend/venv/bin/pip3.12 b/webapp/backend/venv/bin/pip3.12 new file mode 100755 index 0000000..76f6edf --- /dev/null +++ b/webapp/backend/venv/bin/pip3.12 @@ -0,0 +1,8 @@ +#!/home/spirals/Documents/Workspace/Inria/kubediagramswebui/backend/venv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from pip._internal.cli.main import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/webapp/backend/venv/bin/pre-commit b/webapp/backend/venv/bin/pre-commit new file mode 100755 index 0000000..89e5892 --- /dev/null +++ b/webapp/backend/venv/bin/pre-commit @@ -0,0 +1,8 @@ +#!/home/spirals/Documents/Workspace/Inria/kubediagramswebui/backend/venv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from pre_commit.main import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/webapp/backend/venv/bin/pyproject-build b/webapp/backend/venv/bin/pyproject-build new file mode 100755 index 0000000..cce4c19 --- /dev/null +++ b/webapp/backend/venv/bin/pyproject-build @@ -0,0 +1,8 @@ +#!/home/spirals/Documents/Workspace/Inria/kubediagramswebui/backend/venv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from build.__main__ import entrypoint +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(entrypoint()) diff --git a/webapp/backend/venv/bin/python b/webapp/backend/venv/bin/python new file mode 120000 index 0000000..b8a0adb --- /dev/null +++ b/webapp/backend/venv/bin/python @@ -0,0 +1 @@ +python3 \ No newline at end of file diff --git a/webapp/backend/venv/bin/python3 b/webapp/backend/venv/bin/python3 new file mode 120000 index 0000000..ae65fda --- /dev/null +++ b/webapp/backend/venv/bin/python3 @@ -0,0 +1 @@ +/usr/bin/python3 \ No newline at end of file diff --git a/webapp/backend/venv/bin/python3.12 b/webapp/backend/venv/bin/python3.12 new file mode 120000 index 0000000..b8a0adb --- /dev/null +++ b/webapp/backend/venv/bin/python3.12 @@ -0,0 +1 @@ +python3 \ No newline at end of file diff --git a/webapp/backend/venv/bin/virtualenv b/webapp/backend/venv/bin/virtualenv new file mode 100755 index 0000000..1708196 --- /dev/null +++ b/webapp/backend/venv/bin/virtualenv @@ -0,0 +1,8 @@ +#!/home/spirals/Documents/Workspace/Inria/kubediagramswebui/backend/venv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from virtualenv.__main__ import run_with_catch +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(run_with_catch()) diff --git a/webapp/backend/venv/bin/wheel b/webapp/backend/venv/bin/wheel new file mode 100755 index 0000000..ebc3c15 --- /dev/null +++ b/webapp/backend/venv/bin/wheel @@ -0,0 +1,8 @@ +#!/home/spirals/Documents/Workspace/Inria/kubediagramswebui/backend/venv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from wheel._commands import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/webapp/backend/venv/lib/python3.12/site-packages/MarkupSafe-3.0.2.dist-info/INSTALLER b/webapp/backend/venv/lib/python3.12/site-packages/MarkupSafe-3.0.2.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/webapp/backend/venv/lib/python3.12/site-packages/MarkupSafe-3.0.2.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/webapp/backend/venv/lib/python3.12/site-packages/MarkupSafe-3.0.2.dist-info/LICENSE.txt b/webapp/backend/venv/lib/python3.12/site-packages/MarkupSafe-3.0.2.dist-info/LICENSE.txt new file mode 100644 index 0000000..9d227a0 --- /dev/null +++ b/webapp/backend/venv/lib/python3.12/site-packages/MarkupSafe-3.0.2.dist-info/LICENSE.txt @@ -0,0 +1,28 @@ +Copyright 2010 Pallets + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/webapp/backend/venv/lib/python3.12/site-packages/MarkupSafe-3.0.2.dist-info/METADATA b/webapp/backend/venv/lib/python3.12/site-packages/MarkupSafe-3.0.2.dist-info/METADATA new file mode 100644 index 0000000..82261f2 --- /dev/null +++ b/webapp/backend/venv/lib/python3.12/site-packages/MarkupSafe-3.0.2.dist-info/METADATA @@ -0,0 +1,92 @@ +Metadata-Version: 2.1 +Name: MarkupSafe +Version: 3.0.2 +Summary: Safely add untrusted strings to HTML/XML markup. +Maintainer-email: Pallets +License: Copyright 2010 Pallets + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + 3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Project-URL: Donate, https://palletsprojects.com/donate +Project-URL: Documentation, https://markupsafe.palletsprojects.com/ +Project-URL: Changes, https://markupsafe.palletsprojects.com/changes/ +Project-URL: Source, https://github.com/pallets/markupsafe/ +Project-URL: Chat, https://discord.gg/pallets +Classifier: Development Status :: 5 - Production/Stable +Classifier: Environment :: Web Environment +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: BSD License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content +Classifier: Topic :: Text Processing :: Markup :: HTML +Classifier: Typing :: Typed +Requires-Python: >=3.9 +Description-Content-Type: text/markdown +License-File: LICENSE.txt + +# MarkupSafe + +MarkupSafe implements a text object that escapes characters so it is +safe to use in HTML and XML. Characters that have special meanings are +replaced so that they display as the actual characters. This mitigates +injection attacks, meaning untrusted user input can safely be displayed +on a page. + + +## Examples + +```pycon +>>> from markupsafe import Markup, escape + +>>> # escape replaces special characters and wraps in Markup +>>> escape("") +Markup('<script>alert(document.cookie);</script>') + +>>> # wrap in Markup to mark text "safe" and prevent escaping +>>> Markup("Hello") +Markup('hello') + +>>> escape(Markup("Hello")) +Markup('hello') + +>>> # Markup is a str subclass +>>> # methods and operators escape their arguments +>>> template = Markup("Hello {name}") +>>> template.format(name='"World"') +Markup('Hello "World"') +``` + +## Donate + +The Pallets organization develops and supports MarkupSafe and other +popular packages. In order to grow the community of contributors and +users, and allow the maintainers to devote more time to the projects, +[please donate today][]. + +[please donate today]: https://palletsprojects.com/donate diff --git a/webapp/backend/venv/lib/python3.12/site-packages/MarkupSafe-3.0.2.dist-info/RECORD b/webapp/backend/venv/lib/python3.12/site-packages/MarkupSafe-3.0.2.dist-info/RECORD new file mode 100644 index 0000000..0675c5a --- /dev/null +++ b/webapp/backend/venv/lib/python3.12/site-packages/MarkupSafe-3.0.2.dist-info/RECORD @@ -0,0 +1,15 @@ +MarkupSafe-3.0.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +MarkupSafe-3.0.2.dist-info/LICENSE.txt,sha256=SJqOEQhQntmKN7uYPhHg9-HTHwvY-Zp5yESOf_N9B-o,1475 +MarkupSafe-3.0.2.dist-info/METADATA,sha256=aAwbZhSmXdfFuMM-rEHpeiHRkBOGESyVLJIuwzHP-nw,3975 +MarkupSafe-3.0.2.dist-info/RECORD,, +MarkupSafe-3.0.2.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +MarkupSafe-3.0.2.dist-info/WHEEL,sha256=OVgtqZzfzIXXtylXP90gxCZ6CKBCwKYyHM8PpMEjN1M,151 +MarkupSafe-3.0.2.dist-info/top_level.txt,sha256=qy0Plje5IJuvsCBjejJyhDCjEAdcDLK_2agVcex8Z6U,11 +markupsafe/__init__.py,sha256=sr-U6_27DfaSrj5jnHYxWN-pvhM27sjlDplMDPZKm7k,13214 +markupsafe/__pycache__/__init__.cpython-312.pyc,, +markupsafe/__pycache__/_native.cpython-312.pyc,, +markupsafe/_native.py,sha256=hSLs8Jmz5aqayuengJJ3kdT5PwNpBWpKrmQSdipndC8,210 +markupsafe/_speedups.c,sha256=O7XulmTo-epI6n2FtMVOrJXl8EAaIwD2iNYmBI5SEoQ,4149 +markupsafe/_speedups.cpython-312-x86_64-linux-gnu.so,sha256=t1DBZlpsjFA30BOOvXfXfT1wvO_4cS16VbHz1-49q5U,43432 +markupsafe/_speedups.pyi,sha256=ENd1bYe7gbBUf2ywyYWOGUpnXOHNJ-cgTNqetlW8h5k,41 +markupsafe/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 diff --git a/webapp/backend/venv/lib/python3.12/site-packages/MarkupSafe-3.0.2.dist-info/REQUESTED b/webapp/backend/venv/lib/python3.12/site-packages/MarkupSafe-3.0.2.dist-info/REQUESTED new file mode 100644 index 0000000..e69de29 diff --git a/webapp/backend/venv/lib/python3.12/site-packages/MarkupSafe-3.0.2.dist-info/WHEEL b/webapp/backend/venv/lib/python3.12/site-packages/MarkupSafe-3.0.2.dist-info/WHEEL new file mode 100644 index 0000000..057fef6 --- /dev/null +++ b/webapp/backend/venv/lib/python3.12/site-packages/MarkupSafe-3.0.2.dist-info/WHEEL @@ -0,0 +1,6 @@ +Wheel-Version: 1.0 +Generator: setuptools (75.2.0) +Root-Is-Purelib: false +Tag: cp312-cp312-manylinux_2_17_x86_64 +Tag: cp312-cp312-manylinux2014_x86_64 + diff --git a/webapp/backend/venv/lib/python3.12/site-packages/MarkupSafe-3.0.2.dist-info/top_level.txt b/webapp/backend/venv/lib/python3.12/site-packages/MarkupSafe-3.0.2.dist-info/top_level.txt new file mode 100644 index 0000000..75bf729 --- /dev/null +++ b/webapp/backend/venv/lib/python3.12/site-packages/MarkupSafe-3.0.2.dist-info/top_level.txt @@ -0,0 +1 @@ +markupsafe diff --git a/webapp/backend/venv/lib/python3.12/site-packages/PyYAML-6.0.2.dist-info/INSTALLER b/webapp/backend/venv/lib/python3.12/site-packages/PyYAML-6.0.2.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/webapp/backend/venv/lib/python3.12/site-packages/PyYAML-6.0.2.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/webapp/backend/venv/lib/python3.12/site-packages/PyYAML-6.0.2.dist-info/LICENSE b/webapp/backend/venv/lib/python3.12/site-packages/PyYAML-6.0.2.dist-info/LICENSE new file mode 100644 index 0000000..2f1b8e1 --- /dev/null +++ b/webapp/backend/venv/lib/python3.12/site-packages/PyYAML-6.0.2.dist-info/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2017-2021 Ingy döt Net +Copyright (c) 2006-2016 Kirill Simonov + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/webapp/backend/venv/lib/python3.12/site-packages/PyYAML-6.0.2.dist-info/METADATA b/webapp/backend/venv/lib/python3.12/site-packages/PyYAML-6.0.2.dist-info/METADATA new file mode 100644 index 0000000..db029b7 --- /dev/null +++ b/webapp/backend/venv/lib/python3.12/site-packages/PyYAML-6.0.2.dist-info/METADATA @@ -0,0 +1,46 @@ +Metadata-Version: 2.1 +Name: PyYAML +Version: 6.0.2 +Summary: YAML parser and emitter for Python +Home-page: https://pyyaml.org/ +Download-URL: https://pypi.org/project/PyYAML/ +Author: Kirill Simonov +Author-email: xi@resolvent.net +License: MIT +Project-URL: Bug Tracker, https://github.com/yaml/pyyaml/issues +Project-URL: CI, https://github.com/yaml/pyyaml/actions +Project-URL: Documentation, https://pyyaml.org/wiki/PyYAMLDocumentation +Project-URL: Mailing lists, http://lists.sourceforge.net/lists/listinfo/yaml-core +Project-URL: Source Code, https://github.com/yaml/pyyaml +Platform: Any +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Cython +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Classifier: Topic :: Text Processing :: Markup +Requires-Python: >=3.8 +License-File: LICENSE + +YAML is a data serialization format designed for human readability +and interaction with scripting languages. PyYAML is a YAML parser +and emitter for Python. + +PyYAML features a complete YAML 1.1 parser, Unicode support, pickle +support, capable extension API, and sensible error messages. PyYAML +supports standard YAML tags and provides Python-specific tags that +allow to represent an arbitrary Python object. + +PyYAML is applicable for a broad range of tasks from complex +configuration files to object serialization and persistence. diff --git a/webapp/backend/venv/lib/python3.12/site-packages/PyYAML-6.0.2.dist-info/RECORD b/webapp/backend/venv/lib/python3.12/site-packages/PyYAML-6.0.2.dist-info/RECORD new file mode 100644 index 0000000..cdcd0f3 --- /dev/null +++ b/webapp/backend/venv/lib/python3.12/site-packages/PyYAML-6.0.2.dist-info/RECORD @@ -0,0 +1,44 @@ +PyYAML-6.0.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +PyYAML-6.0.2.dist-info/LICENSE,sha256=jTko-dxEkP1jVwfLiOsmvXZBAqcoKVQwfT5RZ6V36KQ,1101 +PyYAML-6.0.2.dist-info/METADATA,sha256=9-odFB5seu4pGPcEv7E8iyxNF51_uKnaNGjLAhz2lto,2060 +PyYAML-6.0.2.dist-info/RECORD,, +PyYAML-6.0.2.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +PyYAML-6.0.2.dist-info/WHEEL,sha256=1pP4yhrbipRtdbm4Rbg3aoTjzc7pDhpHKO0CEY24CNM,152 +PyYAML-6.0.2.dist-info/top_level.txt,sha256=rpj0IVMTisAjh_1vG3Ccf9v5jpCQwAz6cD1IVU5ZdhQ,11 +_yaml/__init__.py,sha256=04Ae_5osxahpJHa3XBZUAf4wi6XX32gR8D6X6p64GEA,1402 +_yaml/__pycache__/__init__.cpython-312.pyc,, +yaml/__init__.py,sha256=N35S01HMesFTe0aRRMWkPj0Pa8IEbHpE9FK7cr5Bdtw,12311 +yaml/__pycache__/__init__.cpython-312.pyc,, +yaml/__pycache__/composer.cpython-312.pyc,, +yaml/__pycache__/constructor.cpython-312.pyc,, +yaml/__pycache__/cyaml.cpython-312.pyc,, +yaml/__pycache__/dumper.cpython-312.pyc,, +yaml/__pycache__/emitter.cpython-312.pyc,, +yaml/__pycache__/error.cpython-312.pyc,, +yaml/__pycache__/events.cpython-312.pyc,, +yaml/__pycache__/loader.cpython-312.pyc,, +yaml/__pycache__/nodes.cpython-312.pyc,, +yaml/__pycache__/parser.cpython-312.pyc,, +yaml/__pycache__/reader.cpython-312.pyc,, +yaml/__pycache__/representer.cpython-312.pyc,, +yaml/__pycache__/resolver.cpython-312.pyc,, +yaml/__pycache__/scanner.cpython-312.pyc,, +yaml/__pycache__/serializer.cpython-312.pyc,, +yaml/__pycache__/tokens.cpython-312.pyc,, +yaml/_yaml.cpython-312-x86_64-linux-gnu.so,sha256=PJFgxnc0f5Dyde6WKmBm6fZWapawmWl7aBRruXjRA80,2481784 +yaml/composer.py,sha256=_Ko30Wr6eDWUeUpauUGT3Lcg9QPBnOPVlTnIMRGJ9FM,4883 +yaml/constructor.py,sha256=kNgkfaeLUkwQYY_Q6Ff1Tz2XVw_pG1xVE9Ak7z-viLA,28639 +yaml/cyaml.py,sha256=6ZrAG9fAYvdVe2FK_w0hmXoG7ZYsoYUwapG8CiC72H0,3851 +yaml/dumper.py,sha256=PLctZlYwZLp7XmeUdwRuv4nYOZ2UBnDIUy8-lKfLF-o,2837 +yaml/emitter.py,sha256=jghtaU7eFwg31bG0B7RZea_29Adi9CKmXq_QjgQpCkQ,43006 +yaml/error.py,sha256=Ah9z-toHJUbE9j-M8YpxgSRM5CgLCcwVzJgLLRF2Fxo,2533 +yaml/events.py,sha256=50_TksgQiE4up-lKo_V-nBy-tAIxkIPQxY5qDhKCeHw,2445 +yaml/loader.py,sha256=UVa-zIqmkFSCIYq_PgSGm4NSJttHY2Rf_zQ4_b1fHN0,2061 +yaml/nodes.py,sha256=gPKNj8pKCdh2d4gr3gIYINnPOaOxGhJAUiYhGRnPE84,1440 +yaml/parser.py,sha256=ilWp5vvgoHFGzvOZDItFoGjD6D42nhlZrZyjAwa0oJo,25495 +yaml/reader.py,sha256=0dmzirOiDG4Xo41RnuQS7K9rkY3xjHiVasfDMNTqCNw,6794 +yaml/representer.py,sha256=IuWP-cAW9sHKEnS0gCqSa894k1Bg4cgTxaDwIcbRQ-Y,14190 +yaml/resolver.py,sha256=9L-VYfm4mWHxUD1Vg4X7rjDRK_7VZd6b92wzq7Y2IKY,9004 +yaml/scanner.py,sha256=YEM3iLZSaQwXcQRg2l2R4MdT0zGP2F9eHkKGKnHyWQY,51279 +yaml/serializer.py,sha256=ChuFgmhU01hj4xgI8GaKv6vfM2Bujwa9i7d2FAHj7cA,4165 +yaml/tokens.py,sha256=lTQIzSVw8Mg9wv459-TjiOQe6wVziqaRlqX2_89rp54,2573 diff --git a/webapp/backend/venv/lib/python3.12/site-packages/PyYAML-6.0.2.dist-info/REQUESTED b/webapp/backend/venv/lib/python3.12/site-packages/PyYAML-6.0.2.dist-info/REQUESTED new file mode 100644 index 0000000..e69de29 diff --git a/webapp/backend/venv/lib/python3.12/site-packages/PyYAML-6.0.2.dist-info/WHEEL b/webapp/backend/venv/lib/python3.12/site-packages/PyYAML-6.0.2.dist-info/WHEEL new file mode 100644 index 0000000..56616a8 --- /dev/null +++ b/webapp/backend/venv/lib/python3.12/site-packages/PyYAML-6.0.2.dist-info/WHEEL @@ -0,0 +1,6 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.44.0) +Root-Is-Purelib: false +Tag: cp312-cp312-manylinux_2_17_x86_64 +Tag: cp312-cp312-manylinux2014_x86_64 + diff --git a/webapp/backend/venv/lib/python3.12/site-packages/PyYAML-6.0.2.dist-info/top_level.txt b/webapp/backend/venv/lib/python3.12/site-packages/PyYAML-6.0.2.dist-info/top_level.txt new file mode 100644 index 0000000..e6475e9 --- /dev/null +++ b/webapp/backend/venv/lib/python3.12/site-packages/PyYAML-6.0.2.dist-info/top_level.txt @@ -0,0 +1,2 @@ +_yaml +yaml diff --git a/webapp/backend/venv/lib/python3.12/site-packages/__pycache__/cfgv.cpython-312.pyc b/webapp/backend/venv/lib/python3.12/site-packages/__pycache__/cfgv.cpython-312.pyc new file mode 100644 index 0000000..af3343b Binary files /dev/null and b/webapp/backend/venv/lib/python3.12/site-packages/__pycache__/cfgv.cpython-312.pyc differ diff --git a/webapp/backend/venv/lib/python3.12/site-packages/__pycache__/docopt.cpython-312.pyc b/webapp/backend/venv/lib/python3.12/site-packages/__pycache__/docopt.cpython-312.pyc new file mode 100644 index 0000000..a3f85b6 Binary files /dev/null and b/webapp/backend/venv/lib/python3.12/site-packages/__pycache__/docopt.cpython-312.pyc differ diff --git a/webapp/backend/venv/lib/python3.12/site-packages/__pycache__/nodeenv.cpython-312.pyc b/webapp/backend/venv/lib/python3.12/site-packages/__pycache__/nodeenv.cpython-312.pyc new file mode 100644 index 0000000..b421a60 Binary files /dev/null and b/webapp/backend/venv/lib/python3.12/site-packages/__pycache__/nodeenv.cpython-312.pyc differ diff --git a/webapp/backend/venv/lib/python3.12/site-packages/_distutils_hack/__init__.py b/webapp/backend/venv/lib/python3.12/site-packages/_distutils_hack/__init__.py new file mode 100644 index 0000000..94f71b9 --- /dev/null +++ b/webapp/backend/venv/lib/python3.12/site-packages/_distutils_hack/__init__.py @@ -0,0 +1,239 @@ +# don't import any costly modules +import os +import sys + +report_url = ( + "https://github.com/pypa/setuptools/issues/new?template=distutils-deprecation.yml" +) + + +def warn_distutils_present(): + if 'distutils' not in sys.modules: + return + import warnings + + warnings.warn( + "Distutils was imported before Setuptools, but importing Setuptools " + "also replaces the `distutils` module in `sys.modules`. This may lead " + "to undesirable behaviors or errors. To avoid these issues, avoid " + "using distutils directly, ensure that setuptools is installed in the " + "traditional way (e.g. not an editable install), and/or make sure " + "that setuptools is always imported before distutils." + ) + + +def clear_distutils(): + if 'distutils' not in sys.modules: + return + import warnings + + warnings.warn( + "Setuptools is replacing distutils. Support for replacing " + "an already imported distutils is deprecated. In the future, " + "this condition will fail. " + f"Register concerns at {report_url}" + ) + mods = [ + name + for name in sys.modules + if name == "distutils" or name.startswith("distutils.") + ] + for name in mods: + del sys.modules[name] + + +def enabled(): + """ + Allow selection of distutils by environment variable. + """ + which = os.environ.get('SETUPTOOLS_USE_DISTUTILS', 'local') + if which == 'stdlib': + import warnings + + warnings.warn( + "Reliance on distutils from stdlib is deprecated. Users " + "must rely on setuptools to provide the distutils module. " + "Avoid importing distutils or import setuptools first, " + "and avoid setting SETUPTOOLS_USE_DISTUTILS=stdlib. " + f"Register concerns at {report_url}" + ) + return which == 'local' + + +def ensure_local_distutils(): + import importlib + + clear_distutils() + + # With the DistutilsMetaFinder in place, + # perform an import to cause distutils to be + # loaded from setuptools._distutils. Ref #2906. + with shim(): + importlib.import_module('distutils') + + # check that submodules load as expected + core = importlib.import_module('distutils.core') + assert '_distutils' in core.__file__, core.__file__ + assert 'setuptools._distutils.log' not in sys.modules + + +def do_override(): + """ + Ensure that the local copy of distutils is preferred over stdlib. + + See https://github.com/pypa/setuptools/issues/417#issuecomment-392298401 + for more motivation. + """ + if enabled(): + warn_distutils_present() + ensure_local_distutils() + + +class _TrivialRe: + def __init__(self, *patterns) -> None: + self._patterns = patterns + + def match(self, string): + return all(pat in string for pat in self._patterns) + + +class DistutilsMetaFinder: + def find_spec(self, fullname, path, target=None): + # optimization: only consider top level modules and those + # found in the CPython test suite. + if path is not None and not fullname.startswith('test.'): + return None + + method_name = 'spec_for_{fullname}'.format(**locals()) + method = getattr(self, method_name, lambda: None) + return method() + + def spec_for_distutils(self): + if self.is_cpython(): + return None + + import importlib + import importlib.abc + import importlib.util + + try: + mod = importlib.import_module('setuptools._distutils') + except Exception: + # There are a couple of cases where setuptools._distutils + # may not be present: + # - An older Setuptools without a local distutils is + # taking precedence. Ref #2957. + # - Path manipulation during sitecustomize removes + # setuptools from the path but only after the hook + # has been loaded. Ref #2980. + # In either case, fall back to stdlib behavior. + return None + + class DistutilsLoader(importlib.abc.Loader): + def create_module(self, spec): + mod.__name__ = 'distutils' + return mod + + def exec_module(self, module): + pass + + return importlib.util.spec_from_loader( + 'distutils', DistutilsLoader(), origin=mod.__file__ + ) + + @staticmethod + def is_cpython(): + """ + Suppress supplying distutils for CPython (build and tests). + Ref #2965 and #3007. + """ + return os.path.isfile('pybuilddir.txt') + + def spec_for_pip(self): + """ + Ensure stdlib distutils when running under pip. + See pypa/pip#8761 for rationale. + """ + if sys.version_info >= (3, 12) or self.pip_imported_during_build(): + return + clear_distutils() + self.spec_for_distutils = lambda: None + + @classmethod + def pip_imported_during_build(cls): + """ + Detect if pip is being imported in a build script. Ref #2355. + """ + import traceback + + return any( + cls.frame_file_is_setup(frame) for frame, line in traceback.walk_stack(None) + ) + + @staticmethod + def frame_file_is_setup(frame): + """ + Return True if the indicated frame suggests a setup.py file. + """ + # some frames may not have __file__ (#2940) + return frame.f_globals.get('__file__', '').endswith('setup.py') + + def spec_for_sensitive_tests(self): + """ + Ensure stdlib distutils when running select tests under CPython. + + python/cpython#91169 + """ + clear_distutils() + self.spec_for_distutils = lambda: None + + sensitive_tests = ( + [ + 'test.test_distutils', + 'test.test_peg_generator', + 'test.test_importlib', + ] + if sys.version_info < (3, 10) + else [ + 'test.test_distutils', + ] + ) + + +for name in DistutilsMetaFinder.sensitive_tests: + setattr( + DistutilsMetaFinder, + f'spec_for_{name}', + DistutilsMetaFinder.spec_for_sensitive_tests, + ) + + +DISTUTILS_FINDER = DistutilsMetaFinder() + + +def add_shim(): + DISTUTILS_FINDER in sys.meta_path or insert_shim() + + +class shim: + def __enter__(self) -> None: + insert_shim() + + def __exit__(self, exc: object, value: object, tb: object) -> None: + _remove_shim() + + +def insert_shim(): + sys.meta_path.insert(0, DISTUTILS_FINDER) + + +def _remove_shim(): + try: + sys.meta_path.remove(DISTUTILS_FINDER) + except ValueError: + pass + + +if sys.version_info < (3, 12): + # DistutilsMetaFinder can only be disabled in Python < 3.12 (PEP 632) + remove_shim = _remove_shim diff --git a/webapp/backend/venv/lib/python3.12/site-packages/_distutils_hack/__pycache__/__init__.cpython-312.pyc b/webapp/backend/venv/lib/python3.12/site-packages/_distutils_hack/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..1dcaf10 Binary files /dev/null and b/webapp/backend/venv/lib/python3.12/site-packages/_distutils_hack/__pycache__/__init__.cpython-312.pyc differ diff --git a/webapp/backend/venv/lib/python3.12/site-packages/_distutils_hack/__pycache__/override.cpython-312.pyc b/webapp/backend/venv/lib/python3.12/site-packages/_distutils_hack/__pycache__/override.cpython-312.pyc new file mode 100644 index 0000000..f668e6c Binary files /dev/null and b/webapp/backend/venv/lib/python3.12/site-packages/_distutils_hack/__pycache__/override.cpython-312.pyc differ diff --git a/webapp/backend/venv/lib/python3.12/site-packages/_distutils_hack/override.py b/webapp/backend/venv/lib/python3.12/site-packages/_distutils_hack/override.py new file mode 100644 index 0000000..2cc433a --- /dev/null +++ b/webapp/backend/venv/lib/python3.12/site-packages/_distutils_hack/override.py @@ -0,0 +1 @@ +__import__('_distutils_hack').do_override() diff --git a/webapp/backend/venv/lib/python3.12/site-packages/_yaml/__init__.py b/webapp/backend/venv/lib/python3.12/site-packages/_yaml/__init__.py new file mode 100644 index 0000000..7baa8c4 --- /dev/null +++ b/webapp/backend/venv/lib/python3.12/site-packages/_yaml/__init__.py @@ -0,0 +1,33 @@ +# This is a stub package designed to roughly emulate the _yaml +# extension module, which previously existed as a standalone module +# and has been moved into the `yaml` package namespace. +# It does not perfectly mimic its old counterpart, but should get +# close enough for anyone who's relying on it even when they shouldn't. +import yaml + +# in some circumstances, the yaml module we imoprted may be from a different version, so we need +# to tread carefully when poking at it here (it may not have the attributes we expect) +if not getattr(yaml, '__with_libyaml__', False): + from sys import version_info + + exc = ModuleNotFoundError if version_info >= (3, 6) else ImportError + raise exc("No module named '_yaml'") +else: + from yaml._yaml import * + import warnings + warnings.warn( + 'The _yaml extension module is now located at yaml._yaml' + ' and its location is subject to change. To use the' + ' LibYAML-based parser and emitter, import from `yaml`:' + ' `from yaml import CLoader as Loader, CDumper as Dumper`.', + DeprecationWarning + ) + del warnings + # Don't `del yaml` here because yaml is actually an existing + # namespace member of _yaml. + +__name__ = '_yaml' +# If the module is top-level (i.e. not a part of any specific package) +# then the attribute should be set to ''. +# https://docs.python.org/3.8/library/types.html +__package__ = '' diff --git a/webapp/backend/venv/lib/python3.12/site-packages/_yaml/__pycache__/__init__.cpython-312.pyc b/webapp/backend/venv/lib/python3.12/site-packages/_yaml/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..3b16225 Binary files /dev/null and b/webapp/backend/venv/lib/python3.12/site-packages/_yaml/__pycache__/__init__.cpython-312.pyc differ diff --git a/webapp/backend/venv/lib/python3.12/site-packages/blinker-1.9.0.dist-info/INSTALLER b/webapp/backend/venv/lib/python3.12/site-packages/blinker-1.9.0.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/webapp/backend/venv/lib/python3.12/site-packages/blinker-1.9.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/webapp/backend/venv/lib/python3.12/site-packages/blinker-1.9.0.dist-info/LICENSE.txt b/webapp/backend/venv/lib/python3.12/site-packages/blinker-1.9.0.dist-info/LICENSE.txt new file mode 100644 index 0000000..79c9825 --- /dev/null +++ b/webapp/backend/venv/lib/python3.12/site-packages/blinker-1.9.0.dist-info/LICENSE.txt @@ -0,0 +1,20 @@ +Copyright 2010 Jason Kirtland + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/webapp/backend/venv/lib/python3.12/site-packages/blinker-1.9.0.dist-info/METADATA b/webapp/backend/venv/lib/python3.12/site-packages/blinker-1.9.0.dist-info/METADATA new file mode 100644 index 0000000..6d343f5 --- /dev/null +++ b/webapp/backend/venv/lib/python3.12/site-packages/blinker-1.9.0.dist-info/METADATA @@ -0,0 +1,60 @@ +Metadata-Version: 2.3 +Name: blinker +Version: 1.9.0 +Summary: Fast, simple object-to-object and broadcast signaling +Author: Jason Kirtland +Maintainer-email: Pallets Ecosystem +Requires-Python: >=3.9 +Description-Content-Type: text/markdown +Classifier: Development Status :: 5 - Production/Stable +Classifier: License :: OSI Approved :: MIT License +Classifier: Programming Language :: Python +Classifier: Typing :: Typed +Project-URL: Chat, https://discord.gg/pallets +Project-URL: Documentation, https://blinker.readthedocs.io +Project-URL: Source, https://github.com/pallets-eco/blinker/ + +# Blinker + +Blinker provides a fast dispatching system that allows any number of +interested parties to subscribe to events, or "signals". + + +## Pallets Community Ecosystem + +> [!IMPORTANT]\ +> This project is part of the Pallets Community Ecosystem. Pallets is the open +> source organization that maintains Flask; Pallets-Eco enables community +> maintenance of related projects. If you are interested in helping maintain +> this project, please reach out on [the Pallets Discord server][discord]. +> +> [discord]: https://discord.gg/pallets + + +## Example + +Signal receivers can subscribe to specific senders or receive signals +sent by any sender. + +```pycon +>>> from blinker import signal +>>> started = signal('round-started') +>>> def each(round): +... print(f"Round {round}") +... +>>> started.connect(each) + +>>> def round_two(round): +... print("This is round two.") +... +>>> started.connect(round_two, sender=2) + +>>> for round in range(1, 4): +... started.send(round) +... +Round 1! +Round 2! +This is round two. +Round 3! +``` + diff --git a/webapp/backend/venv/lib/python3.12/site-packages/blinker-1.9.0.dist-info/RECORD b/webapp/backend/venv/lib/python3.12/site-packages/blinker-1.9.0.dist-info/RECORD new file mode 100644 index 0000000..2eba51c --- /dev/null +++ b/webapp/backend/venv/lib/python3.12/site-packages/blinker-1.9.0.dist-info/RECORD @@ -0,0 +1,13 @@ +blinker-1.9.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +blinker-1.9.0.dist-info/LICENSE.txt,sha256=nrc6HzhZekqhcCXSrhvjg5Ykx5XphdTw6Xac4p-spGc,1054 +blinker-1.9.0.dist-info/METADATA,sha256=uIRiM8wjjbHkCtbCyTvctU37IAZk0kEe5kxAld1dvzA,1633 +blinker-1.9.0.dist-info/RECORD,, +blinker-1.9.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +blinker-1.9.0.dist-info/WHEEL,sha256=CpUCUxeHQbRN5UGRQHYRJorO5Af-Qy_fHMctcQ8DSGI,82 +blinker/__init__.py,sha256=I2EdZqpy4LyjX17Hn1yzJGWCjeLaVaPzsMgHkLfj_cQ,317 +blinker/__pycache__/__init__.cpython-312.pyc,, +blinker/__pycache__/_utilities.cpython-312.pyc,, +blinker/__pycache__/base.cpython-312.pyc,, +blinker/_utilities.py,sha256=0J7eeXXTUx0Ivf8asfpx0ycVkp0Eqfqnj117x2mYX9E,1675 +blinker/base.py,sha256=QpDuvXXcwJF49lUBcH5BiST46Rz9wSG7VW_p7N_027M,19132 +blinker/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 diff --git a/webapp/backend/venv/lib/python3.12/site-packages/blinker-1.9.0.dist-info/REQUESTED b/webapp/backend/venv/lib/python3.12/site-packages/blinker-1.9.0.dist-info/REQUESTED new file mode 100644 index 0000000..e69de29 diff --git a/webapp/backend/venv/lib/python3.12/site-packages/blinker-1.9.0.dist-info/WHEEL b/webapp/backend/venv/lib/python3.12/site-packages/blinker-1.9.0.dist-info/WHEEL new file mode 100644 index 0000000..e3c6fee --- /dev/null +++ b/webapp/backend/venv/lib/python3.12/site-packages/blinker-1.9.0.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: flit 3.10.1 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/webapp/backend/venv/lib/python3.12/site-packages/blinker/__init__.py b/webapp/backend/venv/lib/python3.12/site-packages/blinker/__init__.py new file mode 100644 index 0000000..1772fa4 --- /dev/null +++ b/webapp/backend/venv/lib/python3.12/site-packages/blinker/__init__.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from .base import ANY +from .base import default_namespace +from .base import NamedSignal +from .base import Namespace +from .base import Signal +from .base import signal + +__all__ = [ + "ANY", + "default_namespace", + "NamedSignal", + "Namespace", + "Signal", + "signal", +] diff --git a/webapp/backend/venv/lib/python3.12/site-packages/blinker/__pycache__/__init__.cpython-312.pyc b/webapp/backend/venv/lib/python3.12/site-packages/blinker/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..9687535 Binary files /dev/null and b/webapp/backend/venv/lib/python3.12/site-packages/blinker/__pycache__/__init__.cpython-312.pyc differ diff --git a/webapp/backend/venv/lib/python3.12/site-packages/blinker/__pycache__/_utilities.cpython-312.pyc b/webapp/backend/venv/lib/python3.12/site-packages/blinker/__pycache__/_utilities.cpython-312.pyc new file mode 100644 index 0000000..3cec396 Binary files /dev/null and b/webapp/backend/venv/lib/python3.12/site-packages/blinker/__pycache__/_utilities.cpython-312.pyc differ diff --git a/webapp/backend/venv/lib/python3.12/site-packages/blinker/__pycache__/base.cpython-312.pyc b/webapp/backend/venv/lib/python3.12/site-packages/blinker/__pycache__/base.cpython-312.pyc new file mode 100644 index 0000000..b811f84 Binary files /dev/null and b/webapp/backend/venv/lib/python3.12/site-packages/blinker/__pycache__/base.cpython-312.pyc differ diff --git a/webapp/backend/venv/lib/python3.12/site-packages/blinker/_utilities.py b/webapp/backend/venv/lib/python3.12/site-packages/blinker/_utilities.py new file mode 100644 index 0000000..000c902 --- /dev/null +++ b/webapp/backend/venv/lib/python3.12/site-packages/blinker/_utilities.py @@ -0,0 +1,64 @@ +from __future__ import annotations + +import collections.abc as c +import inspect +import typing as t +from weakref import ref +from weakref import WeakMethod + +T = t.TypeVar("T") + + +class Symbol: + """A constant symbol, nicer than ``object()``. Repeated calls return the + same instance. + + >>> Symbol('foo') is Symbol('foo') + True + >>> Symbol('foo') + foo + """ + + symbols: t.ClassVar[dict[str, Symbol]] = {} + + def __new__(cls, name: str) -> Symbol: + if name in cls.symbols: + return cls.symbols[name] + + obj = super().__new__(cls) + cls.symbols[name] = obj + return obj + + def __init__(self, name: str) -> None: + self.name = name + + def __repr__(self) -> str: + return self.name + + def __getnewargs__(self) -> tuple[t.Any, ...]: + return (self.name,) + + +def make_id(obj: object) -> c.Hashable: + """Get a stable identifier for a receiver or sender, to be used as a dict + key or in a set. + """ + if inspect.ismethod(obj): + # The id of a bound method is not stable, but the id of the unbound + # function and instance are. + return id(obj.__func__), id(obj.__self__) + + if isinstance(obj, (str, int)): + # Instances with the same value always compare equal and have the same + # hash, even if the id may change. + return obj + + # Assume other types are not hashable but will always be the same instance. + return id(obj) + + +def make_ref(obj: T, callback: c.Callable[[ref[T]], None] | None = None) -> ref[T]: + if inspect.ismethod(obj): + return WeakMethod(obj, callback) # type: ignore[arg-type, return-value] + + return ref(obj, callback) diff --git a/webapp/backend/venv/lib/python3.12/site-packages/blinker/base.py b/webapp/backend/venv/lib/python3.12/site-packages/blinker/base.py new file mode 100644 index 0000000..d051b94 --- /dev/null +++ b/webapp/backend/venv/lib/python3.12/site-packages/blinker/base.py @@ -0,0 +1,512 @@ +from __future__ import annotations + +import collections.abc as c +import sys +import typing as t +import weakref +from collections import defaultdict +from contextlib import contextmanager +from functools import cached_property +from inspect import iscoroutinefunction + +from ._utilities import make_id +from ._utilities import make_ref +from ._utilities import Symbol + +F = t.TypeVar("F", bound=c.Callable[..., t.Any]) + +ANY = Symbol("ANY") +"""Symbol for "any sender".""" + +ANY_ID = 0 + + +class Signal: + """A notification emitter. + + :param doc: The docstring for the signal. + """ + + ANY = ANY + """An alias for the :data:`~blinker.ANY` sender symbol.""" + + set_class: type[set[t.Any]] = set + """The set class to use for tracking connected receivers and senders. + Python's ``set`` is unordered. If receivers must be dispatched in the order + they were connected, an ordered set implementation can be used. + + .. versionadded:: 1.7 + """ + + @cached_property + def receiver_connected(self) -> Signal: + """Emitted at the end of each :meth:`connect` call. + + The signal sender is the signal instance, and the :meth:`connect` + arguments are passed through: ``receiver``, ``sender``, and ``weak``. + + .. versionadded:: 1.2 + """ + return Signal(doc="Emitted after a receiver connects.") + + @cached_property + def receiver_disconnected(self) -> Signal: + """Emitted at the end of each :meth:`disconnect` call. + + The sender is the signal instance, and the :meth:`disconnect` arguments + are passed through: ``receiver`` and ``sender``. + + This signal is emitted **only** when :meth:`disconnect` is called + explicitly. This signal cannot be emitted by an automatic disconnect + when a weakly referenced receiver or sender goes out of scope, as the + instance is no longer be available to be used as the sender for this + signal. + + An alternative approach is available by subscribing to + :attr:`receiver_connected` and setting up a custom weakref cleanup + callback on weak receivers and senders. + + .. versionadded:: 1.2 + """ + return Signal(doc="Emitted after a receiver disconnects.") + + def __init__(self, doc: str | None = None) -> None: + if doc: + self.__doc__ = doc + + self.receivers: dict[ + t.Any, weakref.ref[c.Callable[..., t.Any]] | c.Callable[..., t.Any] + ] = {} + """The map of connected receivers. Useful to quickly check if any + receivers are connected to the signal: ``if s.receivers:``. The + structure and data is not part of the public API, but checking its + boolean value is. + """ + + self.is_muted: bool = False + self._by_receiver: dict[t.Any, set[t.Any]] = defaultdict(self.set_class) + self._by_sender: dict[t.Any, set[t.Any]] = defaultdict(self.set_class) + self._weak_senders: dict[t.Any, weakref.ref[t.Any]] = {} + + def connect(self, receiver: F, sender: t.Any = ANY, weak: bool = True) -> F: + """Connect ``receiver`` to be called when the signal is sent by + ``sender``. + + :param receiver: The callable to call when :meth:`send` is called with + the given ``sender``, passing ``sender`` as a positional argument + along with any extra keyword arguments. + :param sender: Any object or :data:`ANY`. ``receiver`` will only be + called when :meth:`send` is called with this sender. If ``ANY``, the + receiver will be called for any sender. A receiver may be connected + to multiple senders by calling :meth:`connect` multiple times. + :param weak: Track the receiver with a :mod:`weakref`. The receiver will + be automatically disconnected when it is garbage collected. When + connecting a receiver defined within a function, set to ``False``, + otherwise it will be disconnected when the function scope ends. + """ + receiver_id = make_id(receiver) + sender_id = ANY_ID if sender is ANY else make_id(sender) + + if weak: + self.receivers[receiver_id] = make_ref( + receiver, self._make_cleanup_receiver(receiver_id) + ) + else: + self.receivers[receiver_id] = receiver + + self._by_sender[sender_id].add(receiver_id) + self._by_receiver[receiver_id].add(sender_id) + + if sender is not ANY and sender_id not in self._weak_senders: + # store a cleanup for weakref-able senders + try: + self._weak_senders[sender_id] = make_ref( + sender, self._make_cleanup_sender(sender_id) + ) + except TypeError: + pass + + if "receiver_connected" in self.__dict__ and self.receiver_connected.receivers: + try: + self.receiver_connected.send( + self, receiver=receiver, sender=sender, weak=weak + ) + except TypeError: + # TODO no explanation or test for this + self.disconnect(receiver, sender) + raise + + return receiver + + def connect_via(self, sender: t.Any, weak: bool = False) -> c.Callable[[F], F]: + """Connect the decorated function to be called when the signal is sent + by ``sender``. + + The decorated function will be called when :meth:`send` is called with + the given ``sender``, passing ``sender`` as a positional argument along + with any extra keyword arguments. + + :param sender: Any object or :data:`ANY`. ``receiver`` will only be + called when :meth:`send` is called with this sender. If ``ANY``, the + receiver will be called for any sender. A receiver may be connected + to multiple senders by calling :meth:`connect` multiple times. + :param weak: Track the receiver with a :mod:`weakref`. The receiver will + be automatically disconnected when it is garbage collected. When + connecting a receiver defined within a function, set to ``False``, + otherwise it will be disconnected when the function scope ends.= + + .. versionadded:: 1.1 + """ + + def decorator(fn: F) -> F: + self.connect(fn, sender, weak) + return fn + + return decorator + + @contextmanager + def connected_to( + self, receiver: c.Callable[..., t.Any], sender: t.Any = ANY + ) -> c.Generator[None, None, None]: + """A context manager that temporarily connects ``receiver`` to the + signal while a ``with`` block executes. When the block exits, the + receiver is disconnected. Useful for tests. + + :param receiver: The callable to call when :meth:`send` is called with + the given ``sender``, passing ``sender`` as a positional argument + along with any extra keyword arguments. + :param sender: Any object or :data:`ANY`. ``receiver`` will only be + called when :meth:`send` is called with this sender. If ``ANY``, the + receiver will be called for any sender. + + .. versionadded:: 1.1 + """ + self.connect(receiver, sender=sender, weak=False) + + try: + yield None + finally: + self.disconnect(receiver) + + @contextmanager + def muted(self) -> c.Generator[None, None, None]: + """A context manager that temporarily disables the signal. No receivers + will be called if the signal is sent, until the ``with`` block exits. + Useful for tests. + """ + self.is_muted = True + + try: + yield None + finally: + self.is_muted = False + + def send( + self, + sender: t.Any | None = None, + /, + *, + _async_wrapper: c.Callable[ + [c.Callable[..., c.Coroutine[t.Any, t.Any, t.Any]]], c.Callable[..., t.Any] + ] + | None = None, + **kwargs: t.Any, + ) -> list[tuple[c.Callable[..., t.Any], t.Any]]: + """Call all receivers that are connected to the given ``sender`` + or :data:`ANY`. Each receiver is called with ``sender`` as a positional + argument along with any extra keyword arguments. Return a list of + ``(receiver, return value)`` tuples. + + The order receivers are called is undefined, but can be influenced by + setting :attr:`set_class`. + + If a receiver raises an exception, that exception will propagate up. + This makes debugging straightforward, with an assumption that correctly + implemented receivers will not raise. + + :param sender: Call receivers connected to this sender, in addition to + those connected to :data:`ANY`. + :param _async_wrapper: Will be called on any receivers that are async + coroutines to turn them into sync callables. For example, could run + the receiver with an event loop. + :param kwargs: Extra keyword arguments to pass to each receiver. + + .. versionchanged:: 1.7 + Added the ``_async_wrapper`` argument. + """ + if self.is_muted: + return [] + + results = [] + + for receiver in self.receivers_for(sender): + if iscoroutinefunction(receiver): + if _async_wrapper is None: + raise RuntimeError("Cannot send to a coroutine function.") + + result = _async_wrapper(receiver)(sender, **kwargs) + else: + result = receiver(sender, **kwargs) + + results.append((receiver, result)) + + return results + + async def send_async( + self, + sender: t.Any | None = None, + /, + *, + _sync_wrapper: c.Callable[ + [c.Callable[..., t.Any]], c.Callable[..., c.Coroutine[t.Any, t.Any, t.Any]] + ] + | None = None, + **kwargs: t.Any, + ) -> list[tuple[c.Callable[..., t.Any], t.Any]]: + """Await all receivers that are connected to the given ``sender`` + or :data:`ANY`. Each receiver is called with ``sender`` as a positional + argument along with any extra keyword arguments. Return a list of + ``(receiver, return value)`` tuples. + + The order receivers are called is undefined, but can be influenced by + setting :attr:`set_class`. + + If a receiver raises an exception, that exception will propagate up. + This makes debugging straightforward, with an assumption that correctly + implemented receivers will not raise. + + :param sender: Call receivers connected to this sender, in addition to + those connected to :data:`ANY`. + :param _sync_wrapper: Will be called on any receivers that are sync + callables to turn them into async coroutines. For example, + could call the receiver in a thread. + :param kwargs: Extra keyword arguments to pass to each receiver. + + .. versionadded:: 1.7 + """ + if self.is_muted: + return [] + + results = [] + + for receiver in self.receivers_for(sender): + if not iscoroutinefunction(receiver): + if _sync_wrapper is None: + raise RuntimeError("Cannot send to a non-coroutine function.") + + result = await _sync_wrapper(receiver)(sender, **kwargs) + else: + result = await receiver(sender, **kwargs) + + results.append((receiver, result)) + + return results + + def has_receivers_for(self, sender: t.Any) -> bool: + """Check if there is at least one receiver that will be called with the + given ``sender``. A receiver connected to :data:`ANY` will always be + called, regardless of sender. Does not check if weakly referenced + receivers are still live. See :meth:`receivers_for` for a stronger + search. + + :param sender: Check for receivers connected to this sender, in addition + to those connected to :data:`ANY`. + """ + if not self.receivers: + return False + + if self._by_sender[ANY_ID]: + return True + + if sender is ANY: + return False + + return make_id(sender) in self._by_sender + + def receivers_for( + self, sender: t.Any + ) -> c.Generator[c.Callable[..., t.Any], None, None]: + """Yield each receiver to be called for ``sender``, in addition to those + to be called for :data:`ANY`. Weakly referenced receivers that are not + live will be disconnected and skipped. + + :param sender: Yield receivers connected to this sender, in addition + to those connected to :data:`ANY`. + """ + # TODO: test receivers_for(ANY) + if not self.receivers: + return + + sender_id = make_id(sender) + + if sender_id in self._by_sender: + ids = self._by_sender[ANY_ID] | self._by_sender[sender_id] + else: + ids = self._by_sender[ANY_ID].copy() + + for receiver_id in ids: + receiver = self.receivers.get(receiver_id) + + if receiver is None: + continue + + if isinstance(receiver, weakref.ref): + strong = receiver() + + if strong is None: + self._disconnect(receiver_id, ANY_ID) + continue + + yield strong + else: + yield receiver + + def disconnect(self, receiver: c.Callable[..., t.Any], sender: t.Any = ANY) -> None: + """Disconnect ``receiver`` from being called when the signal is sent by + ``sender``. + + :param receiver: A connected receiver callable. + :param sender: Disconnect from only this sender. By default, disconnect + from all senders. + """ + sender_id: c.Hashable + + if sender is ANY: + sender_id = ANY_ID + else: + sender_id = make_id(sender) + + receiver_id = make_id(receiver) + self._disconnect(receiver_id, sender_id) + + if ( + "receiver_disconnected" in self.__dict__ + and self.receiver_disconnected.receivers + ): + self.receiver_disconnected.send(self, receiver=receiver, sender=sender) + + def _disconnect(self, receiver_id: c.Hashable, sender_id: c.Hashable) -> None: + if sender_id == ANY_ID: + if self._by_receiver.pop(receiver_id, None) is not None: + for bucket in self._by_sender.values(): + bucket.discard(receiver_id) + + self.receivers.pop(receiver_id, None) + else: + self._by_sender[sender_id].discard(receiver_id) + self._by_receiver[receiver_id].discard(sender_id) + + def _make_cleanup_receiver( + self, receiver_id: c.Hashable + ) -> c.Callable[[weakref.ref[c.Callable[..., t.Any]]], None]: + """Create a callback function to disconnect a weakly referenced + receiver when it is garbage collected. + """ + + def cleanup(ref: weakref.ref[c.Callable[..., t.Any]]) -> None: + # If the interpreter is shutting down, disconnecting can result in a + # weird ignored exception. Don't call it in that case. + if not sys.is_finalizing(): + self._disconnect(receiver_id, ANY_ID) + + return cleanup + + def _make_cleanup_sender( + self, sender_id: c.Hashable + ) -> c.Callable[[weakref.ref[t.Any]], None]: + """Create a callback function to disconnect all receivers for a weakly + referenced sender when it is garbage collected. + """ + assert sender_id != ANY_ID + + def cleanup(ref: weakref.ref[t.Any]) -> None: + self._weak_senders.pop(sender_id, None) + + for receiver_id in self._by_sender.pop(sender_id, ()): + self._by_receiver[receiver_id].discard(sender_id) + + return cleanup + + def _cleanup_bookkeeping(self) -> None: + """Prune unused sender/receiver bookkeeping. Not threadsafe. + + Connecting & disconnecting leaves behind a small amount of bookkeeping + data. Typical workloads using Blinker, for example in most web apps, + Flask, CLI scripts, etc., are not adversely affected by this + bookkeeping. + + With a long-running process performing dynamic signal routing with high + volume, e.g. connecting to function closures, senders are all unique + object instances. Doing all of this over and over may cause memory usage + to grow due to extraneous bookkeeping. (An empty ``set`` for each stale + sender/receiver pair.) + + This method will prune that bookkeeping away, with the caveat that such + pruning is not threadsafe. The risk is that cleanup of a fully + disconnected receiver/sender pair occurs while another thread is + connecting that same pair. If you are in the highly dynamic, unique + receiver/sender situation that has lead you to this method, that failure + mode is perhaps not a big deal for you. + """ + for mapping in (self._by_sender, self._by_receiver): + for ident, bucket in list(mapping.items()): + if not bucket: + mapping.pop(ident, None) + + def _clear_state(self) -> None: + """Disconnect all receivers and senders. Useful for tests.""" + self._weak_senders.clear() + self.receivers.clear() + self._by_sender.clear() + self._by_receiver.clear() + + +class NamedSignal(Signal): + """A named generic notification emitter. The name is not used by the signal + itself, but matches the key in the :class:`Namespace` that it belongs to. + + :param name: The name of the signal within the namespace. + :param doc: The docstring for the signal. + """ + + def __init__(self, name: str, doc: str | None = None) -> None: + super().__init__(doc) + + #: The name of this signal. + self.name: str = name + + def __repr__(self) -> str: + base = super().__repr__() + return f"{base[:-1]}; {self.name!r}>" # noqa: E702 + + +class Namespace(dict[str, NamedSignal]): + """A dict mapping names to signals.""" + + def signal(self, name: str, doc: str | None = None) -> NamedSignal: + """Return the :class:`NamedSignal` for the given ``name``, creating it + if required. Repeated calls with the same name return the same signal. + + :param name: The name of the signal. + :param doc: The docstring of the signal. + """ + if name not in self: + self[name] = NamedSignal(name, doc) + + return self[name] + + +class _PNamespaceSignal(t.Protocol): + def __call__(self, name: str, doc: str | None = None) -> NamedSignal: ... + + +default_namespace: Namespace = Namespace() +"""A default :class:`Namespace` for creating named signals. :func:`signal` +creates a :class:`NamedSignal` in this namespace. +""" + +signal: _PNamespaceSignal = default_namespace.signal +"""Return a :class:`NamedSignal` in :data:`default_namespace` with the given +``name``, creating it if required. Repeated calls with the same name return the +same signal. +""" diff --git a/webapp/backend/venv/lib/python3.12/site-packages/blinker/py.typed b/webapp/backend/venv/lib/python3.12/site-packages/blinker/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/webapp/backend/venv/lib/python3.12/site-packages/build-1.4.0.dist-info/INSTALLER b/webapp/backend/venv/lib/python3.12/site-packages/build-1.4.0.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/webapp/backend/venv/lib/python3.12/site-packages/build-1.4.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/webapp/backend/venv/lib/python3.12/site-packages/build-1.4.0.dist-info/METADATA b/webapp/backend/venv/lib/python3.12/site-packages/build-1.4.0.dist-info/METADATA new file mode 100644 index 0000000..f6fc24c --- /dev/null +++ b/webapp/backend/venv/lib/python3.12/site-packages/build-1.4.0.dist-info/METADATA @@ -0,0 +1,139 @@ +Metadata-Version: 2.4 +Name: build +Version: 1.4.0 +Summary: A simple, correct Python build frontend +Author-email: Filipe Laíns , Bernát Gábor , layday , Henry Schreiner +Requires-Python: >= 3.9 +Description-Content-Type: text/markdown +License-Expression: MIT +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +License-File: LICENSE +Requires-Dist: packaging >= 24.0 +Requires-Dist: pyproject_hooks +Requires-Dist: colorama; os_name == "nt" +Requires-Dist: importlib-metadata >= 4.6; python_full_version < "3.10.2" +Requires-Dist: tomli >= 1.1.0; python_version < "3.11" +Requires-Dist: uv >= 0.1.18 ; extra == "uv" +Requires-Dist: virtualenv >= 20.11 ; extra == "virtualenv" and ( python_version < '3.10') +Requires-Dist: virtualenv >= 20.17 ; extra == "virtualenv" and ( python_version >= '3.10' and python_version < '3.14') +Requires-Dist: virtualenv >= 20.31 ; extra == "virtualenv" and ( python_version >= '3.14') +Project-URL: changelog, https://build.pypa.io/en/stable/changelog.html +Project-URL: homepage, https://build.pypa.io +Project-URL: issues, https://github.com/pypa/build/issues +Project-URL: source, https://github.com/pypa/build +Provides-Extra: uv +Provides-Extra: virtualenv + +# build + +[![pre-commit.ci status](https://results.pre-commit.ci/badge/github/pypa/build/main.svg)](https://results.pre-commit.ci/latest/github/pypa/build/main) +[![CI test](https://github.com/pypa/build/actions/workflows/test.yml/badge.svg)](https://github.com/pypa/build/actions/workflows/test.yml) +[![codecov](https://codecov.io/gh/pypa/build/branch/main/graph/badge.svg)](https://codecov.io/gh/pypa/build) + +[![Documentation Status](https://readthedocs.org/projects/pypa-build/badge/?version=latest)](https://build.pypa.io/en/latest/?badge=latest) +[![PyPI version](https://badge.fury.io/py/build.svg)](https://pypi.org/project/build/) +[![Discord](https://img.shields.io/discord/803025117553754132?label=Discord%20chat%20%23build)](https://discord.gg/pypa) + +A simple, correct Python build frontend. + +See the [documentation](https://build.pypa.io) for more information. + +### Installation + +`build` can be installed via `pip` or an equivalent via: + +```console +$ pip install build +``` + +### Usage + +```console +$ python -m build +``` + +This will build the package in an isolated environment, generating a +source-distribution and wheel in the directory `dist/`. See the +[documentation](https://build.pypa.io) for full information. Build is also +available via the command line as `pyproject-build` once installed. + +### Common arguments + +- `--sdist` (`-s`): Produce just an SDist +- `--wheel` (`-w`): Produce just a wheel +- `--metadata`: Produce just the metadata as JSON. Cannot be used with `--sdist`/`--wheel`. +- `-C