Compare commits

..

66 Commits

Author SHA1 Message Date
d.viti
b394806552 Refactor ingress annotations to include dns-scope conditionally
All checks were successful
Helm Chart Build / lint-only (push) Has been skipped
Helm Chart Build / build-helm (push) Successful in 8s
Build and Deploy / build-api (push) Successful in 44s
Build and Deploy / build-web (push) Successful in 1m6s
2025-10-09 21:09:40 +02:00
d.viti
5710df19d9 Fix web Dockerfile to copy templates directory
All checks were successful
Helm Chart Build / lint-only (push) Has been skipped
Helm Chart Build / build-helm (push) Successful in 8s
Build and Deploy / build-api (push) Successful in 45s
Build and Deploy / build-web (push) Successful in 1m2s
Fixed TemplateNotFound error by adding missing templates directory
to the Docker image.

Error:
  jinja2.exceptions.TemplateNotFound: index.html

Cause:
  Dockerfile was not copying the templates/ directory into the container

Fix:
  Added 'COPY templates/ ./templates/' to Dockerfile

The web application requires templates/ directory for Jinja2 templates:
- templates/base.html
- templates/index.html
- templates/items.html
- templates/users.html
- templates/llm.html

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-09 17:40:01 +02:00
d.viti
7a4c9eaacc Fix API Dockerfile to use requirements.txt and correct port
All checks were successful
Helm Chart Build / lint-only (push) Has been skipped
Helm Chart Build / build-helm (push) Successful in 8s
Build and Deploy / build-api (push) Successful in 39s
Build and Deploy / build-web (push) Successful in 59s
Fixed Dockerfile to properly install all dependencies from requirements.txt
instead of hardcoding them, and corrected the port from 8001 to 8080.

Issues Fixed:
1. ModuleNotFoundError: No module named 'httpx'
   - Dockerfile was hardcoding pip install
   - httpx was missing from hardcoded dependencies

2. Port mismatch
   - Dockerfile was exposing port 8001
   - Application uses port 8080

Changes:
- Copy requirements.txt before installing
- Use pip install -r requirements.txt
- Changed EXPOSE from 8001 to 8080
- Changed CMD port from 8001 to 8080

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-09 17:29:47 +02:00
d.viti
6c4e40400a Move all documentation to web/docs for MkDocs integration
All checks were successful
Helm Chart Build / lint-only (push) Has been skipped
Helm Chart Build / build-helm (push) Successful in 9s
Build and Deploy / build-api (push) Successful in 42s
Build and Deploy / build-web (push) Successful in 1m5s
Moved all documentation files into web/docs/ directory for proper
MkDocs integration and updated navigation structure.

Changes:
========

1. Documentation Structure:
   - Moved SWAGGER-WEB-UPDATE.md → web/docs/swagger-documentation.md
   - Moved ARGOCD-VALUES-UPDATE.md → web/docs/argocd-values-guide.md
   - Formatted both files with MkDocs frontmatter and structure

2. MkDocs Navigation (web/mkdocs.yml):
   - Updated site_url to https://commandware.it/docs
   - Added new section "API Documentation" with Swagger guide
   - Added "ArgoCD Values Guide" to Configuration section
   - Enhanced markdown extensions:
     * Added mermaid diagram support
     * Added task lists
     * Added emoji support
     * Added code annotations
   - Added navigation features (instant, tabs, sections)
   - Added tags plugin
   - Improved copyright notice

3. Swagger Documentation (web/docs/swagger-documentation.md):
   - Complete guide to Swagger UI, ReDoc, and OpenAPI
   - Access instructions via web interface and direct links
   - API overview with architecture diagram
   - Detailed endpoint documentation for all groups:
     * Root (/)
     * Health (/health, /ready)
     * Items CRUD (/items/*)
     * Users (/users/*)
     * LLM (/llm/*)
   - Interactive testing examples
   - OpenAPI specification download instructions
   - Tools integration (Postman, Insomnia, OpenAPI Generator)
   - Cross-references to other documentation pages

4. ArgoCD Values Guide (web/docs/argocd-values-guide.md):
   - Comprehensive guide for updating values.yaml in ArgoCD
   - All modifications explained (before/after)
   - Step-by-step application instructions
   - Verification commands
   - Security best practices

New MkDocs Navigation:
======================
- Home
- Getting Started
- Architecture
  - Overview
  - Kubernetes Resources
- Configuration
  - API7 Gateway
  - Ingress & Routing
  - Service Discovery
  - Secret Management
  - ArgoCD Values Guide ← NEW
  - CI/CD Pipeline
- API Documentation ← NEW SECTION
  - Swagger & OpenAPI
- Troubleshooting

All documentation is now centralized in web/docs/ and accessible
through MkDocs at https://commandware.it/docs

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-09 17:24:53 +02:00
d.viti
146f657bea Enhance Swagger documentation and web UI navigation
Some checks failed
Helm Chart Build / lint-only (push) Has been skipped
Build and Deploy / build-web (push) Failing after 7s
Helm Chart Build / build-helm (push) Successful in 8s
Build and Deploy / build-api (push) Successful in 42s
Enhanced API Swagger documentation and improved web interface navigation
with dropdown menus and better organization.

API Changes (api/main.py):
==========================
- Enhanced FastAPI app description with architecture diagram
- Added detailed rate limiting information
- Added server configurations (production + local)
- Added contact and license information
- Enhanced all endpoint descriptions with:
  * Detailed parameter descriptions
  * Response descriptions
  * Error responses
  * Rate limit information
  * Usage examples
- Added Field descriptions to all Pydantic models
- Added schema examples for better Swagger UI
- Enhanced LLM endpoints with AI rate limiting details
- Added status codes (201, 404, 429, 500) to endpoints
- Improved startup message with docs URLs

Swagger UI Improvements:
- Better organized endpoint groups (Root, Health, Items, Users, LLM)
- Detailed request/response schemas
- Interactive examples for all endpoints
- Rate limiting documentation
- Architecture overview in description

Web Changes (web/templates/base.html):
======================================
- Added dropdown menu for API documentation with:
  * API Root (/)
  * Swagger UI (/docs)
  * ReDoc (/redoc)
  * OpenAPI JSON (/openapi.json)
- Added emoji icons to all menu items for better UX
- Added tooltips (title attributes) to all links
- Renamed "API Config" to "Settings" for clarity
- Added CSS for dropdown menu functionality
- Improved footer text
- Better visual hierarchy with icons

Navigation Menu:
- 🏠 Home
- 📦 Items
- 👥 Users
- 🤖 LLM Chat
- 📚 API Docs (dropdown with 4 options)
- ⚙️ Settings

All endpoints now have comprehensive documentation visible in Swagger UI
at https://commandware.it/api/docs

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-09 17:20:34 +02:00
d.viti
a0dee1d499 Add proxy-rewrite plugin to remove /api prefix
Some checks failed
Helm Chart Build / lint-only (push) Has been skipped
Build and Deploy / build-web (push) Failing after 7s
Helm Chart Build / build-helm (push) Successful in 12s
Build and Deploy / build-api (push) Successful in 42s
Added proxy-rewrite plugin to strip /api prefix from requests before
forwarding to backend API microservice. The API service is a standalone
microservice that expects requests without the /api prefix.

Changes:
- Added proxy-rewrite plugin to both API routes:
  * /api/llm/* route (priority 20)
  * /api/* route (priority 10)
- Uses regex_uri to rewrite: /api/endpoint -> /endpoint

Example transformations:
- /api/health -> /health
- /api/users/123 -> /users/123
- /api/llm/chat -> /llm/chat

Plugin configuration:
  proxy-rewrite:
    regex_uri:
      - "^/api/(.*)"
      - "/$1"

This allows the API microservice to work independently without
needing to handle the /api prefix in its routes.

Reference: https://docs.api7.ai/hub/proxy-rewrite/

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-09 17:05:24 +02:00
d.viti
b8a483db71 -
Some checks failed
Helm Chart Build / lint-only (push) Has been skipped
Build and Deploy / build-web (push) Failing after 7s
Helm Chart Build / build-helm (push) Successful in 8s
Build and Deploy / build-api (push) Successful in 42s
2025-10-09 16:49:24 +02:00
d.viti
530fb3d906 Disable service discovery in chart default values
All checks were successful
Helm Chart Build / lint-only (push) Has been skipped
Helm Chart Build / build-helm (push) Successful in 10s
Build and Deploy / build-api (push) Successful in 53s
Build and Deploy / build-web (push) Successful in 1m2s
Service discovery requires Service Registry to be configured in
API7 Enterprise Dashboard first. Until the registry is configured,
ADC sync fails with: 'service registry not found'

Disabling service discovery to use static upstream nodes instead.

To re-enable:
1. Configure Service Registry in API7 Dashboard
2. Set api7.serviceDiscovery.enabled: true
3. Redeploy

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-09 16:48:13 +02:00
d.viti
84718e5039 Fix ADC service discovery configuration syntax
All checks were successful
Helm Chart Build / lint-only (push) Has been skipped
Helm Chart Build / build-helm (push) Successful in 9s
Build and Deploy / build-api (push) Successful in 47s
Build and Deploy / build-web (push) Successful in 1m10s
Fixed service discovery configuration to use correct ADC syntax.
The namespace should be included in the service_name, not as a
separate namespace_id field.

Error:
✖ Unrecognized key: "namespace_id"
  → at services[0].upstream

Fix:
- Changed from: service_name: my-service + namespace_id: namespace
- Changed to: service_name: namespace/my-service

This matches the ADC/API7 expected format for Kubernetes service
discovery: "namespace/service-name"

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-09 16:36:16 +02:00
d.viti
3e6798a3e5 Add root-level Secret template and update .gitignore
All checks were successful
Helm Chart Build / lint-only (push) Has been skipped
Helm Chart Build / build-helm (push) Successful in 12s
Build and Deploy / build-api (push) Successful in 48s
Build and Deploy / build-web (push) Successful in 1m11s
Added Secret template for API7 Gateway credentials management
and improved .gitignore to prevent credential leaks.

Changes:

1. Secret Template (api7-credentials.yaml.template):
   - Template for creating API7 Gateway admin credentials Secret
   - Clear instructions for getting admin key from cluster
   - Examples for both stringData and base64 encoded data
   - Must be copied and filled in, then applied to cluster

2. .gitignore Updates:
   - Added api7-credentials.yaml to ignore actual secrets
   - Added wildcard *-credentials.yaml for any credential files
   - Excluded templates (!*-credentials.yaml.template)
   - Improved comments for clarity

3. README.md:
   - Comprehensive quick start guide
   - Features overview
   - Installation steps with Secret creation
   - Documentation links
   - Basic troubleshooting

Security:
- Prevents committing actual credentials
- Clear separation between templates and actual secrets
- Instructions for secure credential management

Users should:
1. Copy api7-credentials.yaml.template
2. Fill in actual credentials
3. Apply to cluster
4. Never commit filled secrets to git

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-09 16:06:33 +02:00
d.viti
c5b597c7c1 Move documentation to MkDocs and add comprehensive guides
All checks were successful
Helm Chart Build / lint-only (push) Has been skipped
Helm Chart Build / build-helm (push) Successful in 9s
Build and Deploy / build-api (push) Successful in 44s
Build and Deploy / build-web (push) Successful in 1m10s
Reorganized documentation to be part of MkDocs site with three new
comprehensive guides covering API7 Gateway configuration.

Changes:

1. Documentation Structure:
   - Moved SECRET-MANAGEMENT.md from helm/ to web/docs/
   - Created service-discovery.md with complete guide
   - Created ingress-routing.md with routing architecture
   - Moved externalsecret examples to web/docs/examples/

2. New Documentation - Service Discovery:
   - How service discovery works (architecture diagram)
   - Benefits vs static configuration
   - Configuration examples
   - RBAC requirements
   - Advanced use cases (auto-scaling, rolling updates)
   - Load balancing algorithms
   - Monitoring and troubleshooting
   - Best practices

3. New Documentation - Ingress & Routing:
   - Complete traffic flow architecture
   - Ingress configuration explained
   - Gateway routing rules and priority
   - URI matching patterns (prefix, exact, regex)
   - TLS/SSL with cert-manager
   - Advanced routing scenarios:
     * Multiple domains
     * Path-based routing
     * Header-based routing
     * Method-based routing
   - Configuration examples (microservices, WebSocket, canary)
   - Monitoring and debugging
   - Troubleshooting common issues

4. MkDocs Navigation:
   - Updated mkdocs.yml with new pages in Configuration section
   - Added: Ingress & Routing
   - Added: Service Discovery
   - Added: Secret Management

5. Examples Directory:
   - Created web/docs/examples/ for configuration examples
   - Moved ExternalSecret examples with multiple providers:
     * AWS Secrets Manager
     * HashiCorp Vault
     * Azure Key Vault
     * GCP Secret Manager

All documentation now integrated into MkDocs site with proper
navigation, cross-references, and Material theme styling.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-09 16:00:29 +02:00
d.viti
694709ae9a Add support for existing Secrets and External Secrets Operator
Some checks failed
Helm Chart Build / lint-only (push) Has been skipped
Helm Chart Build / build-helm (push) Failing after 9s
Build and Deploy / build-api (push) Successful in 51s
Build and Deploy / build-web (push) Successful in 1m3s
Enhanced secret management for API7 Gateway credentials with support
for existing Secrets and External Secrets Operator integration.

Changes:

1. Secret Configuration:
   - Added api7.gateway.existingSecret parameter for using existing secrets
   - Added api7.gateway.existingSecretKeys for custom key names
   - Modified secret-api7.yaml to only create secret if existingSecret is empty
   - Updated job-adc-sync.yaml to reference configurable secret name

2. Values.yaml Documentation:
   - Added comprehensive documentation for secret configuration options
   - Documented two approaches: inline config (dev) vs existing secret (prod)
   - Added example kubectl command for creating secrets manually
   - Included instructions for obtaining admin key from API7 EE

3. External Secrets Support:
   - Created externalsecret-api7.yaml.example with complete examples
   - Included examples for AWS Secrets Manager and HashiCorp Vault
   - Documented SecretStore configuration patterns

4. Documentation:
   - Created SECRET-MANAGEMENT.md comprehensive guide
   - Covered all secret management options (inline, manual, external)
   - Added security best practices and troubleshooting guide
   - Included examples for External Secrets Operator setup

Benefits:
- Improved security: Secrets not stored in values.yaml
- Flexibility: Support for any secret management tool
- Production-ready: Works with External Secrets Operator
- Better practices: Clear separation of config vs secrets

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-09 15:53:38 +02:00
d.viti
f5a4071b71 Improve values.yaml documentation and configuration structure
All checks were successful
Helm Chart Build / lint-only (push) Has been skipped
Helm Chart Build / build-helm (push) Successful in 8s
Build and Deploy / build-api (push) Successful in 43s
Build and Deploy / build-web (push) Successful in 1m4s
Enhanced values.yaml with comprehensive documentation and better organization:

Documentation improvements:
- Added detailed inline comments for all API7 Gateway configuration sections
- Documented Ingress routing behavior (gateway vs direct service routing)
- Explained Service Discovery benefits and requirements
- Added detailed plugin configuration documentation (rate limiting, CORS, auth)
- Included usage examples and production recommendations

Configuration enhancements:
- Added gateway.gatewayNamespace for better organization
- Added TLS certificate configuration options (duration, renewBefore, algorithm, size)
- Added ADC resource limits configuration
- Improved CORS and rate limiting documentation with parameter explanations
- Added consumer/authentication documentation

Template updates:
- Updated certificate.yaml to use configurable TLS parameters
- Updated job-adc-sync.yaml to use configurable ADC resources

The values.yaml now serves as comprehensive documentation for all
API7 Gateway features and configuration options, making it easier
for users to understand and customize their deployment.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-09 15:29:32 +02:00
d.viti
e995482bfd Enable Kubernetes Service Discovery for API7 Gateway upstreams
All checks were successful
Helm Chart Build / lint-only (push) Has been skipped
Helm Chart Build / build-helm (push) Successful in 9s
Build and Deploy / build-api (push) Successful in 43s
Build and Deploy / build-web (push) Successful in 1m1s
Configured API7 Gateway to use Kubernetes Service Discovery instead of
static upstream nodes. This enables dynamic discovery of backend Pods
through the Kubernetes API.

Benefits:
- Automatic scaling: New Pods are automatically added to upstream pool
- Health checks: Only healthy Pods receive traffic
- Zero downtime: Automatic updates during deployments and rollouts
- No manual upstream configuration needed

Changes:
- Updated configmap-adc.yaml to use discovery_type: kubernetes
- Service discovery queries Kubernetes API for Pod endpoints
- Falls back to static nodes if serviceDiscovery.enabled is false
- Added documentation in values.yaml explaining the feature

The RBAC permissions (services, endpoints watch) were already configured
in rbac-adc.yaml, so no additional permissions are needed.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-09 15:24:29 +02:00
d.viti
f04862f6f7 Configure Ingress to route traffic through API7 Gateway
All checks were successful
Helm Chart Build / lint-only (push) Has been skipped
Helm Chart Build / build-helm (push) Successful in 8s
Build and Deploy / build-api (push) Successful in 46s
Build and Deploy / build-web (push) Successful in 1m7s
Modified Ingress configuration to route all traffic through the API7
Gateway (gateway-0-1759393614-gateway) instead of directly to application
services. This enables API7's advanced routing, rate limiting, CORS, and
other gateway features.

Changes:
- Updated ingress.yaml template to support gateway backend routing
- Modified values.yaml to route traffic to API7 Gateway service
- Disabled web and api services (now optional) as routing is handled by API7
- Removed nginx.ingress.kubernetes.io/rewrite-target annotation
- Maintained backward compatibility with legacy service-based routing

The Ingress now directs traffic to the API7 Gateway which handles all
routing logic defined in the ADC configuration.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-09 15:18:51 +02:00
d.viti
305e0cc848 Update demo URLs to use commandware.it instead of api7-demo.commandware.it
All checks were successful
Helm Chart Build / lint-only (push) Has been skipped
Helm Chart Build / build-helm (push) Successful in 9s
Build and Deploy / build-api (push) Successful in 48s
Build and Deploy / build-web (push) Successful in 1m5s
2025-10-09 12:25:50 +02:00
d.viti
29ef0c65e5 Fix global_rules section to be conditionally included
All checks were successful
Helm Chart Build / lint-only (push) Has been skipped
Helm Chart Build / build-helm (push) Successful in 8s
Build and Deploy / build-api (push) Successful in 40s
Build and Deploy / build-web (push) Successful in 1m6s
Made the global_rules section conditional on logging being enabled.
Previously, when logging was disabled, global_rules was rendered as
an empty object (null), causing a lint error:
"Invalid input: expected record, received null at global_rules"

Now the entire global_rules section is only included when there are
actual rules to add.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-08 19:36:10 +02:00
d.viti
10b3c2a480 Remove prometheus global rule causing plugin not found error
All checks were successful
Helm Chart Build / lint-only (push) Has been skipped
Helm Chart Build / build-helm (push) Successful in 1m33s
Build and Deploy / build-web (push) Successful in 3m9s
Build and Deploy / build-api (push) Successful in 3m12s
Removed the prometheus-metrics global rule configuration which was
causing "custom plugin (prometheus-metrics) not found" error.

API7 Enterprise doesn't support prometheus as a global rule plugin
in this configuration format. Prometheus metrics can be configured
differently if needed.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-08 19:22:44 +02:00
d.viti
8be1f85718 Fix CORS plugin configuration for API7 Enterprise
All checks were successful
Helm Chart Build / lint-only (push) Has been skipped
Helm Chart Build / build-helm (push) Successful in 2m22s
Build and Deploy / build-api (push) Successful in 4m6s
Build and Deploy / build-web (push) Successful in 4m33s
Changed CORS plugin array values to comma-separated strings:
- allow_origins: from JSON array to comma-separated string
- allow_methods: from JSON array to comma-separated string
- allow_headers: from JSON array to comma-separated string
- expose_headers: from JSON array to comma-separated string

API7 Enterprise expects string values with comma-separated items,
not JSON arrays. This fixes the validation error:
"Invalid type. Expected: string, given: array"

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-08 18:58:09 +02:00
d.viti
80ffa5e4fd Fix API7 Enterprise admin URL to use HTTPS on port 7443
All checks were successful
Helm Chart Build / lint-only (push) Has been skipped
Helm Chart Build / build-helm (push) Successful in 19s
Build and Deploy / build-api (push) Successful in 1m2s
Build and Deploy / build-web (push) Successful in 1m29s
Changed adminUrl to use HTTPS instead of HTTP:
- Before: http://api7ee3-0-1759339083-dashboard:7080
- After: https://api7ee3-0-1759339083-dashboard:7443

Also enabled tlsSkipVerify for the dashboard's self-signed certificate.

Testing revealed that:
- Port 7080 HTTP doesn't work (pod listens on 7081 localhost only)
- Port 7443 HTTPS is the correct admin API endpoint
- Self-signed certificate requires TLS verification skip

This fixes the ADC sync job 404 errors.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-08 18:48:20 +02:00
d.viti
aeafd8c035 Add TLS skip verify support to ADC sync job
All checks were successful
Helm Chart Build / lint-only (push) Has been skipped
Helm Chart Build / build-helm (push) Successful in 26s
Build and Deploy / build-api (push) Successful in 1m8s
Build and Deploy / build-web (push) Successful in 1m31s
Added conditional --tls-skip-verify flag to ADC sync job arguments.
This flag is controlled by .Values.api7.adc.tlsSkipVerify and allows
ADC to connect to API7 Enterprise dashboard with self-signed certificates.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-08 18:40:46 +02:00
d.viti
a82f9d81d1 Simplify API7 Enterprise admin URL to use short service name
All checks were successful
Helm Chart Build / lint-only (push) Has been skipped
Helm Chart Build / build-helm (push) Successful in 9s
Build and Deploy / build-api (push) Successful in 46s
Build and Deploy / build-web (push) Successful in 1m3s
Changed adminUrl from fully qualified domain name to short service name:
- Before: http://api7ee3-0-1759339083-dashboard.api7ee.svc.cluster.local:7080
- After: http://api7ee3-0-1759339083-dashboard:7080

The short name works within the same namespace and is simpler to manage.
This fixes the ADC sync job which was failing with 404 errors.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-08 18:36:48 +02:00
d.viti
5ca3123ecf -
All checks were successful
Helm Chart Build / lint-only (push) Has been skipped
Helm Chart Build / build-helm (push) Successful in 16s
Build and Deploy / build-api (push) Successful in 1m1s
Build and Deploy / build-web (push) Successful in 1m20s
2025-10-08 18:00:48 +02:00
d.viti
6f8e327210 Fix API7 Enterprise admin URL to use dashboard service
Some checks failed
Helm Chart Build / lint-only (push) Has been skipped
Helm Chart Build / build-helm (push) Successful in 16s
Build and Deploy / build-api (push) Has been cancelled
Build and Deploy / build-web (push) Has been cancelled
Changed adminUrl from dp-manager:7900 to dashboard:7080 for API7
Enterprise backend. The dp-manager service is for APISIX, while
API7EE requires the dashboard service which exposes the admin API.

This fixes the 404 errors when ADC tries to fetch configuration:
- GET /api/version
- GET /api/gateway_groups
- GET /api/schema/core

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-08 18:00:28 +02:00
d.viti
05b013d378 Fix ADC global_rules format from array to object
All checks were successful
Helm Chart Build / lint-only (push) Has been skipped
Helm Chart Build / build-helm (push) Successful in 9s
Build and Deploy / build-api (push) Successful in 49s
Build and Deploy / build-web (push) Successful in 1m3s
Changed global_rules from array format (- id: name) to object format
(name:) to match ADC schema requirements. This fixes the lint error:
"Invalid input: expected record, received array at global_rules"

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-08 17:51:45 +02:00
d.viti
104f7a21ff Simplify adc-sync job to use ADC container and args
All checks were successful
Helm Chart Build / lint-only (push) Has been skipped
Helm Chart Build / build-helm (push) Successful in 16s
Build and Deploy / build-api (push) Successful in 59s
Build and Deploy / build-web (push) Successful in 1m24s
2025-10-08 16:05:27 +02:00
d.viti
34ed68cb04 Update ADC install script to export PATH and improve logging
All checks were successful
Helm Chart Build / lint-only (push) Has been skipped
Helm Chart Build / build-helm (push) Successful in 46s
Build and Deploy / build-api (push) Successful in 1m45s
Build and Deploy / build-web (push) Successful in 2m5s
2025-10-08 15:59:20 +02:00
d.viti
2255a469a4 Update ADC installation to use specific version and verify output
Some checks failed
Helm Chart Build / lint-only (push) Has been skipped
Build and Deploy / build-api (push) Failing after 17m43s
Helm Chart Build / build-helm (push) Failing after 17m51s
Build and Deploy / build-web (push) Successful in 19m23s
2025-10-08 15:24:47 +02:00
d.viti
27118b21f8 Use debian:bookworm-slim image for adc-sync job
All checks were successful
Helm Chart Build / lint-only (push) Has been skipped
Helm Chart Build / build-helm (push) Successful in 54s
Build and Deploy / build-api (push) Successful in 45s
Build and Deploy / build-web (push) Successful in 1m10s
Update dependency installation logic to support Debian, Alpine, and
RHEL/CentOS distributions. Add error handling for unsupported Linux
distributions.
2025-10-08 15:09:20 +02:00
d.viti
741117dab8 Refactor ADC install and usage in job-adc-sync script
All checks were successful
Helm Chart Build / lint-only (push) Has been skipped
Helm Chart Build / build-helm (push) Successful in 1m10s
Build and Deploy / build-api (push) Successful in 2m20s
Build and Deploy / build-web (push) Successful in 2m36s
Simplify dependency installation and ADC binary setup. Replace ADC
command lookup logic with direct usage after installation.
2025-10-08 13:55:58 +02:00
d.viti
90c6f6fe62 Make ADC command path detection more robust in job script
Some checks failed
Helm Chart Build / lint-only (push) Has been skipped
Build and Deploy / build-web (push) Failing after 1m15s
Helm Chart Build / build-helm (push) Successful in 1m24s
Build and Deploy / build-api (push) Successful in 2m2s
2025-10-08 13:52:37 +02:00
d.viti
b2e0d5bd10 Refactor Helm chart for API7EE: clarify gateway requirements, improve
All checks were successful
Helm Chart Build / lint-only (push) Has been skipped
Helm Chart Build / build-helm (push) Successful in 31s
Build and Deploy / build-api (push) Successful in 1m14s
Build and Deploy / build-web (push) Successful in 1m36s
ADC config templating, and enhance gateway health checks
2025-10-08 13:44:01 +02:00
d.viti
c0832ff59b Add private key config to certificate template and values
All checks were successful
Helm Chart Build / lint-only (push) Has been skipped
Helm Chart Build / build-helm (push) Successful in 8s
Build and Deploy / build-api (push) Successful in 50s
Build and Deploy / build-web (push) Successful in 1m4s
2025-10-08 13:19:57 +02:00
d.viti
0935010f89 Add enabled flags for health probes in web and api deployments
Some checks failed
Helm Chart Build / lint-only (push) Has been skipped
Build and Deploy / build-web (push) Failing after 7s
Helm Chart Build / build-helm (push) Successful in 8s
Build and Deploy / build-api (push) Successful in 42s
2025-10-08 13:04:15 +02:00
d.viti
7ca15fe1a5 Add readiness endpoint and clear adminKey in Helm values
All checks were successful
Helm Chart Build / lint-only (push) Has been skipped
Helm Chart Build / build-helm (push) Successful in 8s
Build and Deploy / build-api (push) Successful in 43s
Build and Deploy / build-web (push) Successful in 1m1s
2025-10-08 12:41:15 +02:00
d.viti
1b31601543 Rewrite and expand all documentation for API7EE demo platform
All checks were successful
Helm Chart Build / lint-only (push) Has been skipped
Helm Chart Build / build-helm (push) Successful in 8s
Build and Deploy / build-api (push) Successful in 46s
Build and Deploy / build-web (push) Successful in 1m8s
2025-10-07 19:09:40 +02:00
d.viti
118f2c051c Add MkDocs and related packages to requirements
All checks were successful
Helm Chart Build / lint-only (push) Has been skipped
Helm Chart Build / build-helm (push) Successful in 8s
Build and Deploy / build-api (push) Successful in 45s
Build and Deploy / build-web (push) Successful in 1m3s
2025-10-07 18:53:02 +02:00
d.viti
d17e356fcd Simplify Docker build workflow and update routing rules
Some checks failed
Helm Chart Build / lint-only (push) Has been skipped
Helm Chart Build / build-helm (push) Successful in 8s
Build and Deploy / build-web (push) Failing after 45s
Build and Deploy / build-api (push) Successful in 53s
2025-10-07 18:31:56 +02:00
d.viti
2d695ba361 Update ADC config to handle /docs route separately
Some checks failed
Helm Chart Build / lint-only (push) Has been skipped
Build and Deploy / build-web (push) Failing after 8s
Helm Chart Build / build-helm (push) Successful in 8s
Build and Deploy / build-api (push) Successful in 25s
Add a new route for /docs and /docs/* with higher priority. Exclude
/docs from the default route's URI regex to prevent overlap.
2025-10-07 18:15:30 +02:00
d.viti
ed660dce5a Add LLM endpoints, web frontend, and rate limiting config
Some checks failed
Helm Chart Build / lint-only (push) Has been skipped
Helm Chart Build / build-helm (push) Successful in 9s
Build and Deploy / build-api (push) Successful in 33s
Build and Deploy / build-web (push) Failing after 41s
- Added OpenAI-compatible LLM endpoints to API backend - Introduced web
frontend with Jinja2 templates and static assets - Implemented API proxy
routes in web service - Added sample db.json data for items, users,
orders, reviews, categories, llm_requests - Updated ADC and Helm configs
for separate AI and standard rate limiting - Upgraded FastAPI, Uvicorn,
and added httpx, Jinja2, python-multipart dependencies - Added API
configuration modal and client-side JS for web app
2025-10-07 17:29:12 +02:00
d.viti
78baa5ad21 Merge branch 'main' of ssh://git.commandware.com:2222/demos/api7-demo
All checks were successful
Helm Chart Build / lint-only (push) Has been skipped
Helm Chart Build / build-helm (push) Successful in 8s
Build and Deploy / build-web (push) Successful in 25s
Build and Deploy / build-api (push) Successful in 24s
2025-10-07 15:02:32 +02:00
d.viti
e156b7c7a1 Refactor ADC config to use AI rate limiting for /api route 2025-10-07 15:01:22 +02:00
b926845dbf Update .gitea/workflows/build.yml
All checks were successful
Helm Chart Build / lint-only (push) Has been skipped
Helm Chart Build / build-helm (push) Successful in 9s
Build and Deploy / build-web (push) Successful in 29s
Build and Deploy / build-api (push) Successful in 27s
2025-10-06 19:21:51 +02:00
a9bcc71a72 Update .gitea/workflows/build.yml
Some checks failed
Helm Chart Build / lint-only (push) Has been skipped
Build and Deploy / build-web (push) Failing after 10s
Helm Chart Build / build-helm (push) Successful in 10s
Build and Deploy / build-api (push) Failing after 27s
2025-10-06 19:17:11 +02:00
741295150d Update .gitea/workflows/helm-build.yml
Some checks failed
Helm Chart Build / lint-only (push) Has been skipped
Helm Chart Build / build-helm (push) Successful in 10s
Build and Deploy / build-web (push) Failing after 24s
Build and Deploy / build-api (push) Failing after 26s
2025-10-06 16:36:23 +02:00
cc9a4fa8ab Update .gitea/workflows/build.yml
Some checks failed
Helm Chart Build / lint-only (push) Has been skipped
Helm Chart Build / build-helm (push) Failing after 9s
Build and Deploy / build-web (push) Failing after 23s
Build and Deploy / build-api (push) Failing after 25s
2025-10-06 16:35:29 +02:00
d.viti
8f5e4f2776 Add https:// protocol prefix to PACKAGES_REGISTRY URLs
All checks were successful
Helm Chart Build / lint-only (push) Has been skipped
Helm Chart Build / build-helm (push) Successful in 8s
Build and Deploy / build-web (push) Successful in 34s
Build and Deploy / build-api (push) Successful in 33s
- vars.PACKAGES_REGISTRY contains only the domain (e.g., git.commandware.com)
- Added https:// prefix to all registry URLs
- Fixed curl command to properly construct the API endpoint URL
2025-10-03 02:47:58 +02:00
d.viti
daecf80731 Fix Helm chart push URL using vars.PACKAGES_REGISTRY
- Use vars.PACKAGES_REGISTRY with fallback to gitea.server_url
- Consistent with build.yml pattern for registry URLs
- Fixed curl command URL construction issue
- Applied same pattern to all registry references in the workflow
2025-10-03 02:46:36 +02:00
d.viti
11d116bdd1 Simplify Helm workflow to build on main branch only
Some checks failed
Helm Chart Build / lint-only (push) Has been skipped
Helm Chart Build / build-helm (push) Failing after 9s
Build and Deploy / build-web (push) Successful in 37s
Build and Deploy / build-api (push) Successful in 38s
- Removed tag-based triggers and release creation
- Renamed workflow from helm-release.yml to helm-build.yml
- Simplified to always build and publish on main branch push
- Version always comes from Chart.yaml (no modifications)
- Removed all release-related logic and conditions
- Kept PR linting for validation
- Cleaner and simpler workflow focused on continuous delivery
- Updated README to reflect the new workflow structure
2025-10-03 02:42:25 +02:00
d.viti
20c9d2eaf4 Simplify Helm versioning to always use Chart.yaml version
Some checks failed
Helm Chart Build and Release / lint-only (push) Has been skipped
Helm Chart Build and Release / build-and-release-helm (push) Failing after 11s
Build and Deploy / build-web (push) Successful in 37s
Build and Deploy / build-api (push) Successful in 36s
- Removed automatic version generation from tags or timestamps
- Always use version directly from Chart.yaml (single source of truth)
- Tag triggers now only determine if it's a release (not version)
- Release is triggered when tag matches Chart.yaml version
- Manual dispatch version is now for validation only
- Removed version modification during packaging
- Simplified packaging process to use Chart's own version
- Removed --devel flag as all versions are now stable
- Better separation: version managed in Chart.yaml, release triggered by tags
2025-10-03 02:37:50 +02:00
d.viti
a379b26808 Consolidate all Helm build and release tasks into single workflow
- Removed build-helm job from build.yml (Docker builds only now)
- Created comprehensive helm-release.yml that handles:
  - Development builds on main branch pushes (with dev suffix)
  - Release builds on version tags (v*.*.*)
  - Manual releases via workflow_dispatch
  - PR linting without publishing
- Added intelligent version detection:
  - Development versions get timestamp suffix
  - Release versions use clean semver
  - Manual dispatch can specify custom version
- Improved release process with proper Gitea API integration
- Added conditional release creation only for tagged versions
- Updated README to document the new workflow structure
- Separated concerns: build.yml for Docker, helm-release.yml for Helm
2025-10-03 02:35:16 +02:00
d.viti
f9d529ac87 Rename Helm chart to api7ee-demo-k8s
- Changed chart name from api7ee to api7ee-demo-k8s in Chart.yaml
- Renamed helm/api7ee directory to helm/api7ee-demo-k8s
- Updated all references in build.yml workflow
- Updated all references in helm-release.yml workflow
- Updated main README.md with new chart name
- Updated Helm chart README with new chart name
- Verified all old references have been replaced
- Chart packages correctly as api7ee-demo-k8s-{version}.tgz
2025-10-03 02:31:23 +02:00
d.viti
fb396ac71a Fix Helm chart build workflow issues
- Removed unnecessary helm dependency update (no dependencies defined)
- Modified packaging to use a temporary copy of the chart
- This prevents sed modifications from affecting the lint step
- Ensures Chart.yaml version field remains intact
- Added debug output for chart version verification
2025-10-03 02:27:26 +02:00
d.viti
99fd37bfd8 Use gitea.repository variable for dynamic image repository paths
Some checks failed
Helm Chart Release / release-helm (push) Failing after 9s
Build and Deploy / build-web (push) Successful in 35s
Build and Deploy / build-api (push) Successful in 34s
Build and Deploy / build-helm (push) Failing after 7s
- Updated values.yaml to use gitea.repository placeholder
- Modified build.yml workflow to replace repository path dynamically
- Modified helm-release.yml workflow to replace repository path
- This allows the Helm chart to work with any Gitea repository structure
- Image paths are now built as: registry/gitea.repository/component
2025-10-03 02:23:32 +02:00
d.viti
cf2b786738 Update Helm values with correct API7 cluster configuration
- Updated API7 gateway admin URL to use actual DP Manager service
- Changed hosts to api7-demo.commandware.it (matching wildcard ingress)
- Updated cert-manager issuer to cloudflare-acme-prod (existing in cluster)
- Added gatewayService reference to actual gateway service name
- Configured development values with API7 disabled for local testing
- Enhanced production values with proper domains and security settings
- Added support for multiple production domains
- Configured proper rate limits and auth for production
2025-10-03 02:21:33 +02:00
d.viti
e9528217f8 Refactor Helm workflows to trigger on main branch and streamline versioning process
Some checks failed
Helm Chart Release / release-helm (push) Failing after 29s
Build and Deploy / build-web (push) Successful in 38s
Build and Deploy / build-api (push) Successful in 48s
Build and Deploy / build-helm (push) Failing after 7s
2025-10-03 02:09:06 +02:00
d.viti
733826890e Fix Gitea workflow to use Gitea-specific variables and APIs
- Replaced GitHub-specific variables with Gitea equivalents
- Changed GITHUB_REF to GITEA_REF_NAME for tag extraction
- Replaced github.sha with gitea.sha for commit reference
- Removed GitHub release action, using Gitea API directly
- Fixed environment variable references to use proper syntax
- Removed GITHUB_STEP_SUMMARY as it's GitHub-specific
- Updated all VERSION references to use environment variable
2025-10-03 02:06:54 +02:00
d.viti
fd832e9b42 Add API7 ADC integration to Helm chart for automatic gateway configuration
- Added ADC (API7 Declarative CLI) post-install job for automatic gateway setup
- Created ConfigMap with complete API7 routing and service configuration
- Integrated cert-manager for automatic TLS certificate management
- Added support for Kubernetes service discovery
- Implemented auto-publish feature for routes after deployment
- Added comprehensive API7 plugin configurations (rate limiting, CORS, auth)
- Created RBAC resources for ADC job to access cluster resources
- Secured admin credentials using Kubernetes secrets
- Updated values.yaml with extensive API7 configuration options
- Enhanced documentation with API7 setup and troubleshooting guides
2025-10-03 02:04:35 +02:00
d.viti
d818ee6600 Add Helm chart build and publishing to Gitea workflows
- Added Helm chart build job to main CI/CD workflow
- Created dedicated helm-release workflow for version tags
- Integrated Helm packaging with Gitea package registry
- Added automatic chart versioning and publishing
- Updated README with Helm deployment instructions
- Configured chart linting and validation steps
- Added release automation for tagged versions
2025-10-03 01:56:36 +02:00
d.viti
ceee0dcff8 Fix domain references in Helm chart
- Updated Helm repository URL to use git.commandware.com
- Fixed production values to use git.commandware.com as image registry
- Corrected domain references to match actual infrastructure
2025-10-03 01:54:09 +02:00
d.viti
ef93f4a35f Add Helm chart for deploying web and API components
All checks were successful
Build and Deploy / build-web (push) Successful in 36s
Build and Deploy / build-api (push) Successful in 38s
- Created complete Helm chart with deployments, services, and ingress
- Added support for both web frontend and API backend components
- Included autoscaling (HPA) for both components
- Added pod disruption budgets for high availability
- Configured security contexts and best practices
- Created helper templates and configuration management
- Added production and development value files
- Included comprehensive README with installation instructions
2025-10-03 01:51:17 +02:00
d.viti
073d652869 Fix mkdocs build error by moving mkdocs.yml to web root
All checks were successful
Build and Deploy / build-api (push) Successful in 36s
Build and Deploy / build-web (push) Successful in 1m19s
- Moved mkdocs.yml from web/docs/ to web/ directory
- Updated Dockerfile to copy mkdocs.yml and build from correct location
- Set docs_dir to 'docs' in mkdocs.yml configuration
- Fixes Docker build error where mkdocs couldn't find documentation files
2025-10-03 01:44:41 +02:00
d.viti
12ea09a520 fix: update registry references to use PACKAGES_REGISTRY in build workflow
Some checks failed
Build and Deploy / build-api (push) Successful in 58s
Build and Deploy / build-web (push) Failing after 1m3s
2025-10-03 01:36:09 +02:00
d.viti
99447b874b refactor: update build workflow to use dynamic registry and improve login steps
Some checks failed
Build and Deploy / build-web (push) Failing after 23s
Build and Deploy / build-api (push) Failing after 22s
2025-10-03 01:32:12 +02:00
d.viti
62135bbd12 update Gitea login action to use PACKAGES_PUSH_TOKEN
Some checks failed
Build and Push Docker Images / build-web (push) Failing after 18s
Build and Push Docker Images / build-api (push) Failing after 21s
2025-10-03 01:26:50 +02:00
d.viti
ee5e7c698e fixed gitea toklen name 2025-10-03 01:22:48 +02:00
54 changed files with 8129 additions and 800 deletions

View File

@@ -1,7 +1,10 @@
name: Build and Push Docker Images
name: Build and Deploy
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
jobs:
@@ -14,31 +17,20 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Gitea Container Registry
- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: git.commandware.com
username: ${{ gitea.actor }}
password: ${{ secrets.GITEA_TOKEN }}
- name: Extract metadata for web image
id: meta-web
uses: docker/metadata-action@v5
with:
images: git.commandware.com/demos/api7-demo/web
tags: |
type=ref,event=branch
registry: ${{ vars.PACKAGES_REGISTRY || gitea.server_url }}
username: ${{ secrets.USERNAME }}
password: ${{ secrets.TOKEN }}
- name: Build and push web image
uses: docker/build-push-action@v5
with:
context: ./web
file: ./web/Dockerfile
tags: ${{ vars.PACKAGES_REGISTRY || gitea.server_url }}/${{ gitea.repository }}/web:${{ gitea.ref_name }}
push: true
tags: ${{ steps.meta-web.outputs.tags }}
labels: ${{ steps.meta-web.outputs.labels }}
cache-from: type=registry,ref=git.commandware.com/demos/api7-demo/web:buildcache
cache-to: type=registry,ref=git.commandware.com/demos/api7-demo/web:buildcache,mode=max
build-api:
runs-on: ubuntu-latest
@@ -49,28 +41,17 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Gitea Container Registry
- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: git.commandware.com
username: ${{ gitea.actor }}
password: ${{ secrets.GITEA_TOKEN }}
- name: Extract metadata for api image
id: meta-api
uses: docker/metadata-action@v5
with:
images: git.commandware.com/demos/api7-demo/api
tags: |
type=ref,event=branch
registry: ${{ vars.PACKAGES_REGISTRY || gitea.server_url }}
username: ${{ secrets.USERNAME }}
password: ${{ secrets.TOKEN }}
- name: Build and push api image
uses: docker/build-push-action@v5
with:
context: ./api
file: ./api/Dockerfile
tags: ${{ vars.PACKAGES_REGISTRY || gitea.server_url }}/${{ gitea.repository }}/api:${{ gitea.ref_name }}
push: true
tags: ${{ steps.meta-api.outputs.tags }}
labels: ${{ steps.meta-api.outputs.labels }}
cache-from: type=registry,ref=git.commandware.com/demos/api7-demo/api:buildcache
cache-to: type=registry,ref=git.commandware.com/demos/api7-demo/api:buildcache,mode=max

View File

@@ -0,0 +1,101 @@
name: Helm Chart Build
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
jobs:
build-helm:
runs-on: ubuntu-latest
# Only run on main branch pushes (not on PRs)
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetch all history for proper versioning
- name: Install Helm
uses: azure/setup-helm@v3
with:
version: "latest"
- name: Get chart version
run: |
# Get version from Chart.yaml
VERSION=$(grep '^version:' helm/api7ee-demo-k8s/Chart.yaml | awk '{print $2}')
echo "📌 Chart version: ${VERSION}"
echo "VERSION=${VERSION}" >> $GITHUB_ENV
- name: Lint Helm chart
run: |
helm lint helm/api7ee-demo-k8s/
- name: Prepare and package Helm chart
run: |
# Create a temporary copy of the chart for packaging
cp -r helm/api7ee-demo-k8s /tmp/api7ee-demo-k8s-chart
# Update image registry and repository to match Gitea
sed -i "s|registry: gitea.server_url|registry: ${{ vars.PACKAGES_REGISTRY || gitea.server_url }}|g" /tmp/api7ee-demo-k8s-chart/values.yaml
sed -i "s|repository: gitea.repository/|repository: ${{ gitea.repository }}/|g" /tmp/api7ee-demo-k8s-chart/values.yaml
# Package the chart
helm package /tmp/api7ee-demo-k8s-chart
# Store chart filename
echo "CHART_FILE=api7ee-demo-k8s-${VERSION}.tgz" >> $GITHUB_ENV
echo "📦 Packaged chart: api7ee-demo-k8s-${VERSION}.tgz"
- name: Push Helm chart to Gitea Package Registry
run: |
# Upload Helm chart to Gitea package registry
echo "📤 Uploading chart to Gitea Package Registry..."
curl --fail-with-body \
-H "Authorization: token ${{ secrets.TOKEN }}" \
-X POST \
-F "chart=@${CHART_FILE}" \
https://${{ vars.PACKAGES_REGISTRY || gitea.server_url }}/api/packages/${{ gitea.repository_owner }}/helm/api/charts
echo "✅ Helm chart pushed successfully to Gitea Package Registry"
echo "📦 Chart: ${CHART_FILE}"
echo "🔗 Registry URL: https://${{ vars.PACKAGES_REGISTRY || gitea.server_url }}/api/packages/${{ gitea.repository_owner }}/helm"
- name: Summary
run: |
echo "## 📦 Helm Chart Published"
echo ""
echo "- **Version:** ${VERSION}"
echo "- **Chart:** ${CHART_FILE}"
echo "- **Registry:** https://${{ vars.PACKAGES_REGISTRY || gitea.server_url }}/api/packages/${{ gitea.repository_owner }}/helm"
echo ""
echo "### Installation"
echo '```bash'
echo "helm repo add api7ee https://${{ vars.PACKAGES_REGISTRY || gitea.server_url }}/api/packages/${{ gitea.repository_owner }}/helm"
echo "helm repo update"
echo "helm install my-api7ee api7ee/api7ee-demo-k8s --version ${VERSION}"
echo '```'
lint-only:
runs-on: ubuntu-latest
# Only run on PRs
if: github.event_name == 'pull_request'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install Helm
uses: azure/setup-helm@v3
with:
version: "latest"
- name: Lint Helm chart
run: |
echo "🔍 Linting Helm chart for PR..."
helm lint helm/api7ee-demo-k8s/
echo "✅ Helm chart linting passed"

7
.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
# Production values (may contain sensitive data)
/values.yaml
# Secret files with actual credentials
api7-credentials.yaml
*-credentials.yaml
!*-credentials.yaml.template

534
README.md
View File

@@ -1,492 +1,86 @@
# API7 Demo - FastAPI Applications
# API7 Enterprise Demo
Demo project showcasing two FastAPI applications deployed on Kubernetes through API7 Enterprise Gateway with automated CI/CD using Gitea.
Complete production-ready deployment of API7 Enterprise API Gateway on Kubernetes.
## 📁 Project Structure
## Features
```
api7-demo/
├── web/ # Web application
│ ├── main.py # FastAPI web app with HTML UI
│ ├── Dockerfile # Container image
│ └── requirements.txt # Python dependencies
├── api/ # API application
│ ├── main.py # FastAPI REST API with Swagger
│ ├── Dockerfile # Container image
│ └── requirements.txt # Python dependencies
├── .gitea/
│ └── workflows/
│ └── build.yml # CI/CD pipeline
└── README.md
```
-**API7 Enterprise Gateway** with advanced routing and plugins
-**Kubernetes Service Discovery** for dynamic backend discovery
-**Automatic TLS** with cert-manager and Let's Encrypt
-**Rate Limiting** (standard + AI-based for LLM endpoints)
-**CORS** policies for browser-based applications
-**Secure Secret Management** with External Secrets Operator support
-**CI/CD** integration with ArgoCD
-**Complete Documentation** with MkDocs
## 🌐 Applications
### Web Application (`web/`)
Modern web interface with:
- Responsive HTML dashboard
- System information display
- Health check endpoints
**Endpoints:**
- `GET /` - Main webpage
- `GET /health` - Health check
**Port:** 8000
### API Application (`api/`)
REST API with full documentation:
- CRUD operations for Items and Users
- Automatic Swagger/OpenAPI docs
- Data validation with Pydantic
**Key Endpoints:**
- `GET /docs` - Swagger UI
- `GET /items` - List items
- `POST /items` - Create item
- `GET /users` - List users
- `GET /health` - Health check
**Port:** 8001 (internally 8000)
## 🚀 Quick Start
### Local Development
**Web Application:**
```bash
cd web
pip install -r requirements.txt
python main.py
# Access at http://localhost:8000
```
**API Application:**
```bash
cd api
pip install -r requirements.txt
python main.py
# Swagger at http://localhost:8000/docs
```
### Docker
**Build and run Web:**
```bash
docker build -t web-app ./web
docker run -p 8000:8000 web-app
```
**Build and run API:**
```bash
docker build -t api-app ./api
docker run -p 8001:8000 api-app
```
## 🐳 CI/CD with Gitea
### Automated Pipeline
The `.gitea/workflows/build.yml` pipeline automatically:
1. Builds Docker images for both applications
2. Pushes to Gitea container registry
3. Tags images with branch name
4. Implements layer caching for faster builds
**Triggers:**
- Any branch push
- Manual dispatch
**Registry:** `git.commandware.com/demos/api7-demo`
**Images:**
- `git.commandware.com/demos/api7-demo/web:<branch-name>`
- `git.commandware.com/demos/api7-demo/api:<branch-name>`
### Setup
1. **Create `GITEA_TOKEN` secret:**
- Repository Settings → Secrets → Add Secret
- Name: `GITEA_TOKEN`
- Generate token at: User Settings → Applications → Generate Token
- Required scope: `write:package`
2. **Push to trigger build:**
```bash
git push origin main
```
## ☸️ Kubernetes Deployment
## Quick Start
### Prerequisites
- Kubernetes cluster (v1.19+)
- API7 Enterprise Gateway installed
- Namespace: `api7ee`
- Kubernetes cluster (1.24+)
- Helm 3.x
- kubectl configured
- cert-manager installed
- NGINX Ingress Controller
### Deployment Manifest
### Installation
**k8s-deployments.yaml:**
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
namespace: api7ee
spec:
replicas: 2
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- name: web
image: git.commandware.com/demos/api7-demo/web:main
ports:
- containerPort: 8000
name: http
livenessProbe:
httpGet:
path: /health
port: 8000
---
apiVersion: v1
kind: Service
metadata:
name: web-service
namespace: api7ee
spec:
type: ClusterIP
ports:
- port: 80
targetPort: 8000
name: http
selector:
app: web
---
# Similar configuration for api deployment
```
### Deploy
```bash
kubectl apply -f k8s-deployments.yaml
kubectl rollout status deployment/web -n api7ee
kubectl rollout status deployment/api -n api7ee
```
## 🔌 API7 Gateway Configuration
### Route Configuration (ADC)
**api7-routes.yaml:**
```yaml
services:
- name: web-service
upstream:
name: web-upstream
scheme: http
type: roundrobin
discovery_type: kubernetes
service_name: api7ee/web-service:http
routes:
- name: web-route
uris:
- /*
hosts:
- demo.yourdomain.com
plugins:
redirect:
http_to_https: true
- name: api-service
upstream:
name: api-upstream
scheme: http
type: roundrobin
discovery_type: kubernetes
service_name: api7ee/api-service:http
routes:
- name: api-route
priority: 10
uris:
- /api
- /api/*
hosts:
- demo.yourdomain.com
plugins:
redirect:
http_to_https: true
proxy-rewrite:
regex_uri:
- ^/api/(.*)
- /$1
ssls:
- snis:
- "*.yourdomain.com"
certificates:
- certificate: |
-----BEGIN CERTIFICATE-----
<YOUR_CERTIFICATE>
-----END CERTIFICATE-----
key: |
-----BEGIN PRIVATE KEY-----
<YOUR_KEY>
-----END PRIVATE KEY-----
```
### Sync Configuration
```bash
adc sync -f api7-routes.yaml \
--backend api7ee \
--server https://api7-dashboard.yourdomain.com \
--token <YOUR_API7_TOKEN> \
--gateway-group default \
--tls-skip-verify
```
### Publish Routes
Routes must be published via API7 Dashboard to become active:
1. Navigate to **Services** → Select service
2. Go to **Routes** tab
3. Click **"Publish"** for each route
4. Select gateway group: **"default"**
5. Confirm
## 🧪 Testing
### Test Endpoints
```bash
# Web application
curl https://demo.yourdomain.com
# API health check
curl https://demo.yourdomain.com/api/health
# API Swagger (if exposed)
curl https://demo.yourdomain.com/api/docs
# Verify HTTP → HTTPS redirect
curl -I http://demo.yourdomain.com
```
### Verify Service Discovery
```bash
# Scale deployment
kubectl scale deployment web -n api7ee --replicas=5
# API7 automatically discovers new endpoints
kubectl get endpoints -n api7ee web-service
# Scale back
kubectl scale deployment web -n api7ee --replicas=2
```
## 📊 Architecture
```
Internet
LoadBalancer (External IP)
NGINX Ingress Controller
TLS Termination (Let's Encrypt)
API7 Gateway (ClusterIP)
[Kubernetes Service Discovery]
┌──────────────┬──────────────┐
│ Web Service │ API Service │
└──────────────┴──────────────┘
```
### Traffic Routing
- `demo.yourdomain.com/api/*` → API Service (priority 10, strips `/api` prefix)
- `demo.yourdomain.com/*` → Web Service (default priority)
## 🔧 API7 Enterprise Setup Guide
### Certificate Management
**Using cert-manager with Cloudflare:**
```yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: cloudflare-acme-prod
spec:
acme:
email: your-email@example.com
server: https://acme-v02.api.letsencrypt.org/directory
privateKeySecretRef:
name: letsencrypt-prod-key
solvers:
- dns01:
cloudflare:
apiTokenSecretRef:
name: cloudflare-api-token-secret
key: api-token
```
**Create Cloudflare secret:**
```bash
kubectl create secret generic cloudflare-api-token-secret \
-n cert-manager \
--from-literal=api-token=YOUR_TOKEN
```
### Kubernetes Service Discovery
**Configure in API7 Dashboard:**
1. **Settings → Service Registry → Add Service Registry**
2. **Select:** Kubernetes
3. **Configure:**
```
Name: kubernetes-cluster
API Server: https://kubernetes.default.svc.cluster.local:443
Token Path: /var/run/secrets/kubernetes.io/serviceaccount/token
Namespace Filter: api7ee (optional)
1. **Clone the repository:**
```bash
git clone https://git.commandware.com/demos/api7-demo.git
cd api7-demo
```
**Verify RBAC permissions:**
```bash
kubectl create clusterrolebinding api7-gateway-discovery \
--clusterrole=view \
--serviceaccount=<API7_NAMESPACE>:default
```
2. **Create API7 credentials secret:**
```bash
# Copy the template
cp api7-credentials.yaml.template api7-credentials.yaml
### Ingress Configuration
# Get the admin key from API7 installation
kubectl get secret -n api7ee api7ee3-0-1759339083 \
-o jsonpath='{.data.admin_key}' | base64 -d
**Example ingress for API7 Gateway:**
# Edit api7-credentials.yaml and fill in the actual admin key
vim api7-credentials.yaml
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api7-gateway-ingress
namespace: api7ee
spec:
ingressClassName: nginx
tls:
- hosts:
- demo.yourdomain.com
secretName: demo-tls
rules:
- host: demo.yourdomain.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api7-gateway-service
port:
number: 80
```
# Apply the secret
kubectl apply -f api7-credentials.yaml -n api7ee
```
**Note:** Ingress backend points to API7 Gateway service, not application services.
3. **Update values.yaml to use the secret:**
```yaml
api7:
gateway:
existingSecret: "api7-credentials"
```
## 🛠️ Troubleshooting
4. **Deploy with Helm:**
```bash
helm upgrade --install api7ee-demo helm/api7ee-demo-k8s \
--namespace api7ee \
--create-namespace \
--values values.yaml
```
### Routes Return 404
5. **Verify deployment:**
```bash
kubectl get all -n api7ee
kubectl logs -n api7ee job/api7ee-demo-api7ee-demo-k8s-adc-sync
curl https://commandware.it/api/health
```
**Problem:** Routes synced but returning 404
## Documentation
**Solution:** Routes not published. Publish via API7 Dashboard.
View comprehensive documentation at: **[web/docs/](web/docs/)**
### Service Discovery Not Working
- [Getting Started](web/docs/getting-started.md)
- [Architecture](web/docs/architecture.md)
- [Ingress & Routing](web/docs/ingress-routing.md)
- [Service Discovery](web/docs/service-discovery.md)
- [Secret Management](web/docs/secret-management.md)
- [Troubleshooting](web/docs/troubleshooting.md)
**Problem:** `service registry not found` error
## Support
**Checklist:**
- ✅ Service registry configured in dashboard
- ✅ Registry status shows "Connected"
- ✅ RBAC permissions granted
- ✅ Service ports have `name` field
### Certificate Issues
**Problem:** Certificate not trusted
**Solution:**
```bash
# Check certificate status
kubectl get certificate -n api7ee
kubectl describe certificate -n api7ee <cert-name>
# Check cert-manager logs
kubectl logs -n cert-manager -l app.kubernetes.io/name=cert-manager --tail=50
```
### Container Images Not Found
**Problem:** ImagePullBackOff in Kubernetes
**Solution:**
```bash
# Create registry secret
kubectl create secret docker-registry gitea-registry \
--docker-server=git.commandware.com \
--docker-username=<USERNAME> \
--docker-password=<TOKEN> \
-n api7ee
# Add to deployment
spec:
template:
spec:
imagePullSecrets:
- name: gitea-registry
```
## 📚 Resources
- **API7 Documentation:** https://docs.api7.ai/
- **APISIX Documentation:** https://apisix.apache.org/docs/
- **FastAPI Documentation:** https://fastapi.tiangolo.com/
- **Gitea Actions:** https://docs.gitea.com/usage/actions/overview
## 🔐 Best Practices
### Security
- ✅ Use TLS for all external traffic
- ✅ Implement rate limiting on public endpoints
- ✅ Store secrets in Kubernetes Secrets or external vault
- ✅ Use least-privilege RBAC permissions
### Performance
- ✅ Enable service discovery for automatic scaling
- ✅ Configure appropriate resource limits
- ✅ Use Docker layer caching in CI/CD
- ✅ Implement health checks and readiness probes
### Monitoring
- ✅ Monitor gateway metrics in API7 Dashboard
- ✅ Track API usage and performance
- ✅ Set up alerts for certificate expiration
- ✅ Review application logs regularly
## 📝 License
Demo project for API7 Enterprise Gateway evaluation.
---
**Repository:** https://git.commandware.com/demos/api7-demo.git
- Issues: https://git.commandware.com/demos/api7-demo/issues
- API7 Docs: https://docs.api7.ai/enterprise/

65
adc.yaml Normal file
View File

@@ -0,0 +1,65 @@
services:
- name: apache-service
hosts:
- commandware.it
upstream:
name: apache-upstream
scheme: http
type: roundrobin
nodes:
- host: apache-service.api7ee.svc.cluster.local
port: 80
weight: 100
routes:
- name: apache-route
uris:
- /*
vars:
- - uri
- "~~"
- "^(?!/api)"
priority: 1
plugins:
redirect:
http_to_https: true
- name: nginx-api-service
hosts:
- commandware.it
upstream:
name: nginx-upstream
scheme: http
type: roundrobin
nodes:
- host: nginx-service.api7ee.svc.cluster.local
port: 80
weight: 100
routes:
- name: nginx-api-llm-route
uris:
- /api/llm
- /api/llm/*
priority: 20
plugins:
redirect:
http_to_https: true
ai-rate-limiting:
limit: 100
time_window: 60
rejected_code: 429
limit_strategy: "total_tokens"
- name: nginx-api-route
uris:
- /api
- /api/*
priority: 10
plugins:
redirect:
http_to_https: true
limit-count:
count: 100
time_window: 60
rejected_code: 429
key_type: "var"
key: "remote_addr"

View File

@@ -2,14 +2,18 @@ FROM python:3.11-slim
WORKDIR /app
# Install dependencies
RUN pip install --no-cache-dir fastapi uvicorn[standard] pydantic
# Copy requirements first for better caching
COPY requirements.txt .
# Install dependencies from requirements.txt
RUN pip install --no-cache-dir -r requirements.txt
# Copy application
COPY db.json .
COPY main.py .
# Expose port
EXPOSE 8001
EXPOSE 8080
# Run the application
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8001"]
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"]

View File

@@ -1,87 +1,787 @@
# API Service - FastAPI
# API Service - FastAPI Backend
A RESTful API service built with FastAPI featuring Swagger documentation.
A RESTful API service built with FastAPI featuring automatic Swagger documentation, CRUD operations, and integrated LLM endpoints with OpenAI-compatible API support.
## Features
## 📋 Overview
- 📚 Automatic Swagger/OpenAPI documentation
- 🔄 RESTful API endpoints
- ✅ Data validation with Pydantic
- 📊 Statistics and monitoring endpoints
- ❤️ Health check endpoints
This API backend provides:
## Local Development
- **RESTful CRUD API**: Items and Users management
- **LLM Integration**: OpenAI-compatible chat endpoints for AI-powered features
- **Automatic Documentation**: Swagger UI and ReDoc
- **Data Validation**: Pydantic models with type checking
- **Health Monitoring**: Health check endpoints
- **Production Ready**: Designed for Kubernetes deployment with API7 Gateway
### Run with Python
## ✨ Features
### REST API
- **Items Management**: Create, read, update, delete items
- **Users Management**: User CRUD operations
- **Pagination Support**: Query parameters for data filtering
- **Validation**: Automatic request/response validation with Pydantic
### LLM Integration
- **Chat Endpoint**: OpenAI-compatible chat completions API
- **Model Management**: List available LLM models
- **Token Tracking**: Returns token usage per request
- **Configurable**: Supports custom OpenAI-compatible backends (Open WebUI, Ollama, etc.)
- **Rate Limited**: Designed to work with API7's AI rate limiting (100 tokens/60s)
### Documentation
- **Swagger UI**: Interactive API documentation at `/docs`
- **ReDoc**: Alternative documentation at `/redoc`
- **OpenAPI Schema**: JSON schema at `/openapi.json`
## 🚀 Quick Start
### Local Development
#### Prerequisites
```bash
# Install dependencies
pip install -r requirements.txt
python >= 3.8
pip
```
# Run the application
#### Install Dependencies
```bash
cd api
pip install -r requirements.txt
```
#### Run the Application
```bash
# Basic run
python main.py
# Or use uvicorn directly
uvicorn main:app --reload --host 0.0.0.0 --port 8000
uvicorn main:app --reload --host 0.0.0.0 --port 8001
# With custom port
uvicorn main:app --reload --port 8080
```
### Run with Docker
#### Access the API
- **Root**: http://localhost:8001/
- **Swagger UI**: http://localhost:8001/docs
- **ReDoc**: http://localhost:8001/redoc
- **Health Check**: http://localhost:8001/health
### Docker
#### Build Image
```bash
# Build image
docker build -t fastapi-api .
# Run container
docker run -p 8000:8000 fastapi-api
docker build -t api-service .
```
## API Endpoints
#### Run Container
### Items
- `GET /items` - List all items (with pagination)
- `GET /items/{item_id}` - Get specific item
- `POST /items` - Create new item
- `PUT /items/{item_id}` - Update item
- `DELETE /items/{item_id}` - Delete item
### Users
- `GET /users` - List all users
- `GET /users/{user_id}` - Get specific user
- `POST /users` - Create new user
### Other
- `GET /` - API information
- `GET /health` - Health check
- `GET /info` - Service information
- `GET /stats` - API statistics
## Documentation
- Swagger UI: http://localhost:8000/docs
- ReDoc: http://localhost:8000/redoc
- OpenAPI JSON: http://localhost:8000/openapi.json
## Example Requests
### Create Item
```bash
curl -X POST "http://localhost:8000/items" \
# Basic run
docker run -p 8001:8001 api-service
# With environment variables
docker run -p 8001:8001 \
-e OPENAI_API_BASE="http://host.docker.internal:11434/api" \
-e OPENAI_API_KEY="your-api-key" \
-e DEFAULT_LLM_MODEL="videogame-expert" \
api-service
```
## 🔌 API Endpoints
### Information Endpoints
#### `GET /`
Root endpoint with API information.
**Response**:
```json
{
"message": "Welcome to API Demo",
"version": "1.0.0",
"docs": "/docs",
"timestamp": "2025-10-07T10:00:00"
}
```
#### `GET /health`
Health check endpoint.
**Response**:
```json
{
"status": "healthy",
"service": "api",
"timestamp": "2025-10-07T10:00:00"
}
```
### Items Endpoints
#### `GET /items`
Get all items with optional pagination.
**Query Parameters**:
- None (returns all items)
**Response**:
```json
[
{
"id": 1,
"name": "Laptop",
"description": "High-performance laptop",
"price": 999.99,
"in_stock": true
},
{
"id": 2,
"name": "Mouse",
"description": "Wireless mouse",
"price": 29.99,
"in_stock": true
}
]
```
#### `GET /items/{item_id}`
Get a specific item by ID.
**Response**:
```json
{
"id": 1,
"name": "Laptop",
"description": "High-performance laptop",
"price": 999.99,
"in_stock": true
}
```
#### `POST /items`
Create a new item.
**Request Body**:
```json
{
"name": "Monitor",
"description": "4K Display",
"price": 299.99,
"in_stock": true
}
```
**Response**:
```json
{
"id": 4,
"name": "Monitor",
"description": "4K Display",
"price": 299.99,
"in_stock": true
}
```
#### `PUT /items/{item_id}`
Update an existing item.
**Request Body**:
```json
{
"name": "Monitor",
"description": "Updated 4K Display",
"price": 279.99,
"in_stock": true
}
```
#### `DELETE /items/{item_id}`
Delete an item.
**Response**:
```json
{
"message": "Item deleted successfully"
}
```
### Users Endpoints
#### `GET /users`
Get all users.
**Response**:
```json
[
{
"id": 1,
"username": "john_doe",
"email": "john@example.com",
"active": true
}
]
```
#### `GET /users/{user_id}`
Get a specific user by ID.
#### `POST /users`
Create a new user.
**Request Body**:
```json
{
"username": "jane_doe",
"email": "jane@example.com",
"active": true
}
```
### LLM Endpoints
#### `POST /llm/chat`
Send a chat message to the LLM.
**Request Body**:
```json
{
"prompt": "What is The Legend of Zelda?",
"max_tokens": 150,
"temperature": 0.7,
"model": "videogame-expert"
}
```
**Response**:
```json
{
"response": "The Legend of Zelda is a high-fantasy action-adventure video game franchise created by Japanese game designers Shigeru Miyamoto and Takashi Tezuka...",
"tokens_used": 85,
"model": "videogame-expert",
"timestamp": "2025-10-07T10:00:00"
}
```
**Rate Limiting**: When deployed with API7 Gateway, this endpoint is limited to **100 tokens per 60 seconds** using AI rate limiting.
#### `GET /llm/models`
List available LLM models.
**Response**:
```json
{
"models": [
{
"id": "videogame-expert",
"name": "Videogame Expert",
"max_tokens": 4096,
"provider": "Open WebUI"
}
],
"default_model": "videogame-expert",
"timestamp": "2025-10-07T10:00:00"
}
```
#### `GET /llm/health`
LLM service health check.
**Response**:
```json
{
"status": "healthy",
"service": "llm-api",
"provider": "Open WebUI",
"endpoint": "http://localhost/api",
"default_model": "videogame-expert",
"rate_limit": "ai-rate-limiting enabled (100 tokens/60s)",
"timestamp": "2025-10-07T10:00:00"
}
```
## 🔧 Configuration
### Environment Variables
Configure the API service using environment variables:
| Variable | Description | Default | Required |
| ------------------- | ---------------------------------- | ---------------------- | -------- |
| `OPENAI_API_BASE` | OpenAI-compatible API endpoint URL | `http://localhost/api` | No |
| `OPENAI_API_KEY` | API key for LLM service | `your-api-key` | No |
| `DEFAULT_LLM_MODEL` | Default LLM model ID | `your-model-id` | No |
### Example Configuration
**Development**:
```bash
export OPENAI_API_BASE="http://localhost:11434/api"
export OPENAI_API_KEY="not-required-for-ollama"
export DEFAULT_LLM_MODEL="llama2"
```
**Production (Open WebUI)**:
```bash
export OPENAI_API_BASE="https://openwebui.example.com/api"
export OPENAI_API_KEY="sk-xxxxxxxxxxxxxxxx"
export DEFAULT_LLM_MODEL="videogame-expert"
```
**Kubernetes Deployment**:
```yaml
env:
- name: OPENAI_API_BASE
value: "http://openwebui.ai:8080/api"
- name: OPENAI_API_KEY
valueFrom:
secretKeyRef:
name: llm-secrets
key: api-key
- name: DEFAULT_LLM_MODEL
value: "videogame-expert"
```
## 📊 Data Models
### Item Model
```python
class Item(BaseModel):
id: Optional[int] = None
name: str
description: Optional[str] = None
price: float
in_stock: bool = True
```
### User Model
```python
class User(BaseModel):
id: Optional[int] = None
username: str
email: str
active: bool = True
```
### LLM Request Model
```python
class LLMRequest(BaseModel):
prompt: str
max_tokens: Optional[int] = 150
temperature: Optional[float] = 0.7
model: Optional[str] = DEFAULT_MODEL
```
### LLM Response Model
```python
class LLMResponse(BaseModel):
response: str
tokens_used: int
model: str
timestamp: str
```
## 🧪 Testing
### cURL Examples
**Create Item**:
```bash
curl -X POST "http://localhost:8001/items" \
-H "Content-Type: application/json" \
-d '{
"id": 4,
"name": "Monitor",
"description": "4K Display",
"price": 299.99,
"name": "Keyboard",
"description": "Mechanical keyboard",
"price": 79.99,
"in_stock": true
}'
```
### Get Items
**Get Items**:
```bash
curl "http://localhost:8000/items?skip=0&limit=10&in_stock=true"
curl "http://localhost:8001/items"
```
### Get Statistics
**Update Item**:
```bash
curl "http://localhost:8000/stats"
curl -X PUT "http://localhost:8001/items/1" \
-H "Content-Type: application/json" \
-d '{
"name": "Laptop Pro",
"description": "Updated laptop",
"price": 1099.99,
"in_stock": true
}'
```
**Delete Item**:
```bash
curl -X DELETE "http://localhost:8001/items/3"
```
**LLM Chat**:
```bash
curl -X POST "http://localhost:8001/llm/chat" \
-H "Content-Type: application/json" \
-d '{
"prompt": "Tell me about Mario Bros",
"max_tokens": 100,
"temperature": 0.7,
"model": "videogame-expert"
}'
```
**Health Checks**:
```bash
curl "http://localhost:8001/health"
curl "http://localhost:8001/llm/health"
```
### Python Testing
```python
import httpx
# Test item creation
async with httpx.AsyncClient() as client:
response = await client.post(
"http://localhost:8001/items",
json={
"name": "Monitor",
"description": "4K Display",
"price": 299.99,
"in_stock": True
}
)
print(response.json())
# Test LLM chat
async with httpx.AsyncClient() as client:
response = await client.post(
"http://localhost:8001/llm/chat",
json={
"prompt": "What is Minecraft?",
"max_tokens": 150,
"model": "videogame-expert"
}
)
print(response.json())
```
## 🐳 Docker
### Dockerfile
The Dockerfile uses Python 3.11 slim image and installs dependencies directly:
```dockerfile
FROM python:3.11-slim
WORKDIR /app
# Install dependencies
RUN pip install --no-cache-dir fastapi uvicorn[standard] pydantic
# Copy application
COPY db.json .
COPY main.py .
EXPOSE 8001
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8001"]
```
### Build and Run
```bash
# Build
docker build -t api-service:latest .
# Run with environment variables
docker run -d \
--name api-service \
-p 8001:8001 \
-e OPENAI_API_BASE="http://host.docker.internal:11434/api" \
-e OPENAI_API_KEY="your-key" \
api-service:latest
# View logs
docker logs -f api-service
# Stop and remove
docker stop api-service
docker rm api-service
```
## ☸️ Kubernetes Deployment
### Basic Deployment
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-service
namespace: api7ee
spec:
replicas: 3
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
image: git.commandware.com/demos/api7-demo/api:main
ports:
- containerPort: 8001
name: http
env:
- name: OPENAI_API_BASE
value: "http://openwebui.ai:8080/api"
- name: OPENAI_API_KEY
valueFrom:
secretKeyRef:
name: llm-secrets
key: api-key
- name: DEFAULT_LLM_MODEL
value: "videogame-expert"
livenessProbe:
httpGet:
path: /health
port: 8001
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: 8001
initialDelaySeconds: 10
periodSeconds: 5
resources:
limits:
cpu: 1000m
memory: 1Gi
requests:
cpu: 500m
memory: 512Mi
---
apiVersion: v1
kind: Service
metadata:
name: api-service
namespace: api7ee
spec:
type: ClusterIP
ports:
- port: 8080
targetPort: 8001
name: http
selector:
app: api
```
### With Helm
See the [Helm chart README](../helm/api7ee-demo-k8s/README.md) for full deployment options.
```bash
helm install api7ee-demo ./helm/api7ee-demo-k8s \
--set api.image.tag=v1.0.0 \
--set api.replicaCount=5 \
--namespace api7ee
```
## 🔒 Security
### Best Practices
1. **Environment Variables**: Store sensitive data (API keys) in Kubernetes Secrets
2. **Non-root User**: Container runs as non-root user (UID 1000)
3. **Read-only Filesystem**: Root filesystem is read-only
4. **Input Validation**: All requests validated with Pydantic
5. **CORS**: Configure CORS policies in API7 Gateway
6. **Rate Limiting**: API7 Gateway enforces rate limits
### Example Secret
```bash
kubectl create secret generic llm-secrets \
--from-literal=api-key='your-openai-api-key' \
-n api7ee
```
## 📦 Dependencies
### Python Requirements
```
fastapi==0.104.1
uvicorn[standard]==0.24.0
pydantic==2.5.0
httpx==0.26.0
```
### Install
```bash
pip install -r requirements.txt
```
## 🚀 Production Deployment
### Recommended Configuration
```yaml
api:
replicaCount: 5
autoscaling:
enabled: true
minReplicas: 5
maxReplicas: 30
targetCPUUtilizationPercentage: 70
resources:
limits:
cpu: 2000m
memory: 2Gi
requests:
cpu: 1000m
memory: 1Gi
env:
- name: LOG_LEVEL
value: "warn"
- name: ENVIRONMENT
value: "production"
```
### API7 Gateway Integration
When deployed behind API7 Gateway:
**Rate Limiting**:
- `/api/*`: 100 requests/60s per IP (standard rate limiting)
- `/api/llm/*`: 100 tokens/60s (AI rate limiting)
**Routing**:
- Priority 10: `/api/*` routes
- Priority 20: `/api/llm/*` routes (higher priority)
**Plugins**:
- `redirect`: HTTP → HTTPS
- `limit-count`: IP-based rate limiting
- `ai-rate-limiting`: Token-based LLM rate limiting
## 📚 Resources
### Documentation
- **FastAPI**: https://fastapi.tiangolo.com/
- **Pydantic**: https://docs.pydantic.dev/
- **Uvicorn**: https://www.uvicorn.org/
- **OpenAI API**: https://platform.openai.com/docs/api-reference
### Related
- [Main README](../README.md)
- [Web Application README](../web/README.md)
- [Helm Chart README](../helm/api7ee-demo-k8s/README.md)
## 🐛 Troubleshooting
### Common Issues
**Issue**: LLM endpoints return errors
```bash
# Check environment variables
echo $OPENAI_API_BASE
echo $OPENAI_API_KEY
# Test LLM backend directly
curl -X POST "$OPENAI_API_BASE/chat/completions" \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"videogame-expert","messages":[{"role":"user","content":"test"}]}'
```
**Issue**: Rate limiting triggered
```bash
# Check API7 Gateway logs
kubectl logs -n api7ee -l app=api7-gateway
# Response: HTTP 429
# Cause: Exceeded 100 tokens/60s or 100 req/60s
# Solution: Wait for rate limit window to reset
```
**Issue**: Health check fails
```bash
# Check if service is running
curl http://localhost:8001/health
# Check logs
docker logs api-service
# or
kubectl logs -n api7ee -l app=api
```
---
**Version**: 1.0.0 | **Port**: 8001 | **Framework**: FastAPI 0.104.1

199
api/db.json Normal file
View File

@@ -0,0 +1,199 @@
{
"items": [
{
"id": 1,
"name": "Gaming Laptop RTX 4090",
"description": "High-performance gaming laptop with RTX 4090",
"price": 2999.99,
"in_stock": true,
"category": "electronics",
"tags": ["gaming", "laptop", "nvidia"]
},
{
"id": 2,
"name": "Mechanical Keyboard RGB",
"description": "Cherry MX switches with RGB backlighting",
"price": 149.99,
"in_stock": true,
"category": "peripherals",
"tags": ["keyboard", "mechanical", "rgb"]
},
{
"id": 3,
"name": "Wireless Gaming Mouse",
"description": "25K DPI wireless gaming mouse",
"price": 89.99,
"in_stock": false,
"category": "peripherals",
"tags": ["mouse", "wireless", "gaming"]
},
{
"id": 4,
"name": "4K Gaming Monitor 32\"",
"description": "144Hz refresh rate, HDR support",
"price": 599.99,
"in_stock": true,
"category": "displays",
"tags": ["monitor", "4k", "gaming"]
},
{
"id": 5,
"name": "Gaming Headset 7.1",
"description": "Surround sound gaming headset with noise cancellation",
"price": 129.99,
"in_stock": true,
"category": "audio",
"tags": ["headset", "audio", "gaming"]
}
],
"users": [
{
"id": 1,
"username": "john_doe",
"email": "john@example.com",
"active": true,
"role": "user",
"created_at": "2024-01-15T10:30:00Z"
},
{
"id": 2,
"username": "jane_smith",
"email": "jane@example.com",
"active": true,
"role": "admin",
"created_at": "2024-02-20T14:22:00Z"
},
{
"id": 3,
"username": "bob_wilson",
"email": "bob@example.com",
"active": false,
"role": "user",
"created_at": "2024-03-10T09:15:00Z"
},
{
"id": 4,
"username": "alice_johnson",
"email": "alice@example.com",
"active": true,
"role": "moderator",
"created_at": "2024-04-05T16:45:00Z"
}
],
"orders": [
{
"id": 1,
"user_id": 1,
"items": [
{"item_id": 1, "quantity": 1, "price": 2999.99},
{"item_id": 2, "quantity": 1, "price": 149.99}
],
"total": 3149.98,
"status": "shipped",
"created_at": "2024-09-15T12:00:00Z"
},
{
"id": 2,
"user_id": 2,
"items": [
{"item_id": 4, "quantity": 2, "price": 599.99}
],
"total": 1199.98,
"status": "delivered",
"created_at": "2024-09-20T15:30:00Z"
},
{
"id": 3,
"user_id": 4,
"items": [
{"item_id": 5, "quantity": 1, "price": 129.99},
{"item_id": 3, "quantity": 1, "price": 89.99}
],
"total": 219.98,
"status": "pending",
"created_at": "2024-10-01T10:15:00Z"
}
],
"reviews": [
{
"id": 1,
"item_id": 1,
"user_id": 1,
"rating": 5,
"comment": "Amazing laptop! Best purchase ever!",
"created_at": "2024-09-20T14:30:00Z"
},
{
"id": 2,
"item_id": 2,
"user_id": 2,
"rating": 4,
"comment": "Great keyboard, but a bit loud",
"created_at": "2024-09-22T09:45:00Z"
},
{
"id": 3,
"item_id": 4,
"user_id": 2,
"rating": 5,
"comment": "Crystal clear display, perfect for gaming",
"created_at": "2024-09-25T18:20:00Z"
},
{
"id": 4,
"item_id": 5,
"user_id": 4,
"rating": 4,
"comment": "Good sound quality, comfortable to wear",
"created_at": "2024-10-02T11:00:00Z"
}
],
"categories": [
{
"id": 1,
"name": "electronics",
"description": "Electronic devices and gadgets"
},
{
"id": 2,
"name": "peripherals",
"description": "Computer peripherals and accessories"
},
{
"id": 3,
"name": "displays",
"description": "Monitors and display devices"
},
{
"id": 4,
"name": "audio",
"description": "Audio devices and accessories"
}
],
"llm_requests": [
{
"id": 1,
"user_id": 1,
"model": "videogame-expert",
"prompt": "What are the best RPG games of 2024?",
"tokens_used": 250,
"timestamp": "2024-10-05T10:00:00Z"
},
{
"id": 2,
"user_id": 2,
"model": "videogame-expert",
"prompt": "Recommend me games similar to Dark Souls",
"tokens_used": 180,
"timestamp": "2024-10-05T11:30:00Z"
},
{
"id": 3,
"user_id": 1,
"model": "videogame-expert",
"prompt": "What's the best strategy for Elden Ring bosses?",
"tokens_used": 320,
"timestamp": "2024-10-05T14:15:00Z"
}
]
}

View File

@@ -1,28 +1,107 @@
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import List, Optional
from pydantic import BaseModel, Field
import uvicorn
from datetime import datetime
from fastapi import FastAPI, HTTPException, status
from fastapi.responses import JSONResponse
from pydantic import BaseModel
from typing import Optional, List
import os
import httpx
# OpenAI API configuration
OPENAI_API_BASE = os.getenv("OPENAI_API_BASE", "http://localhost/api")
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "your-api-key")
DEFAULT_MODEL = os.getenv("DEFAULT_LLM_MODEL", "your-model-id")
app = FastAPI(
title="API Demo Application",
description="Demo API with Swagger documentation",
version="1.0.0"
title="API7 Enterprise Demo API",
description="""
## API7 Enterprise Demo Application
This API demonstrates the capabilities of API7 Enterprise Gateway with:
* **CRUD Operations** - Items and Users management
* **LLM Integration** - AI-powered chat with rate limiting
* **Health Checks** - Kubernetes-ready endpoints
* **Rate Limiting** - Standard (100 req/min) and AI-based (100 tokens/min)
* **CORS** - Cross-origin resource sharing enabled
* **Proxy Rewrite** - Automatic /api prefix removal by API7 Gateway
### Architecture
\`\`\`
Client → Ingress (NGINX) → API7 Gateway → Backend API
• Rate Limiting
• CORS
• Proxy Rewrite (/api → /)
• Service Discovery
\`\`\`
### Rate Limiting
- **Standard API** (\`/items\`, \`/users\`): 100 requests per 60 seconds per IP
- **LLM API** (\`/llm/*\`): 100 tokens per 60 seconds (AI-based rate limiting)
### Documentation
- **Swagger UI**: [/docs](/docs)
- **ReDoc**: [/redoc](/redoc)
- **OpenAPI JSON**: [/openapi.json](/openapi.json)
""",
version="1.0.0",
contact={
"name": "CommandWare",
"url": "https://commandware.it",
},
license_info={
"name": "MIT",
},
servers=[
{
"url": "https://commandware.it/api",
"description": "Production server (via API7 Gateway)"
},
{
"url": "http://localhost:8080",
"description": "Local development server"
}
]
)
# Models
class Item(BaseModel):
id: Optional[int] = None
name: str
description: Optional[str] = None
price: float
in_stock: bool = True
id: Optional[int] = Field(None, description="Item ID (auto-generated)")
name: str = Field(..., description="Item name", example="Laptop")
description: Optional[str] = Field(None, description="Item description", example="High-performance laptop")
price: float = Field(..., description="Item price", example=999.99, gt=0)
in_stock: bool = Field(True, description="Stock availability")
class Config:
schema_extra = {
"example": {
"name": "Laptop",
"description": "High-performance laptop",
"price": 999.99,
"in_stock": True
}
}
class User(BaseModel):
id: Optional[int] = None
username: str
email: str
active: bool = True
id: Optional[int] = Field(None, description="User ID (auto-generated)")
username: str = Field(..., description="Username", example="john_doe", min_length=3)
email: str = Field(..., description="Email address", example="john@example.com")
active: bool = Field(True, description="User active status")
class Config:
schema_extra = {
"example": {
"username": "john_doe",
"email": "john@example.com",
"active": True
}
}
# In-memory storage
items_db = [
@@ -37,48 +116,150 @@ users_db = [
]
# Root endpoint
@app.get("/")
@app.get(
"/",
tags=["Root"],
summary="API Information",
response_description="API metadata and links"
)
async def root():
"""Root endpoint with API information"""
"""
**Root endpoint** with API information and navigation links.
Returns API version, documentation links, and current timestamp.
"""
return {
"message": "Welcome to API Demo",
"message": "Welcome to API7 Enterprise Demo API",
"version": "1.0.0",
"docs": "/docs",
"documentation": {
"swagger": "/docs",
"redoc": "/redoc",
"openapi": "/openapi.json"
},
"endpoints": {
"items": "/items",
"users": "/users",
"llm": "/llm"
},
"timestamp": datetime.now().isoformat()
}
# Health check
@app.get("/health")
@app.get(
"/health",
tags=["Health"],
summary="Health Check",
response_description="Service health status"
)
async def health():
"""Health check endpoint"""
return {"status": "healthy", "service": "api", "timestamp": datetime.now().isoformat()}
"""
**Health check endpoint** for Kubernetes liveness probe.
Returns service health status and timestamp.
"""
return {
"status": "healthy",
"service": "api",
"timestamp": datetime.now().isoformat()
}
# Readiness check
@app.get(
"/ready",
tags=["Health"],
summary="Readiness Check",
response_description="Service readiness status"
)
async def ready():
"""
**Readiness check endpoint** for Kubernetes readiness probe.
Returns service readiness status and timestamp.
"""
return {
"status": "ready",
"service": "api",
"timestamp": datetime.now().isoformat()
}
# Items endpoints
@app.get("/items", response_model=List[Item], tags=["Items"])
@app.get(
"/items",
response_model=List[Item],
tags=["Items"],
summary="List all items",
response_description="List of all items in inventory"
)
async def get_items():
"""Get all items"""
"""
**Get all items** from the inventory.
Returns a list of all available items with their details.
**Rate Limit**: 100 requests per 60 seconds per IP (via API7 Gateway)
"""
return items_db
@app.get("/items/{item_id}", response_model=Item, tags=["Items"])
@app.get(
"/items/{item_id}",
response_model=Item,
tags=["Items"],
summary="Get item by ID",
response_description="Item details",
responses={404: {"description": "Item not found"}}
)
async def get_item(item_id: int):
"""Get a specific item by ID"""
"""
**Get a specific item** by ID.
- **item_id**: The ID of the item to retrieve
Returns item details if found, otherwise 404 error.
"""
item = next((item for item in items_db if item["id"] == item_id), None)
if item is None:
raise HTTPException(status_code=404, detail="Item not found")
return item
@app.post("/items", response_model=Item, tags=["Items"])
@app.post(
"/items",
response_model=Item,
tags=["Items"],
summary="Create new item",
status_code=status.HTTP_201_CREATED,
response_description="Created item with auto-generated ID"
)
async def create_item(item: Item):
"""Create a new item"""
"""
**Create a new item** in the inventory.
The ID will be auto-generated. Provide:
- **name**: Item name (required)
- **description**: Item description (optional)
- **price**: Item price (required, must be > 0)
- **in_stock**: Stock availability (default: true)
"""
new_id = max([i["id"] for i in items_db]) + 1 if items_db else 1
item_dict = item.dict()
item_dict["id"] = new_id
items_db.append(item_dict)
return item_dict
@app.put("/items/{item_id}", response_model=Item, tags=["Items"])
@app.put(
"/items/{item_id}",
response_model=Item,
tags=["Items"],
summary="Update item",
response_description="Updated item",
responses={404: {"description": "Item not found"}}
)
async def update_item(item_id: int, item: Item):
"""Update an existing item"""
"""
**Update an existing item** by ID.
- **item_id**: The ID of the item to update
- Provide updated item data (name, description, price, in_stock)
"""
for idx, existing_item in enumerate(items_db):
if existing_item["id"] == item_id:
item_dict = item.dict()
@@ -87,37 +268,236 @@ async def update_item(item_id: int, item: Item):
return item_dict
raise HTTPException(status_code=404, detail="Item not found")
@app.delete("/items/{item_id}", tags=["Items"])
@app.delete(
"/items/{item_id}",
tags=["Items"],
summary="Delete item",
status_code=status.HTTP_200_OK,
response_description="Deletion confirmation",
responses={404: {"description": "Item not found"}}
)
async def delete_item(item_id: int):
"""Delete an item"""
"""
**Delete an item** from the inventory.
- **item_id**: The ID of the item to delete
Returns confirmation message if successful.
"""
for idx, item in enumerate(items_db):
if item["id"] == item_id:
items_db.pop(idx)
return {"message": "Item deleted successfully"}
return {"message": "Item deleted successfully", "id": item_id}
raise HTTPException(status_code=404, detail="Item not found")
# Users endpoints
@app.get("/users", response_model=List[User], tags=["Users"])
@app.get(
"/users",
response_model=List[User],
tags=["Users"],
summary="List all users",
response_description="List of all users"
)
async def get_users():
"""Get all users"""
"""
**Get all users** from the system.
Returns a list of all registered users.
**Rate Limit**: 100 requests per 60 seconds per IP (via API7 Gateway)
"""
return users_db
@app.get("/users/{user_id}", response_model=User, tags=["Users"])
@app.get(
"/users/{user_id}",
response_model=User,
tags=["Users"],
summary="Get user by ID",
response_description="User details",
responses={404: {"description": "User not found"}}
)
async def get_user(user_id: int):
"""Get a specific user by ID"""
"""
**Get a specific user** by ID.
- **user_id**: The ID of the user to retrieve
Returns user details if found, otherwise 404 error.
"""
user = next((user for user in users_db if user["id"] == user_id), None)
if user is None:
raise HTTPException(status_code=404, detail="User not found")
return user
@app.post("/users", response_model=User, tags=["Users"])
@app.post(
"/users",
response_model=User,
tags=["Users"],
summary="Create new user",
status_code=status.HTTP_201_CREATED,
response_description="Created user with auto-generated ID"
)
async def create_user(user: User):
"""Create a new user"""
"""
**Create a new user** in the system.
The ID will be auto-generated. Provide:
- **username**: Username (required, min 3 characters)
- **email**: Email address (required)
- **active**: User active status (default: true)
"""
new_id = max([u["id"] for u in users_db]) + 1 if users_db else 1
user_dict = user.dict()
user_dict["id"] = new_id
users_db.append(user_dict)
return user_dict
# LLM endpoints
class LLMRequest(BaseModel):
prompt: str = Field(..., description="The prompt to send to the LLM", example="What is API7 Enterprise?")
max_tokens: Optional[int] = Field(150, description="Maximum tokens in response", example=150, ge=1, le=4096)
temperature: Optional[float] = Field(0.7, description="Sampling temperature (0-2)", example=0.7, ge=0, le=2)
model: Optional[str] = Field(DEFAULT_MODEL, description="Model to use", example="videogame-expert")
class Config:
schema_extra = {
"example": {
"prompt": "What is API7 Enterprise?",
"max_tokens": 150,
"temperature": 0.7,
"model": "videogame-expert"
}
}
class LLMResponse(BaseModel):
response: str = Field(..., description="LLM generated response")
tokens_used: int = Field(..., description="Total tokens used")
model: str = Field(..., description="Model used for generation")
timestamp: str = Field(..., description="Response timestamp")
@app.post(
"/llm/chat",
response_model=LLMResponse,
tags=["LLM"],
summary="LLM Chat Completion",
response_description="LLM generated response",
responses={
429: {"description": "Rate limit exceeded (100 tokens per 60 seconds)"},
500: {"description": "LLM service error"}
}
)
async def llm_chat(request: LLMRequest):
"""
**LLM Chat endpoint** - connects to OpenAI-compatible API (Open WebUI).
This endpoint uses **AI-based rate limiting** via API7 Gateway:
- **Limit**: 100 tokens per 60 seconds
- **Strategy**: total_tokens (input + output)
- **Error**: HTTP 429 when limit exceeded
Provide:
- **prompt**: Your question or prompt
- **max_tokens**: Maximum response length (default: 150)
- **temperature**: Randomness level 0-2 (default: 0.7)
- **model**: Model to use (default: configured model)
"""
try:
async with httpx.AsyncClient() as client:
response = await client.post(
f"{OPENAI_API_BASE}/chat/completions",
headers={
"Authorization": f"Bearer {OPENAI_API_KEY}",
"Content-Type": "application/json"
},
json={
"model": request.model,
"messages": [
{"role": "user", "content": request.prompt}
],
"max_tokens": request.max_tokens,
"temperature": request.temperature
},
timeout=30.0
)
response.raise_for_status()
data = response.json()
# Extract response and token usage
llm_response = data["choices"][0]["message"]["content"]
tokens_used = data.get("usage", {}).get("total_tokens", 0)
return LLMResponse(
response=llm_response,
tokens_used=tokens_used,
model=request.model,
timestamp=datetime.now().isoformat()
)
except httpx.HTTPStatusError as e:
raise HTTPException(status_code=e.response.status_code, detail=f"OpenAI API error: {e.response.text}")
except Exception as e:
raise HTTPException(status_code=500, detail=f"LLM service error: {str(e)}")
@app.get(
"/llm/models",
tags=["LLM"],
summary="List available LLM models",
response_description="List of available models"
)
async def list_llm_models():
"""
**List available LLM models**.
Returns the list of models available through the configured LLM provider (Open WebUI).
"""
return {
"models": [
{
"id": "videogame-expert",
"name": "Videogame Expert",
"max_tokens": 4096,
"provider": "Open WebUI",
"description": "Specialized model for videogame-related questions"
}
],
"default_model": DEFAULT_MODEL,
"provider": "Open WebUI",
"timestamp": datetime.now().isoformat()
}
@app.get(
"/llm/health",
tags=["LLM"],
summary="LLM service health check",
response_description="LLM service health status"
)
async def llm_health():
"""
**LLM service health check**.
Returns the health status of the LLM integration, including:
- Provider information
- Endpoint configuration
- Rate limit settings
"""
return {
"status": "healthy",
"service": "llm-api",
"provider": "Open WebUI",
"endpoint": OPENAI_API_BASE,
"default_model": DEFAULT_MODEL,
"rate_limit": {
"enabled": True,
"limit": 100,
"window": "60 seconds",
"strategy": "total_tokens",
"managed_by": "API7 Gateway (ai-rate-limiting plugin)"
},
"timestamp": datetime.now().isoformat()
}
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8001)
port = int(os.getenv("PORT", 8080))
print(f"Starting API server on port {port}")
print(f"Swagger UI: http://localhost:{port}/docs")
print(f"ReDoc: http://localhost:{port}/redoc")
uvicorn.run(app, host="0.0.0.0", port=port)

View File

@@ -1,3 +1,4 @@
fastapi==0.104.1
uvicorn[standard]==0.24.0
fastapi==0.109.0
uvicorn==0.27.0
pydantic==2.5.0
httpx==0.26.0

View File

@@ -0,0 +1,55 @@
# API7 Gateway Credentials Secret Template
#
# This is a template for creating the API7 Gateway admin credentials secret.
# DO NOT commit this file with actual credentials filled in!
#
# Usage:
# 1. Copy this template: cp api7-credentials.yaml.template api7-credentials.yaml
# 2. Fill in the actual values (see instructions below)
# 3. Apply to cluster: kubectl apply -f api7-credentials.yaml -n api7ee
# 4. Update values.yaml to reference this secret:
# api7.gateway.existingSecret: "api7-credentials"
#
# IMPORTANT: Add api7-credentials.yaml to .gitignore!
---
apiVersion: v1
kind: Secret
metadata:
name: api7-credentials
namespace: api7ee
labels:
app.kubernetes.io/name: api7ee-demo
app.kubernetes.io/component: api7-gateway
type: Opaque
stringData:
# Dashboard Admin API URL
# This is the HTTPS endpoint of the API7 Enterprise Dashboard
# Default: https://api7ee3-0-1759339083-dashboard:7443
admin-url: "https://api7ee3-0-1759339083-dashboard:7443"
# Admin API Key
# Get the actual key from the API7 Enterprise installation:
# kubectl get secret -n api7ee api7ee3-0-1759339083 -o jsonpath='{.data.admin_key}' | base64 -d
#
# REPLACE WITH YOUR ACTUAL KEY:
admin-key: "YOUR_ADMIN_KEY_HERE"
# Gateway Group
# Logical grouping of gateway instances
# Default: default
gateway-group: "default"
---
# Example using base64 encoded values (alternative to stringData)
# apiVersion: v1
# kind: Secret
# metadata:
# name: api7-credentials
# namespace: api7ee
# type: Opaque
# data:
# # Base64 encoded values
# admin-url: aHR0cHM6Ly9hcGk3ZWUzLTAtMTc1OTMzOTA4My1kYXNoYm9hcmQ6NzQ0Mw==
# admin-key: WU9VUl9BRE1JTl9LRVlfSEVSRQ==
# gateway-group: ZGVmYXVsdA==

View File

@@ -0,0 +1,23 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/

View File

@@ -0,0 +1,17 @@
apiVersion: v2
name: api7ee-demo-k8s
description: A Helm chart for API7 Enterprise Edition demo application
type: application
version: 0.1.0
appVersion: "1.0.0"
keywords:
- api7
- api-gateway
- web
- api
home: https://demo.commandware.it
sources:
- https://git.commandware.com/demos/api7-demo
maintainers:
- name: CommandWare
email: support@commandware.com

View File

@@ -0,0 +1,864 @@
# API7 Enterprise Edition Demo - Helm Chart
A comprehensive Helm chart for deploying the API7 Enterprise Edition demo platform on Kubernetes. This chart deploys both web frontend and API backend services with full API7 Gateway integration, including automatic ADC configuration, TLS management, and advanced rate limiting.
## 📋 Overview
This Helm chart provides:
- **Dual Service Deployment**: Web frontend + API backend
- **API7 Gateway Configuration**: Automatic ADC (API7 Declarative CLI) sync to existing gateway
- ⚠️ **Does NOT install API7 Gateway** - gateway must already exist
- Only configures routes, services, and upstreams on existing gateway
- **TLS/SSL Management**: cert-manager integration or custom certificates
- **Service Discovery**: Kubernetes-native service discovery
- **Rate Limiting**: Standard and AI token-based rate limiting
- **Autoscaling**: Horizontal Pod Autoscaler (HPA) support
- **High Availability**: Pod Disruption Budgets and multi-replica deployment
- **Security**: Pod Security Contexts, RBAC, and Network Policies
## 🎯 Features
### Deployments
- **Web Service** (apache-service): FastAPI frontend with embedded documentation
- **API Service** (nginx-service): FastAPI backend with REST API and LLM endpoints
### API7 Gateway Configuration
- **Automatic ADC Sync**: Configures routes, services, and upstreams
- **Advanced Rate Limiting**:
- Standard: 100 req/60s per IP for `/api/*`
- AI: 100 tokens/60s for `/api/llm/*`
- **Route Prioritization**: LLM (20) > API (10) > Web (1)
- **TLS/SSL**: Automatic HTTPS redirect and certificate management
- **Plugins**: CORS, Prometheus metrics, request logging, authentication
### Kubernetes Resources
- Deployments (Web and API)
- Services (ClusterIP)
- Ingress (NGINX)
- ConfigMaps (Application config and ADC config)
- Secrets (TLS certificates, API7 credentials)
- ServiceAccount and RBAC
- HorizontalPodAutoscaler
- PodDisruptionBudget
- Certificate (cert-manager)
- Job (ADC sync)
## 📦 Prerequisites
### Required
- **Kubernetes**: v1.19 or higher
- **Helm**: 3.8.0 or higher
- **API7 Enterprise Edition Gateway**: **MUST be already installed and running**
- This chart does NOT install API7 Gateway
- This chart only configures routes and services on an existing gateway
- You need the DP Manager admin URL and admin key
- Example: `http://api7ee3-0-xxx-dp-manager.api7ee.svc.cluster.local:7900`
### Optional
- **Ingress Controller**: NGINX Ingress Controller (for direct Kubernetes ingress)
- **cert-manager**: v1.0+ (for automatic TLS certificate management)
### Before Installation
1. **Verify API7 Gateway is Running**:
```bash
kubectl get pods -n api7ee | grep gateway
kubectl get pods -n api7ee | grep dp-manager
```
2. **Get API7 Admin Credentials**:
```bash
# Get DP Manager URL
kubectl get svc -n api7ee | grep dp-manager
# Get admin key from API7 Dashboard or documentation
# Default is usually generated during API7 installation
```
3. **Test Gateway Connectivity**:
```bash
# Test from within cluster
kubectl run test-curl --rm -i --restart=Never --image=curlimages/curl:latest \
-- curl -s http://api7ee3-0-xxx-dp-manager.api7ee.svc.cluster.local:7900/version
```
## 🚀 Installation
### Add Helm Repository
```bash
# Add the Gitea Helm repository
helm repo add api7ee https://git.commandware.com/api/packages/demos/helm
# Update repositories
helm repo update
# Search for available versions
helm search repo api7ee/api7ee-demo-k8s
```
### Basic Installation
```bash
# Install with default values
helm install api7ee-demo api7ee/api7ee-demo-k8s \
--namespace api7ee \
--create-namespace
# Check installation
helm list -n api7ee
kubectl get pods -n api7ee
```
### Installation with Custom Values
```bash
# Install with custom values file
helm install api7ee-demo api7ee/api7ee-demo-k8s \
--namespace api7ee \
--create-namespace \
-f custom-values.yaml
# Install with command-line overrides
helm install api7ee-demo api7ee/api7ee-demo-k8s \
--namespace api7ee \
--set web.replicaCount=3 \
--set api.replicaCount=5 \
--set api7.gateway.adminKey="your-admin-key" \
--set api7.hosts[0]="your-domain.com"
```
### Install from Local Chart
```bash
# Install from local directory
helm install api7ee-demo ./helm/api7ee-demo-k8s \
--namespace api7ee \
--create-namespace
# With development values
helm install api7ee-dev ./helm/api7ee-demo-k8s \
-f ./helm/api7ee-demo-k8s/values-dev.yaml \
--namespace api7ee-dev
```
## ⚙️ Configuration
### Global Configuration
| Parameter | Description | Default |
| ------------------------- | ---------------------- | ------- |
| `global.imageRegistry` | Global Docker registry | `""` |
| `global.imagePullSecrets` | Image pull secrets | `[]` |
### Web Service Configuration
| Parameter | Description | Default |
| ------------------------------------------------ | --------------------- | ---------------------- |
| `web.enabled` | Enable Web component | `true` |
| `web.replicaCount` | Number of replicas | `2` |
| `web.image.registry` | Image registry | `gitea.server_url` |
| `web.image.repository` | Image repository | `gitea.repository/web` |
| `web.image.tag` | Image tag | `main` |
| `web.image.pullPolicy` | Image pull policy | `IfNotPresent` |
| `web.service.type` | Service type | `ClusterIP` |
| `web.service.port` | Service port | `8000` |
| `web.resources.limits.cpu` | CPU limit | `500m` |
| `web.resources.limits.memory` | Memory limit | `512Mi` |
| `web.resources.requests.cpu` | CPU request | `250m` |
| `web.resources.requests.memory` | Memory request | `256Mi` |
| `web.autoscaling.enabled` | Enable HPA | `false` |
| `web.autoscaling.minReplicas` | Min replicas | `2` |
| `web.autoscaling.maxReplicas` | Max replicas | `10` |
| `web.autoscaling.targetCPUUtilizationPercentage` | CPU target | `80` |
| `web.env` | Environment variables | `[]` |
### API Service Configuration
| Parameter | Description | Default |
| ------------------------------------------------ | --------------------- | ---------------------------------- |
| `api.enabled` | Enable API component | `true` |
| `api.replicaCount` | Number of replicas | `3` |
| `api.image.registry` | Image registry | `gitea.server_url` |
| `api.image.repository` | Image repository | `gitea.repository/api` |
| `api.image.tag` | Image tag | `main` |
| `api.image.pullPolicy` | Image pull policy | `IfNotPresent` |
| `api.service.type` | Service type | `ClusterIP` |
| `api.service.port` | Service port | `8080` |
| `api.resources.limits.cpu` | CPU limit | `1000m` |
| `api.resources.limits.memory` | Memory limit | `1Gi` |
| `api.resources.requests.cpu` | CPU request | `500m` |
| `api.resources.requests.memory` | Memory request | `512Mi` |
| `api.autoscaling.enabled` | Enable HPA | `true` |
| `api.autoscaling.minReplicas` | Min replicas | `3` |
| `api.autoscaling.maxReplicas` | Max replicas | `20` |
| `api.autoscaling.targetCPUUtilizationPercentage` | CPU target | `70` |
| `api.env` | Environment variables | `[{name: LOG_LEVEL, value: info}]` |
### Ingress Configuration
| Parameter | Description | Default |
| --------------------------- | ------------------- | ---------------------------- |
| `ingress.enabled` | Enable ingress | `true` |
| `ingress.className` | Ingress class | `nginx` |
| `ingress.annotations` | Ingress annotations | See values.yaml |
| `ingress.hosts[0].host` | Hostname | `commandware.it` |
| `ingress.tls[0].secretName` | TLS secret | `api7ee-tls` |
| `ingress.tls[0].hosts` | TLS hosts | `[commandware.it]` |
### API7 Gateway Configuration
| Parameter | Description | Default |
| ----------------------------- | ---------------------- | ------------------------------------ |
| `api7.enabled` | Enable API7 ADC config | `true` |
| `api7.adc.image` | ADC Docker image | `ghcr.io/api7/adc:latest` |
| `api7.adc.verbose` | Verbose logging | `true` |
| `api7.adc.tlsSkipVerify` | Skip TLS verify | `false` |
| `api7.gateway.adminUrl` | API7 Admin API URL | `http://api7ee3-0-xxx-dp-manager...` |
| `api7.gateway.adminKey` | API7 Admin API key | `edd1c9f034335f136f87ad84b625c8f1` |
| `api7.gateway.group` | Gateway group | `default` |
| `api7.gateway.gatewayService` | Gateway service name | `gateway-0-xxx-gateway` |
| `api7.backend` | Backend type | `api7ee` |
| `api7.autoPublish` | Auto-publish routes | `true` |
| `api7.hosts` | Routing hosts | `[commandware.it]` |
### TLS Configuration
| Parameter | Description | Default |
| --------------------------------- | ------------------- | ---------------------- |
| `api7.tls.enabled` | Enable TLS | `true` |
| `api7.tls.certManager.enabled` | Use cert-manager | `true` |
| `api7.tls.certManager.issuer` | ClusterIssuer name | `cloudflare-acme-prod` |
| `api7.tls.certManager.issuerKind` | Issuer kind | `ClusterIssuer` |
| `api7.tls.secretName` | Existing TLS secret | `""` |
| `api7.tls.certificate` | Direct certificate | `""` |
| `api7.tls.key` | Direct key | `""` |
### Rate Limiting Configuration
| Parameter | Description | Default |
| ---------------------------------------- | ----------------------------- | -------------- |
| `api7.plugins.rateLimit.enabled` | Enable standard rate limiting | `true` |
| `api7.plugins.rateLimit.count` | Request limit | `100` |
| `api7.plugins.rateLimit.timeWindow` | Time window (seconds) | `60` |
| `api7.plugins.rateLimit.keyType` | Key type | `var` |
| `api7.plugins.rateLimit.key` | Key variable | `remote_addr` |
| `api7.plugins.aiRateLimit.enabled` | Enable AI rate limiting | `true` |
| `api7.plugins.aiRateLimit.limit` | Token limit | `100` |
| `api7.plugins.aiRateLimit.timeWindow` | Time window (seconds) | `60` |
| `api7.plugins.aiRateLimit.limitStrategy` | Limit strategy | `total_tokens` |
### Additional Plugins
| Parameter | Description | Default |
| --------------------------------- | ------------------------- | ------- |
| `api7.plugins.cors.enabled` | Enable CORS | `true` |
| `api7.plugins.cors.allowOrigins` | Allowed origins | `["*"]` |
| `api7.plugins.auth.enabled` | Enable authentication | `false` |
| `api7.plugins.prometheus.enabled` | Enable Prometheus metrics | `true` |
| `api7.plugins.logging.enabled` | Enable request logging | `false` |
## 📝 Configuration Examples
### Production Deployment
```yaml
# production-values.yaml
global:
imageRegistry: "git.commandware.com"
imagePullSecrets:
- name: registry-secret
web:
replicaCount: 3
image:
tag: "v1.0.0"
pullPolicy: Always
resources:
limits:
cpu: 1000m
memory: 1Gi
requests:
cpu: 500m
memory: 512Mi
autoscaling:
enabled: true
minReplicas: 3
maxReplicas: 15
api:
replicaCount: 5
image:
tag: "v1.0.0"
pullPolicy: Always
resources:
limits:
cpu: 2000m
memory: 2Gi
requests:
cpu: 1000m
memory: 1Gi
autoscaling:
enabled: true
minReplicas: 5
maxReplicas: 30
api7:
gateway:
adminKey: "${API7_ADMIN_KEY}" # Use secret
hosts:
- api7-demo.yourdomain.com
plugins:
rateLimit:
count: 1000
auth:
enabled: true
```
```bash
helm install api7ee-prod api7ee/api7ee-demo-k8s \
-f production-values.yaml \
--namespace api7ee-prod \
--create-namespace
```
### Custom Domain and TLS
```yaml
# custom-domain-values.yaml
api7:
hosts:
- api.example.com
- demo.example.com
tls:
certManager:
enabled: true
issuer: letsencrypt-prod
ingress:
hosts:
- host: api.example.com
paths:
- path: /
pathType: Prefix
service: web
tls:
- secretName: example-tls
hosts:
- api.example.com
```
### High Resource Environment
```yaml
# high-resources-values.yaml
web:
replicaCount: 5
resources:
limits:
cpu: 2000m
memory: 2Gi
requests:
cpu: 1000m
memory: 1Gi
autoscaling:
enabled: true
maxReplicas: 20
api:
replicaCount: 10
resources:
limits:
cpu: 4000m
memory: 4Gi
requests:
cpu: 2000m
memory: 2Gi
autoscaling:
enabled: true
maxReplicas: 50
podDisruptionBudget:
enabled: true
minAvailable: 3
```
### Custom API7 Gateway
```yaml
# custom-gateway-values.yaml
api7:
gateway:
adminUrl: http://my-api7-gateway:9180
adminKey: "my-custom-key"
group: production
gatewayService: my-gateway-service
backend: apisix # or api7ee
autoPublish: false # Manual publish
```
### LLM Configuration
```yaml
# llm-values.yaml
api:
env:
- name: OPENAI_API_BASE
value: "http://openwebui.ai:8080/api"
- name: OPENAI_API_KEY
valueFrom:
secretKeyRef:
name: llm-secrets
key: api-key
- name: DEFAULT_LLM_MODEL
value: "videogame-expert"
api7:
plugins:
aiRateLimit:
enabled: true
limit: 200 # Higher limit
timeWindow: 60
```
### Development Environment
Use the included `values-dev.yaml`:
```bash
helm install api7ee-dev ./helm/api7ee-demo-k8s \
-f ./helm/api7ee-demo-k8s/values-dev.yaml \
--namespace api7ee-dev
```
## 🔄 Upgrade and Rollback
### Upgrade Release
```bash
# Upgrade with new values
helm upgrade api7ee-demo api7ee/api7ee-demo-k8s \
--namespace api7ee \
-f new-values.yaml
# Upgrade with inline values
helm upgrade api7ee-demo api7ee/api7ee-demo-k8s \
--namespace api7ee \
--reuse-values \
--set api.replicaCount=5
# Force upgrade
helm upgrade api7ee-demo api7ee/api7ee-demo-k8s \
--namespace api7ee \
--force
```
### Rollback
```bash
# View release history
helm history api7ee-demo -n api7ee
# Rollback to previous release
helm rollback api7ee-demo -n api7ee
# Rollback to specific revision
helm rollback api7ee-demo 3 -n api7ee
```
### Verify Upgrade
```bash
# Check release status
helm status api7ee-demo -n api7ee
# View current values
helm get values api7ee-demo -n api7ee
# Check all resources
kubectl get all -n api7ee -l app.kubernetes.io/instance=api7ee-demo
```
## 🗑️ Uninstallation
```bash
# Uninstall release
helm uninstall api7ee-demo --namespace api7ee
# Keep release history (for rollback)
helm uninstall api7ee-demo --namespace api7ee --keep-history
# Delete namespace
kubectl delete namespace api7ee
```
## 🔍 Verification and Testing
### Check Deployment Status
```bash
# Helm release status
helm list -n api7ee
helm status api7ee-demo -n api7ee
# Kubernetes resources
kubectl get all -n api7ee
kubectl get pods -n api7ee -w
# Check specific resources
kubectl get deployments -n api7ee
kubectl get services -n api7ee
kubectl get ingress -n api7ee
kubectl get hpa -n api7ee
```
### View Logs
```bash
# Web service logs
kubectl logs -n api7ee -l app=apache-service --tail=50
# API service logs
kubectl logs -n api7ee -l app=nginx-service --tail=50
# ADC sync job logs
kubectl logs -n api7ee -l app.kubernetes.io/component=adc-sync
# All logs
kubectl logs -n api7ee -l app.kubernetes.io/instance=api7ee-demo --all-containers
```
### Test Endpoints
```bash
# Get ingress URL
INGRESS_HOST=$(kubectl get ingress -n api7ee api7ee-demo-ingress -o jsonpath='{.spec.rules[0].host}')
# Test web service
curl https://$INGRESS_HOST/
# Test API
curl https://$INGRESS_HOST/api/items
# Test health checks
curl https://$INGRESS_HOST/health
curl https://$INGRESS_HOST/api/health
# Test LLM endpoint
curl -X POST https://$INGRESS_HOST/api/llm/chat \
-H "Content-Type: application/json" \
-d '{
"prompt": "What is Zelda?",
"max_tokens": 100
}'
```
## 🔧 Troubleshooting
### Common Issues
#### 1. ADC Sync Job Fails
**Check job logs**:
```bash
kubectl logs -n api7ee -l app.kubernetes.io/component=adc-sync
# Common errors:
# - "connection refused" → Check adminUrl
# - "unauthorized" → Verify adminKey
# - "route not found" → Check backend type (api7ee vs apisix)
```
**Solution**:
```bash
# Update API7 credentials
helm upgrade api7ee-demo api7ee/api7ee-demo-k8s \
--namespace api7ee \
--reuse-values \
--set api7.gateway.adminKey="correct-admin-key"
```
#### 2. Pods Not Starting
**Check pod status**:
```bash
kubectl describe pod -n api7ee <pod-name>
# Common issues:
# - ImagePullBackOff → Check image registry credentials
# - CrashLoopBackOff → Check application logs
# - Pending → Check resources and node capacity
```
**Solution for ImagePullBackOff**:
```bash
# Create registry secret
kubectl create secret docker-registry gitea-registry \
--docker-server=git.commandware.com \
--docker-username=<USERNAME> \
--docker-password=<TOKEN> \
-n api7ee
# Update values
helm upgrade api7ee-demo api7ee/api7ee-demo-k8s \
--namespace api7ee \
--reuse-values \
--set global.imagePullSecrets[0].name=gitea-registry
```
#### 3. Routes Return 404
**Cause**: Routes not published to gateway group
**Check API7 Dashboard**:
```
Services → Select service → Routes → Check publication status
```
**Solution**:
```bash
# Enable auto-publish
helm upgrade api7ee-demo api7ee/api7ee-demo-k8s \
--namespace api7ee \
--reuse-values \
--set api7.autoPublish=true
# Or publish manually via Dashboard:
# Click "Publish" → Select "default" gateway group
```
#### 4. TLS Certificate Issues
**Check certificate status**:
```bash
kubectl get certificate -n api7ee
kubectl describe certificate -n api7ee api7ee-tls
# Check cert-manager logs
kubectl logs -n cert-manager -l app=cert-manager --tail=50
```
**Verify cert-manager configuration**:
```bash
# Check ClusterIssuer
kubectl get clusterissuer cloudflare-acme-prod
# Check challenge
kubectl get challenge -n api7ee
```
#### 5. Service Discovery Not Working
**Check services have named ports**:
```bash
kubectl get svc -n api7ee apache-service -o yaml | grep -A5 ports
# Ports must have 'name' field:
# ports:
# - port: 80
# name: http # ← Required
```
**Check endpoints**:
```bash
kubectl get endpoints -n api7ee
```
#### 6. HPA Not Scaling
**Check HPA status**:
```bash
kubectl get hpa -n api7ee
kubectl describe hpa -n api7ee <hpa-name>
```
**Verify metrics-server**:
```bash
kubectl top nodes
kubectl top pods -n api7ee
# If metrics not available, install metrics-server:
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
```
## 📊 Monitoring
### Prometheus Metrics
If Prometheus is enabled:
```yaml
metrics:
enabled: true
serviceMonitor:
enabled: true
interval: 30s
```
**View metrics**:
```bash
# Check ServiceMonitor
kubectl get servicemonitor -n api7ee
# Query Prometheus
curl http://<prometheus-server>/api/v1/query?query=up{job="api7ee"}
```
### API7 Dashboard
Access API7 Dashboard to view:
- Route traffic and statistics
- Rate limiting metrics
- Service health and upstreams
- Plugin performance
## 🔐 Security
### Security Features
- **Pod Security Context**: Runs as non-root user (UID 1000)
- **Security Context**: Drops all capabilities, prevents privilege escalation
- **Read-only Root Filesystem**: Enabled for both services
- **Network Policies**: Optional network policy support
- **RBAC**: ServiceAccount with minimal permissions
- **Secrets Management**: TLS certificates and API keys stored securely
### Enable Network Policies
```yaml
networkPolicy:
enabled: true
ingress:
- from:
- namespaceSelector:
matchLabels:
name: api7ee
ports:
- protocol: TCP
port: 8000
```
### Secrets Management
**Create secrets before installation**:
```bash
# API7 admin key
kubectl create secret generic api7-admin-key \
--from-literal=adminKey='your-admin-key' \
-n api7ee
# LLM API key
kubectl create secret generic llm-secrets \
--from-literal=api-key='your-llm-api-key' \
-n api7ee
# Image pull secret
kubectl create secret docker-registry gitea-registry \
--docker-server=git.commandware.com \
--docker-username=<user> \
--docker-password=<token> \
-n api7ee
```
**Reference in values**:
```yaml
api7:
gateway:
adminKey: "${API7_ADMIN_KEY}"
api:
env:
- name: OPENAI_API_KEY
valueFrom:
secretKeyRef:
name: llm-secrets
key: api-key
```
## 📚 Chart Structure
```
helm/api7ee-demo-k8s/
├── Chart.yaml # Chart metadata
├── values.yaml # Default values
├── values-dev.yaml # Development overrides
├── values-production.yaml # Production overrides
├── templates/
│ ├── NOTES.txt # Post-install notes
│ ├── _helpers.tpl # Template helpers
│ ├── deployment-web.yaml # Web deployment
│ ├── deployment-api.yaml # API deployment
│ ├── service-web.yaml # Web service
│ ├── service-api.yaml # API service
│ ├── ingress.yaml # Ingress resource
│ ├── configmap.yaml # Application config
│ ├── configmap-adc.yaml # API7 ADC config
│ ├── job-adc-sync.yaml # ADC sync job
│ ├── secret.yaml # Application secrets
│ ├── secret-api7.yaml # API7 secrets
│ ├── certificate.yaml # cert-manager certificate
│ ├── serviceaccount.yaml # ServiceAccount
│ ├── rbac-adc.yaml # RBAC for ADC
│ ├── hpa-web.yaml # Web HPA
│ ├── hpa-api.yaml # API HPA
│ └── poddisruptionbudget.yaml # PDB
└── README.md # This file
```
## 📦 Chart Information
- **Name**: api7ee-demo-k8s
- **Version**: 0.1.0
- **App Version**: 1.0.0
- **Type**: application
- **Keywords**: api7, api-gateway, web, api
- **Home**: https://demo.commandware.it
- **Sources**: https://git.commandware.com/demos/api7-demo
## 📄 Resources
### Documentation
- **Helm**: https://helm.sh/docs/
- **API7 Enterprise**: https://docs.api7.ai/
- **Kubernetes**: https://kubernetes.io/docs/
### Related
- [Main README](../../README.md)
- [Web Application README](../../web/README.md)
- [API Application README](../../api/README.md)
## 🤝 Support
- **Issues**: https://git.commandware.com/demos/api7-demo/issues
- **Email**: support@commandware.com
- **Repository**: https://git.commandware.com/demos/api7-demo
---
**Chart Version**: 0.1.0 | **Maintainer**: CommandWare | **License**: Demo Project

View File

@@ -0,0 +1,58 @@
1. Get the application URL by running these commands:
{{- if .Values.ingress.enabled }}
{{- range $host := .Values.ingress.hosts }}
{{- range .paths }}
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
{{- end }}
{{- end }}
{{- else if contains "NodePort" .Values.web.service.type }}
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "api7ee.fullname" . }}-web)
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
echo http://$NODE_IP:$NODE_PORT
{{- else if contains "LoadBalancer" .Values.web.service.type }}
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "api7ee.fullname" . }}-web'
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "api7ee.fullname" . }}-web --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
echo http://$SERVICE_IP:{{ .Values.web.service.port }}
{{- else if contains "ClusterIP" .Values.web.service.type }}
echo "Visit http://127.0.0.1:8080 to use your application"
kubectl --namespace {{ .Release.Namespace }} port-forward service/{{ include "api7ee.fullname" . }}-web 8080:{{ .Values.web.service.port }}
{{- end }}
2. Check the deployment status:
kubectl get deployments -n {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "api7ee.name" . }},app.kubernetes.io/instance={{ .Release.Name }}"
3. View the pods:
kubectl get pods -n {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "api7ee.name" . }},app.kubernetes.io/instance={{ .Release.Name }}"
4. Check the logs:
# For Web component:
kubectl logs -n {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "api7ee.name" . }},app.kubernetes.io/instance={{ .Release.Name }},app.kubernetes.io/component=web"
# For API component:
kubectl logs -n {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "api7ee.name" . }},app.kubernetes.io/instance={{ .Release.Name }},app.kubernetes.io/component=api"
5. Scale the deployments:
# Scale Web component:
kubectl scale deployment {{ include "api7ee.fullname" . }}-web -n {{ .Release.Namespace }} --replicas=3
# Scale API component:
kubectl scale deployment {{ include "api7ee.fullname" . }}-api -n {{ .Release.Namespace }} --replicas=5
{{- if .Values.web.autoscaling.enabled }}
6. Web Horizontal Pod Autoscaler is enabled:
Min replicas: {{ .Values.web.autoscaling.minReplicas }}
Max replicas: {{ .Values.web.autoscaling.maxReplicas }}
Target CPU: {{ .Values.web.autoscaling.targetCPUUtilizationPercentage }}%
Target Memory: {{ .Values.web.autoscaling.targetMemoryUtilizationPercentage }}%
{{- end }}
{{- if .Values.api.autoscaling.enabled }}
7. API Horizontal Pod Autoscaler is enabled:
Min replicas: {{ .Values.api.autoscaling.minReplicas }}
Max replicas: {{ .Values.api.autoscaling.maxReplicas }}
Target CPU: {{ .Values.api.autoscaling.targetCPUUtilizationPercentage }}%
Target Memory: {{ .Values.api.autoscaling.targetMemoryUtilizationPercentage }}%
{{- end }}

View File

@@ -0,0 +1,62 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "api7ee.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "api7ee.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "api7ee.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "api7ee.labels" -}}
helm.sh/chart: {{ include "api7ee.chart" . }}
{{ include "api7ee.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "api7ee.selectorLabels" -}}
app.kubernetes.io/name: {{ include "api7ee.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "api7ee.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "api7ee.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,30 @@
{{- if and .Values.api7.enabled .Values.api7.tls.enabled .Values.api7.tls.certManager.enabled }}
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: {{ include "api7ee.fullname" . }}-tls
labels:
{{- include "api7ee.labels" . | nindent 4 }}
spec:
secretName: {{ .Values.api7.tls.secretName | default (printf "%s-tls" (include "api7ee.fullname" .)) }}
issuerRef:
name: {{ .Values.api7.tls.certManager.issuer }}
kind: {{ .Values.api7.tls.certManager.issuerKind | default "ClusterIssuer" }}
commonName: {{ first .Values.api7.hosts }}
dnsNames:
{{- range .Values.api7.hosts }}
- {{ . | quote }}
{{- end }}
privateKey:
algorithm: {{ .Values.api7.tls.privateKey.algorithm | default "RSA" }}
encoding: PKCS1
size: {{ .Values.api7.tls.privateKey.size | default 2048 }}
rotationPolicy: {{ .Values.api7.tls.privateKey.rotationPolicy | default "Always" }}
usages:
- digital signature
- key encipherment
- server auth
- client auth
duration: {{ .Values.api7.tls.duration | default "2160h" }}
renewBefore: {{ .Values.api7.tls.renewBefore | default "720h" }}
{{- end }}

View File

@@ -0,0 +1,172 @@
{{- if .Values.api7.enabled }}
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "api7ee.fullname" . }}-adc-config
labels:
{{- include "api7ee.labels" . | nindent 4 }}
app.kubernetes.io/component: adc
data:
adc-config.yaml: |
services:
# Web Frontend Service
- name: {{ include "api7ee.fullname" . }}-web-service
hosts:
- {{ (first .Values.api7.hosts) | quote }}
upstream:
name: {{ include "api7ee.fullname" . }}-web-upstream
scheme: http
type: roundrobin
{{- if .Values.api7.serviceDiscovery.enabled }}
# Use Kubernetes Service Discovery
discovery_type: kubernetes
{{- if .Values.api7.serviceDiscovery.namespace }}
service_name: {{ .Values.api7.serviceDiscovery.namespace }}/{{ include "api7ee.fullname" . }}-web
{{- else }}
service_name: {{ .Release.Namespace }}/{{ include "api7ee.fullname" . }}-web
{{- end }}
{{- else }}
# Static nodes configuration
nodes:
- host: {{ include "api7ee.fullname" . }}-web.{{ .Release.Namespace }}.svc.cluster.local
port: {{ .Values.web.service.port }}
weight: 100
{{- end }}
routes:
# Route for web frontend (all paths except /api)
- name: {{ include "api7ee.fullname" . }}-web-route
uris:
- /*
vars:
- - uri
- "~~"
- "^(?!/api)"
priority: 1
plugins:
{{- if .Values.api7.tls.enabled }}
redirect:
http_to_https: true
{{- end }}
{{- if .Values.api7.plugins.cors.enabled }}
cors:
allow_origins: {{ .Values.api7.plugins.cors.allowOrigins | join "," | quote }}
allow_methods: {{ .Values.api7.plugins.cors.allowMethods | join "," | quote }}
allow_headers: {{ .Values.api7.plugins.cors.allowHeaders | join "," | quote }}
expose_headers: {{ .Values.api7.plugins.cors.exposeHeaders | join "," | quote }}
max_age: {{ .Values.api7.plugins.cors.maxAge }}
allow_credential: {{ .Values.api7.plugins.cors.allowCredentials }}
{{- end }}
# API Backend Service
- name: {{ include "api7ee.fullname" . }}-api-service
hosts:
- {{ (first .Values.api7.hosts) | quote }}
upstream:
name: {{ include "api7ee.fullname" . }}-api-upstream
scheme: http
type: roundrobin
{{- if .Values.api7.serviceDiscovery.enabled }}
# Use Kubernetes Service Discovery
discovery_type: kubernetes
{{- if .Values.api7.serviceDiscovery.namespace }}
service_name: {{ .Values.api7.serviceDiscovery.namespace }}/{{ include "api7ee.fullname" . }}-api
{{- else }}
service_name: {{ .Release.Namespace }}/{{ include "api7ee.fullname" . }}-api
{{- end }}
{{- else }}
# Static nodes configuration
nodes:
- host: {{ include "api7ee.fullname" . }}-api.{{ .Release.Namespace }}.svc.cluster.local
port: {{ .Values.api.service.port }}
weight: 100
{{- end }}
routes:
# High priority route for LLM endpoints with AI rate limiting
- name: {{ include "api7ee.fullname" . }}-api-llm-route
uris:
- /api/llm
- /api/llm/*
priority: 20
plugins:
{{- if .Values.api7.tls.enabled }}
redirect:
http_to_https: true
{{- end }}
# Remove /api prefix before forwarding to backend
proxy-rewrite:
regex_uri:
- "^/api/(.*)"
- "/$1"
{{- if .Values.api7.plugins.cors.enabled }}
cors:
allow_origins: {{ .Values.api7.plugins.cors.allowOrigins | join "," | quote }}
allow_methods: {{ .Values.api7.plugins.cors.allowMethods | join "," | quote }}
allow_headers: {{ .Values.api7.plugins.cors.allowHeaders | join "," | quote }}
expose_headers: {{ .Values.api7.plugins.cors.exposeHeaders | join "," | quote }}
max_age: {{ .Values.api7.plugins.cors.maxAge }}
allow_credential: {{ .Values.api7.plugins.cors.allowCredentials }}
{{- end }}
{{- if .Values.api7.plugins.aiRateLimit.enabled }}
ai-rate-limiting:
limit: {{ .Values.api7.plugins.aiRateLimit.limit }}
time_window: {{ .Values.api7.plugins.aiRateLimit.timeWindow }}
rejected_code: {{ .Values.api7.plugins.aiRateLimit.rejectedCode }}
limit_strategy: {{ .Values.api7.plugins.aiRateLimit.limitStrategy | quote }}
{{- end }}
# Standard API route with request rate limiting
- name: {{ include "api7ee.fullname" . }}-api-route
uris:
- /api
- /api/*
priority: 10
plugins:
{{- if .Values.api7.tls.enabled }}
redirect:
http_to_https: true
{{- end }}
# Remove /api prefix before forwarding to backend
proxy-rewrite:
regex_uri:
- "^/api/(.*)"
- "/$1"
{{- if .Values.api7.plugins.cors.enabled }}
cors:
allow_origins: {{ .Values.api7.plugins.cors.allowOrigins | join "," | quote }}
allow_methods: {{ .Values.api7.plugins.cors.allowMethods | join "," | quote }}
allow_headers: {{ .Values.api7.plugins.cors.allowHeaders | join "," | quote }}
expose_headers: {{ .Values.api7.plugins.cors.exposeHeaders | join "," | quote }}
max_age: {{ .Values.api7.plugins.cors.maxAge }}
allow_credential: {{ .Values.api7.plugins.cors.allowCredentials }}
{{- end }}
{{- if .Values.api7.plugins.rateLimit.enabled }}
limit-count:
count: {{ .Values.api7.plugins.rateLimit.count }}
time_window: {{ .Values.api7.plugins.rateLimit.timeWindow }}
rejected_code: {{ .Values.api7.plugins.rateLimit.rejectedCode }}
key_type: {{ .Values.api7.plugins.rateLimit.keyType | quote }}
key: {{ .Values.api7.plugins.rateLimit.key | quote }}
{{- end }}
{{- if .Values.api7.plugins.auth.enabled }}
# API Consumers for authentication
consumers:
{{- range .Values.api7.consumers }}
- username: {{ .username }}
plugins:
key-auth:
key: {{ .apiKey }}
{{- end }}
{{- end }}
{{- if .Values.api7.plugins.logging.enabled }}
# Global Rules
global_rules:
request-logging:
plugins:
http-logger:
uri: {{ .Values.api7.plugins.logging.endpoint }}
batch_max_size: {{ .Values.api7.plugins.logging.batchMaxSize | default 1000 }}
inactive_timeout: {{ .Values.api7.plugins.logging.inactiveTimeout | default 5 }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,10 @@
{{- if .Values.configMap.data }}
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "api7ee.fullname" . }}
labels:
{{- include "api7ee.labels" . | nindent 4 }}
data:
{{- toYaml .Values.configMap.data | nindent 2 }}
{{- end }}

View File

@@ -0,0 +1,81 @@
{{- if .Values.api.enabled }}
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "api7ee.fullname" . }}-api
labels:
{{- include "api7ee.labels" . | nindent 4 }}
app.kubernetes.io/component: api
spec:
{{- if not .Values.api.autoscaling.enabled }}
replicas: {{ .Values.api.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "api7ee.selectorLabels" . | nindent 6 }}
app.kubernetes.io/component: api
template:
metadata:
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
labels:
{{- include "api7ee.selectorLabels" . | nindent 8 }}
app.kubernetes.io/component: api
spec:
{{- with .Values.global.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "api7ee.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: api
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.global.imageRegistry | default .Values.api.image.registry }}/{{ .Values.api.image.repository }}:{{ .Values.api.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.api.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.api.service.targetPort }}
protocol: TCP
{{- if and .Values.api.healthProbes.enabled .Values.api.livenessProbe.enabled }}
livenessProbe:
{{- omit .Values.api.livenessProbe "enabled" | toYaml | nindent 12 }}
{{- end }}
{{- if and .Values.api.healthProbes.enabled .Values.api.readinessProbe.enabled }}
readinessProbe:
{{- omit .Values.api.readinessProbe "enabled" | toYaml | nindent 12 }}
{{- end }}
resources:
{{- toYaml .Values.api.resources | nindent 12 }}
env:
- name: PORT
value: "{{ .Values.api.service.targetPort }}"
{{- with .Values.api.env }}
{{- toYaml . | nindent 12 }}
{{- end }}
{{- if .Values.configMap.data }}
envFrom:
- configMapRef:
name: {{ include "api7ee.fullname" . }}
{{- end }}
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
{{- with .Values.api.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.api.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.api.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,81 @@
{{- if .Values.web.enabled }}
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "api7ee.fullname" . }}-web
labels:
{{- include "api7ee.labels" . | nindent 4 }}
app.kubernetes.io/component: web
spec:
{{- if not .Values.web.autoscaling.enabled }}
replicas: {{ .Values.web.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "api7ee.selectorLabels" . | nindent 6 }}
app.kubernetes.io/component: web
template:
metadata:
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
labels:
{{- include "api7ee.selectorLabels" . | nindent 8 }}
app.kubernetes.io/component: web
spec:
{{- with .Values.global.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "api7ee.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: web
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.global.imageRegistry | default .Values.web.image.registry }}/{{ .Values.web.image.repository }}:{{ .Values.web.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.web.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.web.service.targetPort }}
protocol: TCP
{{- if and .Values.web.healthProbes.enabled .Values.web.livenessProbe.enabled }}
livenessProbe:
{{- omit .Values.web.livenessProbe "enabled" | toYaml | nindent 12 }}
{{- end }}
{{- if and .Values.web.healthProbes.enabled .Values.web.readinessProbe.enabled }}
readinessProbe:
{{- omit .Values.web.readinessProbe "enabled" | toYaml | nindent 12 }}
{{- end }}
resources:
{{- toYaml .Values.web.resources | nindent 12 }}
env:
- name: PORT
value: "{{ .Values.web.service.targetPort }}"
{{- with .Values.web.env }}
{{- toYaml . | nindent 12 }}
{{- end }}
{{- if .Values.configMap.data }}
envFrom:
- configMapRef:
name: {{ include "api7ee.fullname" . }}
{{- end }}
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
{{- with .Values.web.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.web.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.web.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,33 @@
{{- if and .Values.api.enabled .Values.api.autoscaling.enabled }}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "api7ee.fullname" . }}-api
labels:
{{- include "api7ee.labels" . | nindent 4 }}
app.kubernetes.io/component: api
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ include "api7ee.fullname" . }}-api
minReplicas: {{ .Values.api.autoscaling.minReplicas }}
maxReplicas: {{ .Values.api.autoscaling.maxReplicas }}
metrics:
{{- if .Values.api.autoscaling.targetCPUUtilizationPercentage }}
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: {{ .Values.api.autoscaling.targetCPUUtilizationPercentage }}
{{- end }}
{{- if .Values.api.autoscaling.targetMemoryUtilizationPercentage }}
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: {{ .Values.api.autoscaling.targetMemoryUtilizationPercentage }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,33 @@
{{- if and .Values.web.enabled .Values.web.autoscaling.enabled }}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "api7ee.fullname" . }}-web
labels:
{{- include "api7ee.labels" . | nindent 4 }}
app.kubernetes.io/component: web
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ include "api7ee.fullname" . }}-web
minReplicas: {{ .Values.web.autoscaling.minReplicas }}
maxReplicas: {{ .Values.web.autoscaling.maxReplicas }}
metrics:
{{- if .Values.web.autoscaling.targetCPUUtilizationPercentage }}
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: {{ .Values.web.autoscaling.targetCPUUtilizationPercentage }}
{{- end }}
{{- if .Values.web.autoscaling.targetMemoryUtilizationPercentage }}
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: {{ .Values.web.autoscaling.targetMemoryUtilizationPercentage }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,59 @@
{{- if .Values.ingress.enabled -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "api7ee.fullname" . }}
labels:
{{- include "api7ee.labels" . | nindent 4 }}
annotations:
{{- if .Values.ingress.dnsScope }}
dns-scope: {{ .Values.ingress.dnsScope | quote }}
{{- end }}
{{- with .Values.ingress.annotations }}
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if .Values.ingress.className }}
ingressClassName: {{ .Values.ingress.className }}
{{- end }}
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
pathType: {{ .pathType }}
backend:
{{- if .gateway }}
{{- /* Use API7 Gateway as backend */}}
service:
name: {{ .gateway.serviceName }}
port:
number: {{ .gateway.port | default 80 }}
{{- else if .service }}
{{- /* Legacy: Use application service as backend */}}
service:
{{- if eq .service "web" }}
name: {{ include "api7ee.fullname" $ }}-web
port:
number: {{ $.Values.web.service.port }}
{{- else if eq .service "api" }}
name: {{ include "api7ee.fullname" $ }}-api
port:
number: {{ $.Values.api.service.port }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,74 @@
{{- if .Values.api7.enabled }}
apiVersion: batch/v1
kind: Job
metadata:
name: {{ include "api7ee.fullname" . }}-adc-sync
labels:
{{- include "api7ee.labels" . | nindent 4 }}
app.kubernetes.io/component: adc-sync
annotations:
"helm.sh/hook": post-install,post-upgrade
"helm.sh/hook-weight": "10"
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
backoffLimit: 3
activeDeadlineSeconds: 300
template:
metadata:
labels:
{{- include "api7ee.selectorLabels" . | nindent 8 }}
app.kubernetes.io/component: adc-sync
spec:
restartPolicy: Never
serviceAccountName: {{ include "api7ee.serviceAccountName" . }}
containers:
- name: adc-sync
image: ghcr.io/api7/adc:latest
imagePullPolicy: IfNotPresent
args:
- sync
- -f
- /config/adc-config.yaml
- --backend
- {{ .Values.api7.backend | default "api7ee" }}
- --server
- $(API7_ADMIN_URL)
- --token
- $(API7_ADMIN_KEY)
- --gateway-group
- $(API7_GATEWAY_GROUP)
{{- if .Values.api7.adc.tlsSkipVerify }}
- --tls-skip-verify
{{- end }}
{{- if .Values.api7.adc.verbose }}
- --verbose
{{- end }}
env:
- name: API7_ADMIN_URL
valueFrom:
secretKeyRef:
name: {{ .Values.api7.gateway.existingSecret | default (printf "%s-api7-admin" (include "api7ee.fullname" .)) }}
key: {{ .Values.api7.gateway.existingSecretKeys.adminUrl | default "admin-url" }}
- name: API7_ADMIN_KEY
valueFrom:
secretKeyRef:
name: {{ .Values.api7.gateway.existingSecret | default (printf "%s-api7-admin" (include "api7ee.fullname" .)) }}
key: {{ .Values.api7.gateway.existingSecretKeys.adminKey | default "admin-key" }}
- name: API7_GATEWAY_GROUP
valueFrom:
secretKeyRef:
name: {{ .Values.api7.gateway.existingSecret | default (printf "%s-api7-admin" (include "api7ee.fullname" .)) }}
key: {{ .Values.api7.gateway.existingSecretKeys.group | default "gateway-group" }}
volumeMounts:
- name: adc-config
mountPath: /config
readOnly: true
{{- if .Values.api7.adc.resources }}
resources:
{{- toYaml .Values.api7.adc.resources | nindent 12 }}
{{- end }}
volumes:
- name: adc-config
configMap:
name: {{ include "api7ee.fullname" . }}-adc-config
{{- end }}

View File

@@ -0,0 +1,40 @@
{{- if .Values.podDisruptionBudget.enabled }}
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: {{ include "api7ee.fullname" . }}-web
labels:
{{- include "api7ee.labels" . | nindent 4 }}
app.kubernetes.io/component: web
spec:
{{- if .Values.podDisruptionBudget.minAvailable }}
minAvailable: {{ .Values.podDisruptionBudget.minAvailable }}
{{- end }}
{{- if .Values.podDisruptionBudget.maxUnavailable }}
maxUnavailable: {{ .Values.podDisruptionBudget.maxUnavailable }}
{{- end }}
selector:
matchLabels:
{{- include "api7ee.selectorLabels" . | nindent 6 }}
app.kubernetes.io/component: web
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: {{ include "api7ee.fullname" . }}-api
labels:
{{- include "api7ee.labels" . | nindent 4 }}
app.kubernetes.io/component: api
spec:
{{- if .Values.podDisruptionBudget.minAvailable }}
minAvailable: {{ .Values.podDisruptionBudget.minAvailable }}
{{- end }}
{{- if .Values.podDisruptionBudget.maxUnavailable }}
maxUnavailable: {{ .Values.podDisruptionBudget.maxUnavailable }}
{{- end }}
selector:
matchLabels:
{{- include "api7ee.selectorLabels" . | nindent 6 }}
app.kubernetes.io/component: api
{{- end }}

View File

@@ -0,0 +1,36 @@
{{- if and .Values.api7.enabled .Values.serviceAccount.create }}
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: {{ include "api7ee.fullname" . }}-adc
labels:
{{- include "api7ee.labels" . | nindent 4 }}
rules:
# Allow reading secrets (for certificates)
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list"]
# Allow reading services and endpoints for service discovery
- apiGroups: [""]
resources: ["services", "endpoints"]
verbs: ["get", "list", "watch"]
# Allow reading pods for health checks
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: {{ include "api7ee.fullname" . }}-adc
labels:
{{- include "api7ee.labels" . | nindent 4 }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: {{ include "api7ee.fullname" . }}-adc
subjects:
- kind: ServiceAccount
name: {{ include "api7ee.serviceAccountName" . }}
namespace: {{ .Release.Namespace }}
{{- end }}

View File

@@ -0,0 +1,14 @@
{{- if and .Values.api7.enabled (not .Values.api7.gateway.existingSecret) }}
apiVersion: v1
kind: Secret
metadata:
name: {{ include "api7ee.fullname" . }}-api7-admin
labels:
{{- include "api7ee.labels" . | nindent 4 }}
app.kubernetes.io/component: api7
type: Opaque
stringData:
admin-key: {{ .Values.api7.gateway.adminKey | quote }}
admin-url: {{ .Values.api7.gateway.adminUrl | quote }}
gateway-group: {{ .Values.api7.gateway.group | default "default" | quote }}
{{- end }}

View File

@@ -0,0 +1,13 @@
{{- if .Values.secrets.create }}
apiVersion: v1
kind: Secret
metadata:
name: {{ include "api7ee.fullname" . }}
labels:
{{- include "api7ee.labels" . | nindent 4 }}
type: Opaque
data:
{{- range $key, $val := .Values.secrets.data }}
{{ $key }}: {{ $val | b64enc | quote }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,23 @@
{{- if .Values.api.enabled }}
apiVersion: v1
kind: Service
metadata:
name: {{ include "api7ee.fullname" . }}-api
labels:
{{- include "api7ee.labels" . | nindent 4 }}
app.kubernetes.io/component: api
{{- with .Values.api.service.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
type: {{ .Values.api.service.type }}
ports:
- port: {{ .Values.api.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "api7ee.selectorLabels" . | nindent 4 }}
app.kubernetes.io/component: api
{{- end }}

View File

@@ -0,0 +1,23 @@
{{- if .Values.web.enabled }}
apiVersion: v1
kind: Service
metadata:
name: {{ include "api7ee.fullname" . }}-web
labels:
{{- include "api7ee.labels" . | nindent 4 }}
app.kubernetes.io/component: web
{{- with .Values.web.service.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
type: {{ .Values.web.service.type }}
ports:
- port: {{ .Values.web.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "api7ee.selectorLabels" . | nindent 4 }}
app.kubernetes.io/component: web
{{- end }}

View File

@@ -0,0 +1,12 @@
{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "api7ee.serviceAccountName" . }}
labels:
{{- include "api7ee.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,80 @@
# Development environment values for api7ee
# This file contains development-specific configuration overrides
web:
replicaCount: 1
image:
tag: "latest"
pullPolicy: Always
resources:
limits:
cpu: 250m
memory: 256Mi
requests:
cpu: 100m
memory: 128Mi
autoscaling:
enabled: false
api:
replicaCount: 1
image:
tag: "latest"
pullPolicy: Always
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 250m
memory: 256Mi
autoscaling:
enabled: false
env:
- name: LOG_LEVEL
value: "debug"
- name: ENVIRONMENT
value: "development"
- name: DEBUG
value: "true"
ingress:
enabled: false # Use port-forward in dev
podDisruptionBudget:
enabled: false
# Disable security features for easier debugging
podSecurityContext:
runAsNonRoot: false
runAsUser: 0
fsGroup: 0
securityContext:
allowPrivilegeEscalation: true
readOnlyRootFilesystem: false
runAsNonRoot: false
runAsUser: 0
# API7 configuration for development
api7:
enabled: false # Disable API7 ADC in dev to simplify local testing
gateway:
adminUrl: http://api7ee3-0-1759339083-dp-manager.api7ee.svc.cluster.local:7900
adminKey: "dev-admin-key" # Use different key for dev
hosts:
- api7-demo-dev.commandware.it
tls:
enabled: false # No TLS in dev
plugins:
rateLimit:
enabled: false # No rate limiting in dev
auth:
enabled: false # No auth in dev for easier testing

View File

@@ -0,0 +1,106 @@
# Production environment values for api7ee
# This file contains production-specific configuration overrides
global:
imageRegistry: "git.commandware.com"
imagePullSecrets:
- name: registry-secret
web:
replicaCount: 3
image:
tag: "v1.0.0" # Use specific version in production
pullPolicy: Always
resources:
limits:
cpu: 1000m
memory: 1Gi
requests:
cpu: 500m
memory: 512Mi
autoscaling:
enabled: true
minReplicas: 3
maxReplicas: 15
api:
replicaCount: 5
image:
tag: "v1.0.0" # Use specific version in production
pullPolicy: Always
resources:
limits:
cpu: 2000m
memory: 2Gi
requests:
cpu: 1000m
memory: 1Gi
autoscaling:
enabled: true
minReplicas: 5
maxReplicas: 30
env:
- name: LOG_LEVEL
value: "warn"
- name: ENVIRONMENT
value: "production"
ingress:
annotations:
nginx.ingress.kubernetes.io/rate-limit: "100"
nginx.ingress.kubernetes.io/ssl-protocols: "TLSv1.2 TLSv1.3"
nginx.ingress.kubernetes.io/ssl-ciphers: "HIGH:!aNULL:!MD5"
podDisruptionBudget:
enabled: true
minAvailable: 2
metrics:
enabled: true
serviceMonitor:
enabled: true
interval: 15s
networkPolicy:
enabled: true
# API7 configuration for production
api7:
enabled: true
gateway:
adminUrl: http://api7ee3-0-1759339083-dp-manager.api7ee.svc.cluster.local:7900
adminKey: "${API7_ADMIN_KEY}" # Should be provided via secret in production
gatewayService: gateway-0-1759393614-gateway
hosts:
- commandware.it
- api7-demo.commandware.com # Additional production domain
tls:
enabled: true
certManager:
enabled: true
issuer: cloudflare-acme-prod
plugins:
rateLimit:
enabled: true
count: 1000 # Higher limits for production
timeWindow: 60
apiCount: 10000 # Much higher for API endpoints
cors:
enabled: true
allowOrigins:
["https://commandware.it", "https://api7-demo.commandware.com"]
allowCredentials: true
auth:
enabled: true # Enable auth in production
prometheus:
enabled: true
logging:
enabled: true
endpoint: http://logging-service.monitoring:8080/logs

View File

@@ -0,0 +1,445 @@
# Default values for api7ee.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
global:
# Global image registry to use for all images
imageRegistry: ""
# Image pull secrets for all images
imagePullSecrets: []
# Configuration for the Web component
web:
enabled: false # Disabled when using API7 Gateway routing
replicaCount: 2
image:
registry: gitea.server_url # Will be replaced with actual Gitea URL
repository: gitea.repository/web # Will be replaced with actual repository path
pullPolicy: IfNotPresent
tag: "main" # Override with specific version
service:
type: ClusterIP
port: 8000
targetPort: 8000
annotations: {}
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 250m
memory: 256Mi
autoscaling:
enabled: false
minReplicas: 2
maxReplicas: 10
targetCPUUtilizationPercentage: 80
targetMemoryUtilizationPercentage: 80
nodeSelector: {}
tolerations: []
affinity: {}
# Additional environment variables
env: []
# Liveness and readiness probes
healthProbes:
enabled: true # Set to false to disable both probes
livenessProbe:
enabled: true # Set to false to disable liveness probe
httpGet:
path: /docs
port: http
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
enabled: true # Set to false to disable readiness probe
httpGet:
path: /docs
port: http
initialDelaySeconds: 10
periodSeconds: 5
# Configuration for the API component
api:
enabled: false # Disabled when using API7 Gateway routing
replicaCount: 3
image:
registry: gitea.server_url # Will be replaced with actual Gitea URL
repository: gitea.repository/api # Will be replaced with actual repository path
pullPolicy: IfNotPresent
tag: "main" # Override with specific version
service:
type: ClusterIP
port: 8080
targetPort: 8080
annotations: {}
resources:
limits:
cpu: 1000m
memory: 1Gi
requests:
cpu: 500m
memory: 512Mi
autoscaling:
enabled: true
minReplicas: 3
maxReplicas: 20
targetCPUUtilizationPercentage: 70
targetMemoryUtilizationPercentage: 75
nodeSelector: {}
tolerations: []
affinity: {}
# Additional environment variables
env:
- name: LOG_LEVEL
value: "info"
# Liveness and readiness probes
healthProbes:
enabled: true # Set to false to disable both probes
livenessProbe:
enabled: true # Set to false to disable liveness probe
httpGet:
path: /health
port: http
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
enabled: true # Set to false to disable readiness probe
httpGet:
path: /ready
port: http
initialDelaySeconds: 10
periodSeconds: 5
# Ingress Configuration
# Routes external traffic to the API7 Gateway, which then applies routing rules,
# plugins (rate limiting, CORS, authentication), and forwards to backend services
ingress:
enabled: true
className: "nginx" # Ingress controller class (nginx, traefik, etc.)
annotations:
cert-manager.io/cluster-issuer: "cloudflare-acme-prod"
# Add custom annotations as needed:
# nginx.ingress.kubernetes.io/proxy-body-size: "10m"
# nginx.ingress.kubernetes.io/proxy-connect-timeout: "600"
# Gateway routing configuration
# All traffic is routed through API7 Gateway for advanced features:
# - Dynamic routing based on ADC configuration
# - Rate limiting (standard and AI-based)
# - CORS policies
# - Authentication/Authorization
# - Request/Response transformation
hosts:
- host: commandware.it
paths:
- path: /
pathType: Prefix
# Route to API7 Gateway (recommended for production)
gateway:
serviceName: gateway-0-1759393614-gateway # API7 Gateway service name
port: 80 # Gateway HTTP port (443 for HTTPS)
namespace: api7ee # Gateway service namespace
# Direct service routing (legacy, not recommended)
# Only use this if you need to bypass API7 Gateway
# - path: /
# pathType: Prefix
# service: web # Routes directly to web service
# - path: /api
# pathType: Prefix
# service: api # Routes directly to API service
# TLS/SSL Configuration
tls:
- secretName: api7ee-tls # Certificate secret name (created by cert-manager)
hosts:
- commandware.it
# ServiceAccount configuration
serviceAccount:
create: true
annotations: {}
name: ""
# Pod Security Context
podSecurityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 1000
# Security Context for containers
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 1000
# Network Policies
networkPolicy:
enabled: false
policyTypes:
- Ingress
- Egress
ingress: []
egress: []
# Pod Disruption Budget
podDisruptionBudget:
enabled: true
minAvailable: 1
# maxUnavailable: 1
# Monitoring and metrics
metrics:
enabled: false
serviceMonitor:
enabled: false
interval: 30s
path: /metrics
labels: {}
# ConfigMap for shared configuration
configMap:
data: {}
# Secrets for sensitive data
secrets:
create: false
data: {}
# ============================================================================
# API7 Gateway Configuration
# ============================================================================
# API7 Enterprise provides advanced API Gateway features including:
# - Dynamic routing with service discovery
# - Rate limiting (standard and AI-based for LLM endpoints)
# - CORS policies
# - Authentication/Authorization
# - Request/Response transformation
# - Observability and metrics
api7:
enabled: true # Enable API7 ADC (API Declarative Configuration) sync
# ADC Container Settings
# ADC syncs declarative configuration from ConfigMap to API7 Gateway
adc:
image: ghcr.io/api7/adc:latest # ADC container image
imagePullPolicy: IfNotPresent
verbose: true # Enable verbose logging for debugging
tlsSkipVerify: true # Skip TLS verification (required for self-signed dashboard certificates)
# Resources for ADC sync job
resources:
limits:
cpu: 500m
memory: 256Mi
requests:
cpu: 100m
memory: 128Mi
# API7 Gateway Connection Settings
gateway:
# ========================================================================
# Secret Configuration
# ========================================================================
# Option 1: Use existing Secret (recommended for production)
# Provide the name of an existing Kubernetes Secret containing credentials.
# This is useful when using External Secrets Operator, Sealed Secrets,
# or manually managed secrets for better security.
#
# The secret must contain these keys (or customize with existingSecretKeys):
# - admin-url: Dashboard admin API URL
# - admin-key: Dashboard admin API key
# - gateway-group: Gateway group name
#
# Example: Create secret manually:
# kubectl create secret generic api7-credentials \
# --from-literal=admin-url=https://api7ee3-0-1759339083-dashboard:7443 \
# --from-literal=admin-key=YOUR_ADMIN_KEY \
# --from-literal=gateway-group=default \
# -n api7ee
existingSecret: "" # Name of existing secret (leave empty to create new secret)
# Custom key names for existing secret (optional)
# Only needed if your existing secret uses different key names
existingSecretKeys:
adminUrl: admin-url # Key name for admin URL
adminKey: admin-key # Key name for admin key
group: gateway-group # Key name for gateway group
# ========================================================================
# Option 2: Inline Configuration (for development only)
# ========================================================================
# When existingSecret is empty, a Secret will be created with these values.
# NOT RECOMMENDED for production - use existingSecret instead!
# Dashboard Admin API URL (HTTPS required for API7 Enterprise)
# The dashboard service exposes the admin API on port 7443
adminUrl: https://api7ee3-0-1759339083-dashboard:7443
# Admin API key (CHANGE THIS IN PRODUCTION!)
# Obtain from: kubectl get secret -n api7ee api7ee3-0-1759339083 -o jsonpath='{.data.admin_key}' | base64 -d
adminKey: ""
# Gateway group name (logical grouping of gateway instances)
group: default
# ========================================================================
# Gateway Service Configuration
# ========================================================================
# Gateway service name (for traffic routing)
# This is the Kubernetes service that routes traffic to APISIX data plane
gatewayService: gateway-0-1759393614-gateway
gatewayNamespace: api7ee # Gateway service namespace
# Backend Type
# - api7ee: API7 Enterprise (includes all enterprise features)
# - apisix: Open source APISIX (limited features)
backend: api7ee
# Auto-publish Routes
# When true, routes are automatically published after ADC sync
# When false, routes must be manually published via dashboard
autoPublish: true
# Domain Hosts
# List of domains that API7 Gateway will handle
# Must match Ingress hosts for proper routing
hosts:
- commandware.it
# TLS/SSL Configuration
tls:
enabled: true # Enable HTTPS for API7 Gateway
# Option 1: Use cert-manager (Recommended)
# Automatically provisions and renews certificates
certManager:
enabled: true
issuer: cloudflare-acme-prod # ClusterIssuer/Issuer name
issuerKind: ClusterIssuer # ClusterIssuer or Issuer
# Private key settings
privateKey:
rotationPolicy: Always # Always or Never (cert-manager >= v1.18.0)
algorithm: RSA # RSA or ECDSA
size: 2048 # Key size in bits
# Certificate lifetime
duration: 2160h # 90 days
renewBefore: 720h # Renew 30 days before expiry
# Option 2: Use existing TLS secret
secretName: "" # Leave empty to auto-generate name
# Option 3: Provide certificates directly (NOT recommended for production)
certificate: ""
key: ""
# ============================================================================
# Service Discovery Configuration
# ============================================================================
# When enabled, API7 Gateway dynamically discovers backend Pods through
# Kubernetes API instead of using static upstream node configuration.
#
# Benefits:
# - Automatic scaling: New Pods are automatically added to upstream pool
# - Health checks: Only healthy/ready Pods receive traffic
# - Zero downtime: Seamless updates during deployments and rollouts
# - No manual configuration: Eliminates need to specify Pod IPs/hostnames
#
# Requirements:
# - RBAC permissions for services, endpoints (already configured in rbac-adc.yaml)
# - Service must exist in Kubernetes
serviceDiscovery:
enabled: false # Disabled: requires Service Registry configuration in API7 Dashboard
namespace: "" # Leave empty to use release namespace
# ============================================================================
# API7 Plugins Configuration
# ============================================================================
# Plugins provide advanced features like rate limiting, CORS, auth, etc.
# Each plugin can be enabled/disabled and configured independently
plugins:
# Standard Rate Limiting
# Applied to /api routes (except /api/llm)
# Limits requests per IP address
rateLimit:
enabled: true
count: 100 # Max requests per time window
timeWindow: 60 # Time window in seconds
rejectedCode: 429 # HTTP status code for rejected requests
keyType: "var" # Key type: "var", "var_combination", "constant"
key: "remote_addr" # Variable name for key (client IP)
# AI Rate Limiting
# Applied to /api/llm routes
# Specialized rate limiting for LLM/AI endpoints based on token usage
aiRateLimit:
enabled: true
limit: 100 # Max tokens per time window
timeWindow: 60 # Time window in seconds
rejectedCode: 429 # HTTP status code
limitStrategy: "total_tokens" # Strategy: "total_tokens", "input_tokens", "output_tokens"
# CORS (Cross-Origin Resource Sharing)
# Enables browser-based applications to access the API
cors:
enabled: true
allowOrigins: ["*"] # Allowed origins (use specific domains in production)
allowMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD", "PATCH"]
allowHeaders: ["*"] # Allowed headers
exposeHeaders: ["*"] # Headers exposed to browser
maxAge: 3600 # Preflight cache duration (seconds)
allowCredentials: false # Allow credentials (cookies, auth headers)
# Authentication
# Key-based authentication for API access
auth:
enabled: false # Enable to require API keys
header: X-API-Key # Header name for API key
# Prometheus Metrics
# Exposes metrics for monitoring and observability
prometheus:
enabled: true
# Metrics endpoint: http://<gateway>:9091/apisix/prometheus/metrics
# Request Logging
# Sends request logs to external logging service
logging:
enabled: false # Enable to send logs to external service
endpoint: http://logging-service:8080/logs # Logging service URL
batchMaxSize: 1000 # Max batch size before sending
inactiveTimeout: 5 # Max wait time (seconds) before sending batch
# ============================================================================
# API Consumers
# ============================================================================
# Consumers represent API clients with authentication credentials
# Used with auth plugin (when auth.enabled: true)
consumers:
- username: demo-user
apiKey: demo-key-12345 # Change in production!
- username: admin
apiKey: admin-key-67890 # Change in production!

12
web/.env.example Normal file
View File

@@ -0,0 +1,12 @@
# API Backend Configuration
# Set this to the base URL where the API service is running
# Local development
API_BASE_URL=http://localhost:8001
# Production
# API_BASE_URL=https://commandware.it/api
# Other examples
# API_BASE_URL=http://api:8001
# API_BASE_URL=http://192.168.1.100:8001

View File

@@ -10,10 +10,13 @@ RUN pip install --no-cache-dir -r requirements.txt
# Copy application and documentation
COPY main.py .
COPY templates/ ./templates/
COPY static/ ./static/
COPY docs/ ./docs/
COPY mkdocs.yml .
# Build documentation during image build
RUN mkdocs build -f docs/mkdocs.yml -d site
RUN mkdocs build -d site
# Expose port
EXPOSE 8000

View File

@@ -1,53 +1,96 @@
# Web Application with Documentation
# Web Application - FastAPI Frontend with Documentation
FastAPI web application serving a demo dashboard and comprehensive MkDocs documentation.
A modern web frontend built with FastAPI featuring an interactive dashboard, embedded MkDocs documentation, and API proxy functionality for seamless integration with the backend API service.
## Features
## 📋 Overview
- **Web Dashboard**: Simple HTML interface with metrics display
- **Documentation Site**: Complete API7 Enterprise setup guide at `/docs`
- **Health Checks**: Monitoring endpoint at `/health`
This web application serves as the frontend for the API7 Enterprise Edition demo platform, providing:
## Documentation
- **Interactive Dashboard**: Modern UI with real-time statistics
- **Embedded Documentation**: Complete MkDocs documentation site at `/docs`
- **API Proxy**: Seamless proxying of requests to the backend API
- **LLM Chat Interface**: Interactive chat with AI videogame expert
- **Health Monitoring**: Health check endpoints
- **Static Assets**: Custom CSS and JavaScript for enhanced UX
The application includes comprehensive documentation built with MkDocs Material theme, covering:
## ✨ Features
- **Architecture Overview**: Complete infrastructure and component details
- **Getting Started**: Quick setup and deployment guide
- **API7 Configuration**: Route and service configuration with examples
### Web Pages
- **Home Page** (`/`): Dashboard with feature overview and statistics
- **Items Page** (`/items`): Browse and manage items
- **Users Page** (`/users`): View and manage users
- **LLM Chat** (`/llm`): Interactive AI chat interface
- **Documentation** (`/docs`): Full project documentation (MkDocs)
### API Proxy Endpoints
The web application proxies the following API endpoints:
- `GET /api/items` → Backend `/items`
- `GET /api/items/{item_id}` → Backend `/items/{item_id}`
- `GET /api/users` → Backend `/users`
- `GET /api/users/{user_id}` → Backend `/users/{user_id}`
- `POST /api/llm/chat` → Backend `/llm/chat`
- `GET /api/llm/models` → Backend `/llm/models`
- `GET /api/llm/health` → Backend `/llm/health`
- `GET /api/config` → Returns frontend configuration
### Documentation Site
The embedded MkDocs documentation includes:
- **Getting Started**: Quick setup guide
- **Architecture**: System architecture and component details
- **Kubernetes Resources**: Complete resource reference
- **CI/CD Pipeline**: Gitea Actions workflow documentation
- **API7 Configuration**: Gateway setup and configuration
- **CI/CD Pipeline**: Automation and deployment workflows
- **Troubleshooting**: Common issues and solutions
## Running Locally
## 🚀 Quick Start
### Prerequisites
```bash
python >= 3.8
pip
```
### Local Development
#### Install Dependencies
```bash
cd web
pip install -r requirements.txt
```
### Start Application
#### Run the Application
```bash
# Basic run (connects to localhost:8001 by default)
python main.py
# With custom API backend URL
export API_BASE_URL="http://localhost:8001"
python main.py
# Or use uvicorn directly
uvicorn main:app --reload --host 0.0.0.0 --port 8000
```
**Access**:
#### Access the Application
- Main Page: http://localhost:8000
- Documentation: http://localhost:8000/docs/
- Health Check: http://localhost:8000/health
- **Home**: http://localhost:8000
- **Items**: http://localhost:8000/items
- **Users**: http://localhost:8000/users
- **LLM Chat**: http://localhost:8000/llm
- **Documentation**: http://localhost:8000/docs
- **Health Check**: http://localhost:8000/health
### Build Documentation Manually
### Docker
```bash
mkdocs build -f docs/mkdocs.yml -d site
```
## Docker
### Build Image
#### Build Image
```bash
docker build -t web-app .
@@ -56,23 +99,379 @@ docker build -t web-app .
The Dockerfile:
1. Installs Python dependencies
2. Copies application and documentation source
3. Builds static documentation site with MkDocs
4. Serves both the app and docs
2. Copies application code, templates, static assets, and docs
3. Builds static documentation with MkDocs
4. Serves both the app and documentation
### Run Container
#### Run Container
```bash
# Basic run
docker run -p 8000:8000 web-app
# With custom API backend URL
docker run -p 8000:8000 \
-e API_BASE_URL="http://api-backend:8001" \
web-app
# Run with host network (for local testing)
docker run --network host \
-e API_BASE_URL="http://localhost:8001" \
web-app
```
## Kubernetes Deployment
## 🔧 Configuration
### Environment Variables
| Variable | Description | Default | Required |
| -------------- | --------------- | ----------------------- | -------- |
| `API_BASE_URL` | Backend API URL | `http://localhost:8001` | No |
### Example Configuration
**Development**:
```bash
export API_BASE_URL="http://localhost:8001"
python main.py
```
**Docker**:
```bash
docker run -p 8000:8000 \
-e API_BASE_URL="http://api-service:8001" \
web-app
```
**Kubernetes**:
```yaml
env:
- name: API_BASE_URL
value: "http://nginx-service.api7ee.svc.cluster.local:8080"
```
## 📄 Pages and Routes
### HTML Pages
#### `GET /`
Home dashboard with feature cards and statistics.
**Features**:
- Feature cards (Items, Users, LLM, API Docs)
- Real-time statistics (item count, user count)
- Feature highlights
- Links to all sections
#### `GET /items`
Items management page.
**Features**:
- Display all items from API
- Real-time data fetching
- Interactive UI
#### `GET /users`
Users management page.
**Features**:
- Display all users from API
- User information cards
- Real-time data updates
#### `GET /llm`
LLM chat interface.
**Features**:
- Interactive chat with AI
- Markdown rendering for responses
- Token usage display
- Rate limit indicator (100 tokens/60s)
- Model selection (videogame-expert)
- Real-time status updates
### API Proxy Routes
#### `GET /api/items`
Proxy to backend API items endpoint.
**Backend**: `GET {API_BASE_URL}/items`
#### `GET /api/items/{item_id}`
Proxy to backend API specific item endpoint.
**Backend**: `GET {API_BASE_URL}/items/{item_id}`
#### `GET /api/users`
Proxy to backend API users endpoint.
**Backend**: `GET {API_BASE_URL}/users`
#### `GET /api/users/{user_id}`
Proxy to backend API specific user endpoint.
**Backend**: `GET {API_BASE_URL}/users/{user_id}`
#### `POST /api/llm/chat`
Proxy to backend LLM chat endpoint.
**Backend**: `POST {API_BASE_URL}/llm/chat`
**Request Body**:
```json
{
"prompt": "What is Zelda?",
"max_tokens": 150,
"temperature": 0.7,
"model": "videogame-expert"
}
```
#### `GET /api/llm/models`
Proxy to backend LLM models endpoint.
**Backend**: `GET {API_BASE_URL}/llm/models`
#### `GET /api/llm/health`
Proxy to backend LLM health check.
**Backend**: `GET {API_BASE_URL}/llm/health`
### Health Check
#### `GET /health`
Web application health check.
**Response**:
```json
{
"status": "healthy",
"service": "web",
"version": "1.0.0",
"api_backend": "http://localhost:8001",
"api_status": "healthy"
}
```
**API Status Values**:
- `healthy`: Backend is responding
- `unhealthy`: Backend returned non-200 status
- `unreachable`: Cannot connect to backend
### Configuration Endpoint
#### `GET /api/config`
Get current frontend configuration.
**Response**:
```json
{
"api_base_url": "http://localhost:8001"
}
```
## 📚 Documentation
### MkDocs Configuration
The application includes a comprehensive documentation site built with MkDocs Material theme.
**Configuration file**: `mkdocs.yml`
**Theme Features**:
- Material Design theme
- Light/Dark mode toggle
- Navigation tabs and sections
- Search functionality
- Syntax highlighting
- Code copy buttons
### Documentation Structure
```
docs/
├── index.md # Documentation home
├── getting-started.md # Quick start guide
├── architecture.md # System architecture
├── kubernetes-resources.md # K8s resources reference
├── api7-configuration.md # API7 Gateway setup
├── cicd-pipeline.md # CI/CD documentation
└── troubleshooting.md # Troubleshooting guide
```
### Building Documentation
#### Serve Locally (Development)
```bash
cd web
mkdocs serve
# Access at http://localhost:8001
```
#### Build Static Site
```bash
mkdocs build -d site
# Output directory: web/site/
```
The Docker build automatically builds the documentation during image creation.
### Updating Documentation
1. Edit markdown files in `docs/` directory
2. Test locally with `mkdocs serve`
3. Commit changes
4. Rebuild Docker image or re-deploy
## 🎨 Templates and Static Assets
### Templates (Jinja2)
Located in `templates/`:
- **base.html**: Base template with common HTML structure
- **index.html**: Home dashboard page
- **items.html**: Items management page
- **users.html**: Users management page
- **llm.html**: LLM chat interface
### Static Assets
Located in `static/`:
- **css/**: Custom stylesheets
- **js/**: JavaScript files
### Template Variables
Templates have access to:
- `request`: Starlette Request object
- Context variables passed from route handlers
## 🧪 Testing
### Manual Testing
```bash
# Test home page
curl http://localhost:8000/
# Test health check
curl http://localhost:8000/health
# Test API proxy
curl http://localhost:8000/api/items
curl http://localhost:8000/api/users
# Test LLM proxy
curl -X POST http://localhost:8000/api/llm/chat \
-H "Content-Type: application/json" \
-d '{
"prompt": "What is Mario?",
"max_tokens": 100
}'
# Test configuration
curl http://localhost:8000/api/config
```
### Browser Testing
1. Navigate to http://localhost:8000
2. Check all navigation links work
3. Verify items and users load
4. Test LLM chat interface
5. Browse documentation at /docs
## 🐳 Docker
### Dockerfile Breakdown
```dockerfile
FROM python:3.11-slim
WORKDIR /app
# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application files
COPY main.py .
COPY docs/ ./docs/
COPY static/ ./static/
COPY templates/ ./templates/
COPY mkdocs.yml .
# Build documentation
RUN mkdocs build -d site
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
```
### Multi-stage Build Benefits
- Dependencies installed during build
- Documentation pre-built as static files
- Smaller runtime container
- Faster startup time
### Build and Push
```bash
# Build
docker build -t git.commandware.com/demos/api7-demo/web:latest .
# Tag
docker tag git.commandware.com/demos/api7-demo/web:latest \
git.commandware.com/demos/api7-demo/web:v1.0.0
# Push
docker push git.commandware.com/demos/api7-demo/web:latest
docker push git.commandware.com/demos/api7-demo/web:v1.0.0
```
## ☸️ Kubernetes Deployment
### Basic Deployment
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
name: web-service
namespace: api7ee
spec:
replicas: 2
@@ -90,14 +489,28 @@ spec:
ports:
- containerPort: 8000
name: http
env:
- name: API_BASE_URL
value: "http://nginx-service.api7ee.svc.cluster.local:8080"
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 10
periodSeconds: 5
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 250m
memory: 256Mi
---
apiVersion: v1
kind: Service
@@ -114,55 +527,259 @@ spec:
app: web
```
## Documentation Structure
### With Helm
```
docs/
├── mkdocs.yml # MkDocs configuration
├── index.md # Documentation home
├── getting-started.md # Quick start guide
├── architecture.md # Infrastructure architecture
├── kubernetes-resources.md # K8s resources reference
├── api7-configuration.md # API7 Gateway config guide
├── cicd-pipeline.md # CI/CD documentation
└── troubleshooting.md # Troubleshooting guide
See the [Helm chart README](../helm/api7ee-demo-k8s/README.md) for full deployment options.
```bash
helm install api7ee-demo ./helm/api7ee-demo-k8s \
--set web.image.tag=v1.0.0 \
--set web.replicaCount=3 \
--namespace api7ee
```
## Endpoints
## 📦 Dependencies
| Endpoint | Description |
| --------- | --------------------------- |
| `/` | Main web dashboard |
| `/docs/` | Documentation site (MkDocs) |
| `/health` | Health check endpoint |
### Python Requirements
## CI/CD
```
fastapi==0.104.1
uvicorn[standard]==0.24.0
jinja2==3.1.3
python-multipart==0.0.6
httpx==0.26.0
mkdocs==1.5.3
mkdocs-material==9.5.3
pymdown-extensions==10.7
```
Images are automatically built and pushed to Gitea registry on every push:
### Install
**Registry**: `git.commandware.com/demos/api7-demo/web:<branch>`
```bash
pip install -r requirements.txt
```
See [CI/CD Pipeline documentation](docs/cicd-pipeline.md) for details.
## Development
### Update Documentation
1. Edit markdown files in `docs/` directory
2. Test locally:
```bash
mkdocs serve -f docs/mkdocs.yml
```
3. View at http://localhost:8001
4. Rebuild with `mkdocs build` or rebuild Docker image
### Dependencies
### Dependency Breakdown
- **fastapi**: Web framework
- **uvicorn**: ASGI server
- **uvicorn**: ASGI server with WebSocket support
- **jinja2**: Template engine for HTML pages
- **python-multipart**: Form data parsing
- **httpx**: Async HTTP client for API proxying
- **mkdocs**: Documentation site generator
- **mkdocs-material**: Material theme for MkDocs
- **pymdown-extensions**: Additional markdown extensions
## 🚀 Production Deployment
### Recommended Configuration
```yaml
web:
replicaCount: 3
autoscaling:
enabled: true
minReplicas: 3
maxReplicas: 15
targetCPUUtilizationPercentage: 80
resources:
limits:
cpu: 1000m
memory: 1Gi
requests:
cpu: 500m
memory: 512Mi
env:
- name: API_BASE_URL
value: "http://nginx-service.api7ee.svc.cluster.local:8080"
```
### API7 Gateway Integration
When deployed behind API7 Gateway:
**Routing**:
- Priority 1: `/*` routes to web service (matches all except `/api`)
- Hosts: `commandware.it`
**Plugins**:
- `redirect`: HTTP → HTTPS enforcement
- No rate limiting on web pages (only on API endpoints)
**Service Discovery**:
```yaml
upstream:
name: apache-upstream
nodes:
- host: apache-service.api7ee.svc.cluster.local
port: 80
weight: 100
```
## 🔒 Security
### Best Practices
1. **Non-root User**: Container runs as non-root (UID 1000)
2. **Read-only Filesystem**: Root filesystem is read-only
3. **CORS**: Configured via API7 Gateway
4. **Input Validation**: FastAPI automatic validation
5. **Proxy Security**: Validates API responses before forwarding
### Security Headers
Consider adding these headers via API7 Gateway:
```yaml
plugins:
response-rewrite:
headers:
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
X-XSS-Protection: "1; mode=block"
```
## 📊 Monitoring
### Health Checks
The `/health` endpoint provides:
- Web service status
- API backend connectivity
- API backend health status
**Kubernetes Probes**:
```yaml
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 10
periodSeconds: 5
```
### Logging
Monitor application logs:
```bash
# Docker
docker logs -f web-app
# Kubernetes
kubectl logs -n api7ee -l app=web --tail=100 -f
# Specific pod
kubectl logs -n api7ee web-service-xxx-xxx -f
```
## 🐛 Troubleshooting
### Common Issues
**Issue**: Cannot connect to API backend
```bash
# Check API_BASE_URL
curl http://localhost:8000/api/config
# Check backend health
curl http://localhost:8000/health
# Response shows api_status: "unreachable"
# Solution: Verify API_BASE_URL is correct and backend is running
```
**Issue**: Documentation not showing
```bash
# Rebuild documentation
mkdocs build -d site
# Check if site directory exists
ls -la site/
# Rebuild Docker image
docker build -t web-app .
```
**Issue**: Static assets not loading
```bash
# Check static directory exists
ls -la static/
# Verify static mount in Docker
docker run -it web-app ls -la /app/static
# Check browser console for 404 errors
```
**Issue**: Templates not rendering
```bash
# Check templates directory
ls -la templates/
# Verify template files exist
ls templates/*.html
# Check application logs for Jinja2 errors
docker logs web-app
```
### Debug Mode
Run with debug logging:
```python
# In main.py, add:
import logging
logging.basicConfig(level=logging.DEBUG)
# Or with uvicorn:
uvicorn main:app --log-level debug
```
## 📚 Resources
### Documentation
- **FastAPI**: https://fastapi.tiangolo.com/
- **MkDocs**: https://www.mkdocs.org/
- **MkDocs Material**: https://squidfunk.github.io/mkdocs-material/
- **Jinja2**: https://jinja.palletsprojects.com/
- **HTTPX**: https://www.python-httpx.org/
### Related
- [Main README](../README.md)
- [API Application README](../api/README.md)
- [Helm Chart README](../helm/api7ee-demo-k8s/README.md)
## 🎯 Future Enhancements
Potential improvements:
- [ ] User authentication
- [ ] WebSocket support for real-time updates
- [ ] Progressive Web App (PWA) features
- [ ] Advanced caching strategies
- [ ] Internationalization (i18n)
- [ ] Enhanced error pages
---
_FastAPI web application with integrated MkDocs documentation for API7 Enterprise demo._
**Version**: 1.0.0 | **Port**: 8000 | **Framework**: FastAPI 0.104.1 | **Theme**: Material for MkDocs 9.5.3

View File

@@ -0,0 +1,274 @@
# ArgoCD Values Update Guide
Questo documento descrive le modifiche da applicare al `values.yaml` in ArgoCD.
## 📋 Modifiche Principali
### 1. Service Discovery - DISABILITATO
**Prima:**
```yaml
api7:
serviceDiscovery:
enabled: true
```
**Dopo:**
```yaml
api7:
serviceDiscovery:
enabled: false # Disabled until Service Registry is configured in API7 Dashboard
namespace: ""
```
**Motivo:** Service discovery richiede la configurazione di un Service Registry nel Dashboard API7 Enterprise. Finché non viene configurato, ADC sync fallisce con errore `service registry not found`.
---
### 2. Existing Secret - CONFIGURATO
**Prima:**
```yaml
api7:
gateway:
existingSecret: ""
```
**Dopo:**
```yaml
api7:
gateway:
# Use existing secret for production (recommended)
# The secret api7-credentials already exists with the correct admin key
existingSecret: "api7-credentials"
existingSecretKeys:
adminUrl: admin-url
adminKey: admin-key
group: gateway-group
```
**Motivo:** Il Secret `api7-credentials` esiste già nel namespace `api7ee` con la chiave admin corretta. Usarlo evita di creare un nuovo Secret vuoto.
---
### 3. Gateway Namespace - AGGIUNTO
**Prima:**
```yaml
api7:
gateway:
gatewayService: gateway-0-1759393614-gateway
```
**Dopo:**
```yaml
api7:
gateway:
gatewayService: gateway-0-1759393614-gateway
gatewayNamespace: api7ee
```
---
### 4. ADC Resources - CONFIGURATE
```yaml
api7:
adc:
resources:
limits:
cpu: 500m
memory: 256Mi
requests:
cpu: 100m
memory: 128Mi
```
---
### 5. Ingress Configuration - AGGIORNATA
**Prima:**
```yaml
ingress:
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
hosts:
- host: commandware.it
paths:
- path: /
gateway:
serviceName: gateway-0-1759393614-gateway
port: 80
- path: /api
gateway:
serviceName: gateway-0-1759393614-gateway
port: 80
tls:
- secretName: api7ee-demo-tls
```
**Dopo:**
```yaml
ingress:
className: nginx
annotations:
cert-manager.io/cluster-issuer: cloudflare-acme-prod
hosts:
- host: commandware.it
paths:
- path: /
pathType: Prefix
gateway:
serviceName: gateway-0-1759393614-gateway
port: 80
namespace: api7ee
tls:
- secretName: api7ee-tls
hosts:
- commandware.it
```
**Modifiche:**
- Rimossa annotation `nginx.ingress.kubernetes.io/rewrite-target: /`
- Aggiunta annotation `cert-manager.io/cluster-issuer`
- Un solo path `/` invece di due path separati (`/` e `/api`)
- Aggiunto `namespace: api7ee` al gateway
- Cambiato `secretName` da `api7ee-demo-tls` a `api7ee-tls`
---
### 6. TLS Configuration - COMPLETATA
```yaml
api7:
tls:
enabled: true
certManager:
enabled: true
issuer: cloudflare-acme-prod
issuerKind: ClusterIssuer
privateKey:
rotationPolicy: Always
algorithm: RSA
size: 2048
duration: 2160h # 90 days
renewBefore: 720h # 30 days before expiry
```
---
## 🔄 Come Applicare le Modifiche
### Opzione 1: Interfaccia Web ArgoCD
1. Accedi ad ArgoCD Dashboard
2. Seleziona l'applicazione `api7ee-demo`
3. Clicca su "APP DETAILS" → "PARAMETERS"
4. Modifica i valori come sopra indicato
5. Clicca "SAVE"
6. Clicca "SYNC" per applicare le modifiche
### Opzione 2: Via CLI
```bash
# Edit application
kubectl edit application -n argocd api7ee-demo
# O usa ArgoCD CLI
argocd app set api7ee-demo --values /path/to/updated-values.yaml
argocd app sync api7ee-demo
```
### Opzione 3: Usa il values.yaml aggiornato
Il file `values.yaml` nella root del progetto è già stato aggiornato con tutte le modifiche.
Puoi copiarlo direttamente nella configurazione ArgoCD.
---
## ✅ Verifica Post-Sync
Dopo la sincronizzazione, verifica che tutto funzioni:
### 1. Check ConfigMap
```bash
kubectl get configmap -n api7ee api7ee-demo-api7ee-demo-k8s-adc-config \
-o jsonpath='{.data.adc-config\.yaml}' | grep "discovery_type"
```
**Risultato atteso:** Nessun output (service discovery disabilitato)
### 2. Check Secret
```bash
kubectl get secret -n api7ee api7ee-demo-api7ee-demo-k8s-api7-admin \
-o jsonpath='{.data.admin-key}' | base64 -d | wc -c
```
**Risultato atteso:** `88` (chiave presente)
### 3. Check ADC Sync Job
```bash
kubectl logs -n api7ee job/api7ee-demo-api7ee-demo-k8s-adc-sync --tail=20
```
**Risultato atteso:**
```
[ADC] ✔ success Sync configuration
[ADC] ★ star All is well, see you next time!
```
### 4. Test Routing
```bash
# Test web
curl -I https://commandware.it/
# Test API
curl -I https://commandware.it/api/health
```
**Risultato atteso:** HTTP 200 o 405 (metodi funzionanti)
---
## 📝 Note Importanti
### Service Discovery
Per riabilitare service discovery in futuro:
1. **Configura Service Registry nel Dashboard API7:**
- Accedi al Dashboard: `https://api7ee3-0-1759339083-dashboard:7443`
- Vai a: Gateway Groups → Service Registries
- Aggiungi: Kubernetes Service Registry
- Configura: API Server URL e Token
2. **Abilita nel values.yaml:**
```yaml
api7:
serviceDiscovery:
enabled: true
```
3. **Sync ArgoCD**
### Secret Management
Il Secret `api7-credentials` contiene:
- `admin-url`: URL del Dashboard API7
- `admin-key`: Chiave di autenticazione (88 caratteri)
- `gateway-group`: Nome del gateway group (`default`)
Per ruotare le credenziali:
```bash
kubectl patch secret -n api7ee api7-credentials \
-p '{"data":{"admin-key":"<new-base64-encoded-key>"}}'
```
---
## 🔗 Riferimenti
- [Service Discovery Guide](web/docs/service-discovery.md)
- [Secret Management Guide](web/docs/secret-management.md)
- [Ingress & Routing Guide](web/docs/ingress-routing.md)
- [API7 Enterprise Docs](https://docs.api7.ai/enterprise/)
---
**Data aggiornamento:** 2025-10-09
**Versione Chart:** api7ee-demo-k8s-0.1.0

View File

@@ -0,0 +1,120 @@
# Example ExternalSecret for API7 Gateway credentials
# This file demonstrates how to use External Secrets Operator with API7 Gateway
#
# Documentation: https://external-secrets.io/
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: api7-gateway-credentials
namespace: api7ee
labels:
app.kubernetes.io/name: api7ee-demo
app.kubernetes.io/component: api7
spec:
# Refresh interval for fetching secrets from external provider
refreshInterval: 1h
# Reference to SecretStore
secretStoreRef:
name: vault-backend # Name of your SecretStore
kind: SecretStore # or ClusterSecretStore
# Target Secret configuration
target:
name: api7-credentials
creationPolicy: Owner
template:
type: Opaque
data:
# Map external secret keys to Kubernetes secret keys
admin-url: "{{ .adminUrl }}"
admin-key: "{{ .adminKey }}"
gateway-group: "{{ .group }}"
# Data to fetch from external provider
data:
- secretKey: adminUrl
remoteRef:
key: api7/gateway # Path in external secret store
property: admin_url # Property name
- secretKey: adminKey
remoteRef:
key: api7/gateway
property: admin_key
- secretKey: group
remoteRef:
key: api7/gateway
property: gateway_group
---
# Example SecretStore for AWS Secrets Manager
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: aws-secretsmanager
namespace: api7ee
spec:
provider:
aws:
service: SecretsManager
region: us-east-1
auth:
jwt:
serviceAccountRef:
name: external-secrets-sa
---
# Example SecretStore for HashiCorp Vault
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: vault-backend
namespace: api7ee
spec:
provider:
vault:
server: "https://vault.example.com"
path: "secret"
version: "v2"
auth:
kubernetes:
mountPath: "kubernetes"
role: "api7-role"
serviceAccountRef:
name: api7ee-demo-api7ee-demo-k8s
---
# Example SecretStore for Azure Key Vault
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: azure-keyvault
namespace: api7ee
spec:
provider:
azurekv:
vaultUrl: "https://my-vault.vault.azure.net"
authType: WorkloadIdentity
serviceAccountRef:
name: api7ee-demo-api7ee-demo-k8s
---
# Example SecretStore for GCP Secret Manager
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: gcp-secretmanager
namespace: api7ee
spec:
provider:
gcpsm:
projectID: "my-project"
auth:
workloadIdentity:
clusterLocation: us-central1
clusterName: my-cluster
serviceAccountRef:
name: api7ee-demo-api7ee-demo-k8s

616
web/docs/ingress-routing.md Normal file
View File

@@ -0,0 +1,616 @@
# Ingress and Gateway Routing
Learn how external traffic is routed through Kubernetes Ingress to the API7 Gateway, and then to backend services.
## Architecture Overview
```mermaid
graph LR
A[External User] -->|HTTPS| B[Ingress Controller]
B -->|HTTP/HTTPS| C[API7 Gateway]
C -->|Apply Plugins| C
C -->|Route Traffic| D[Backend Service 1]
C -->|Route Traffic| E[Backend Service 2]
C -->|Route Traffic| F[Backend Service 3]
G[cert-manager] -->|TLS Cert| B
H[ADC ConfigMap] -->|Routing Rules| C
```
**Traffic Flow:**
1. **External request** arrives at Ingress Controller (NGINX)
2. **Ingress Controller** terminates TLS and forwards to API7 Gateway
3. **API7 Gateway** applies policies (rate limiting, CORS, auth)
4. **Gateway** routes to backend services based on ADC rules
5. **Backend services** process request and return response
## Ingress Configuration
### Current Setup
The Helm chart configures Ingress to route **all traffic** through API7 Gateway:
```yaml
# values.yaml
ingress:
enabled: true
className: "nginx"
hosts:
- host: commandware.it
paths:
- path: /
pathType: Prefix
gateway:
serviceName: gateway-0-1759393614-gateway
port: 80
```
Generated Ingress resource:
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api7ee-demo
annotations:
cert-manager.io/cluster-issuer: cloudflare-acme-prod
spec:
ingressClassName: nginx
tls:
- hosts:
- commandware.it
secretName: api7ee-tls
rules:
- host: commandware.it
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: gateway-0-1759393614-gateway
port:
number: 80
```
### Why Route Through Gateway?
**With API7 Gateway (Recommended):**
```
User → Ingress → API7 Gateway → Backend
TLS, Basic Routing
Advanced Features:
- Dynamic routing
- Rate limiting
- CORS policies
- Authentication
- Request transformation
- Metrics & logging
```
**Direct to Service (Legacy):**
```
User → Ingress → Backend
TLS, Basic Routing
❌ No advanced features
❌ No rate limiting
❌ No CORS
❌ No centralized auth
```
## Gateway Routing Rules
### ADC Configuration
API7 Gateway uses **ADC (API Declarative Configuration)** to define routing rules:
```yaml
# Managed by Helm chart in ConfigMap
services:
- name: web-service
hosts:
- commandware.it
upstream:
discovery_type: kubernetes
service_name: my-web-app
namespace_id: default
routes:
- name: web-route
uris:
- /*
priority: 1
plugins:
cors: {...}
redirect:
http_to_https: true
- name: api-service
hosts:
- commandware.it
upstream:
discovery_type: kubernetes
service_name: my-api
namespace_id: default
routes:
- name: api-route
uris:
- /api
- /api/*
priority: 10
plugins:
limit-count: {...}
cors: {...}
```
### Route Priority
Routes are evaluated by **priority** (higher number = higher priority):
```yaml
routes:
# Evaluated FIRST (highest priority)
- name: api-llm-route
uris:
- /api/llm/*
priority: 20
plugins:
ai-rate-limiting: {...}
# Evaluated SECOND
- name: api-route
uris:
- /api/*
priority: 10
plugins:
limit-count: {...}
# Evaluated LAST (lowest priority)
- name: web-route
uris:
- /*
priority: 1
```
**Example Request Matching:**
- `GET /api/llm/chat` → Matches `api-llm-route` (priority 20)
- `GET /api/users` → Matches `api-route` (priority 10)
- `GET /about` → Matches `web-route` (priority 1)
### URI Matching Patterns
**Prefix matching:**
```yaml
uris:
- /api/* # Matches /api/users, /api/v1/products, etc.
- /admin/* # Matches /admin/dashboard, /admin/users, etc.
```
**Exact matching:**
```yaml
uris:
- /health # Matches only /health
- /ready # Matches only /ready
```
**Regex matching:**
```yaml
vars:
- - uri
- "~~"
- "^(?!/api)" # Match all URIs NOT starting with /api
```
## TLS/SSL Configuration
### Certificate Management
The setup uses **cert-manager** with Cloudflare ACME:
```yaml
# values.yaml
ingress:
annotations:
cert-manager.io/cluster-issuer: cloudflare-acme-prod
tls:
- secretName: api7ee-tls
hosts:
- commandware.it
```
Generated Certificate:
```yaml
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: api7ee-demo-tls
spec:
secretName: api7ee-tls
issuerRef:
name: cloudflare-acme-prod
kind: ClusterIssuer
dnsNames:
- commandware.it
duration: 2160h # 90 days
renewBefore: 720h # Renew 30 days before expiry
```
### HTTP to HTTPS Redirect
API7 Gateway handles HTTP→HTTPS redirect:
```yaml
plugins:
redirect:
http_to_https: true
```
**Request flow:**
1. User visits `http://commandware.it/api`
2. Gateway returns `301 Moved Permanently`
3. Browser redirects to `https://commandware.it/api`
## Advanced Routing Scenarios
### Multiple Domains
Route different domains to different backends:
```yaml
services:
- name: main-site
hosts:
- commandware.it
upstream:
service_name: main-web
routes:
- uris: ["/*"]
- name: api-site
hosts:
- api.commandware.it
upstream:
service_name: api-backend
routes:
- uris: ["/*"]
```
### Path-Based Routing
Route based on URL paths:
```yaml
routes:
# Route 1: Frontend SPA
- name: frontend
uris:
- /
- /app/*
- /assets/*
upstream:
service_name: frontend-app
# Route 2: API endpoints
- name: backend-api
uris:
- /api/*
upstream:
service_name: backend-api
# Route 3: Admin panel
- name: admin
uris:
- /admin/*
upstream:
service_name: admin-panel
plugins:
key-auth: {...} # Require authentication
```
### Header-Based Routing
Route based on HTTP headers:
```yaml
routes:
# Route for API v2 clients
- name: api-v2
uris:
- /api/*
vars:
- - http_x_api_version
- "=="
- "v2"
upstream:
service_name: api-v2
# Route for API v1 clients (default)
- name: api-v1
uris:
- /api/*
upstream:
service_name: api-v1
```
### Method-Based Routing
Route based on HTTP method:
```yaml
routes:
# Read operations → Read-only replica
- name: read-ops
uris:
- /api/*
methods:
- GET
- HEAD
upstream:
service_name: api-readonly
# Write operations → Primary backend
- name: write-ops
uris:
- /api/*
methods:
- POST
- PUT
- DELETE
- PATCH
upstream:
service_name: api-primary
```
## Configuration Examples
### Example 1: Microservices API
```yaml
services:
# User service
- name: users-api
hosts:
- api.commandware.it
upstream:
discovery_type: kubernetes
service_name: users-service
routes:
- uris: ["/users", "/users/*"]
plugins:
limit-count:
count: 100
time_window: 60
# Orders service
- name: orders-api
hosts:
- api.commandware.it
upstream:
discovery_type: kubernetes
service_name: orders-service
routes:
- uris: ["/orders", "/orders/*"]
plugins:
key-auth: {}
# Products service
- name: products-api
hosts:
- api.commandware.it
upstream:
discovery_type: kubernetes
service_name: products-service
routes:
- uris: ["/products", "/products/*"]
```
### Example 2: API with WebSocket
```yaml
services:
- name: websocket-service
hosts:
- ws.commandware.it
upstream:
service_name: websocket-backend
routes:
# WebSocket endpoint
- name: ws-route
uris:
- /ws
enable_websocket: true
# HTTP fallback
- name: http-route
uris:
- /*
```
### Example 3: Canary Deployment
```yaml
services:
# 90% traffic to stable version
- name: app-stable
hosts:
- app.commandware.it
upstream:
service_name: app-v1
nodes:
- weight: 90
routes:
- uris: ["/*"]
# 10% traffic to canary version
- name: app-canary
hosts:
- app.commandware.it
upstream:
service_name: app-v2
nodes:
- weight: 10
routes:
- uris: ["/*"]
```
## Monitoring and Debugging
### Check Ingress Status
```bash
# Get Ingress resource
kubectl get ingress -n api7ee
# Describe Ingress
kubectl describe ingress api7ee-demo -n api7ee
# Check Ingress Controller logs
kubectl logs -n ingress-nginx -l app.kubernetes.io/name=ingress-nginx
```
### Check Gateway Routing
```bash
# View ADC configuration
kubectl get configmap api7ee-demo-api7ee-demo-k8s-adc-config -n api7ee -o yaml
# Check Gateway logs
kubectl logs -n api7ee -l app.kubernetes.io/name=gateway
# Test routing
curl -v https://commandware.it/api/health
```
### Verify TLS Certificate
```bash
# Check certificate
kubectl get certificate -n api7ee
# Describe certificate
kubectl describe certificate api7ee-demo-tls -n api7ee
# View certificate secret
kubectl get secret api7ee-tls -n api7ee -o yaml
# Test TLS connection
openssl s_client -connect commandware.it:443 -servername commandware.it
```
## Troubleshooting
### 404 Not Found
**Cause:** No route matches the request
**Debug:**
```bash
# Check ADC routes
kubectl get configmap -n api7ee api7ee-demo-adc-config -o yaml | grep -A 10 "routes:"
# Check Gateway logs
kubectl logs -n api7ee -l app.kubernetes.io/name=gateway | grep 404
```
### 502 Bad Gateway
**Cause:** Backend service is unavailable
**Debug:**
```bash
# Check backend service
kubectl get svc my-backend -n api7ee
# Check backend Pods
kubectl get pods -l app=my-backend -n api7ee
# Check service discovery
kubectl get endpoints my-backend -n api7ee
```
### TLS Certificate Issues
**Cause:** cert-manager failed to issue certificate
**Debug:**
```bash
# Check certificate status
kubectl describe certificate api7ee-demo-tls -n api7ee
# Check cert-manager logs
kubectl logs -n cert-manager -l app=cert-manager
# Check certificate request
kubectl get certificaterequest -n api7ee
```
## Best Practices
### 1. Always Use Gateway for Production
**Do:** Route through API7 Gateway for advanced features
**Don't:** Route directly to backend services
### 2. Use Descriptive Route Names
```yaml
# Good
routes:
- name: api-users-list
- name: api-products-create
- name: web-frontend-spa
# Bad
routes:
- name: route1
- name: route2
```
### 3. Set Appropriate Priorities
Higher priority for more specific routes:
```yaml
# Specific route (high priority)
- uris: ["/api/v2/users/*"]
priority: 20
# General route (low priority)
- uris: ["/api/*"]
priority: 10
```
### 4. Enable TLS Everywhere
Always use HTTPS in production:
```yaml
plugins:
redirect:
http_to_https: true
```
### 5. Monitor Gateway Metrics
Set up monitoring for:
- Request latency
- Error rates (4xx, 5xx)
- Upstream health
- TLS certificate expiry
## Next Steps
- [Configure Service Discovery](service-discovery.md)
- [Manage Secrets Securely](secret-management.md)
- [Set Up Rate Limiting](api7-configuration.md#rate-limiting)
## References
- [Kubernetes Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/)
- [cert-manager Documentation](https://cert-manager.io/docs/)
- [API7 Gateway Routing](https://docs.api7.ai/enterprise/key-concepts/routes/)

View File

@@ -0,0 +1,233 @@
# Secret Management for API7 Gateway
This document explains how to manage API7 Gateway credentials securely.
## Overview
API7 Gateway requires the following credentials:
- **Admin URL**: Dashboard admin API endpoint
- **Admin Key**: Authentication token for admin API
- **Gateway Group**: Logical grouping of gateway instances
## Configuration Options
### Option 1: Inline Configuration (Development Only)
**⚠️ NOT RECOMMENDED for production**
Credentials are stored directly in `values.yaml`:
```yaml
api7:
gateway:
existingSecret: "" # Leave empty
adminUrl: https://api7ee3-0-1759339083-dashboard:7443
adminKey: "your-admin-key"
group: default
```
The chart will create a Kubernetes Secret automatically.
### Option 2: Existing Secret (Recommended)
**✅ RECOMMENDED for production**
Create a Secret manually or using a secret management tool, then reference it:
```yaml
api7:
gateway:
existingSecret: "api7-credentials" # Name of your secret
existingSecretKeys:
adminUrl: admin-url
adminKey: admin-key
group: gateway-group
```
#### Create Secret Manually
```bash
kubectl create secret generic api7-credentials \
--from-literal=admin-url=https://api7ee3-0-1759339083-dashboard:7443 \
--from-literal=admin-key=YOUR_ADMIN_KEY \
--from-literal=gateway-group=default \
-n api7ee
```
#### Get Admin Key from API7 Enterprise
```bash
# Get admin key from API7 Enterprise installation
kubectl get secret -n api7ee api7ee3-0-1759339083 \
-o jsonpath='{.data.admin_key}' | base64 -d
```
### Option 3: External Secrets Operator
**✅ RECOMMENDED for production with centralized secret management**
Use External Secrets Operator to sync secrets from external providers like:
- AWS Secrets Manager
- HashiCorp Vault
- Azure Key Vault
- GCP Secret Manager
- And more...
#### Step 1: Install External Secrets Operator
```bash
helm repo add external-secrets https://charts.external-secrets.io
helm install external-secrets external-secrets/external-secrets -n external-secrets-system --create-namespace
```
#### Step 2: Configure SecretStore
Example for HashiCorp Vault:
```yaml
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: vault-backend
namespace: api7ee
spec:
provider:
vault:
server: "https://vault.example.com"
path: "secret"
version: "v2"
auth:
kubernetes:
mountPath: "kubernetes"
role: "api7-role"
serviceAccountRef:
name: api7ee-demo-api7ee-demo-k8s
```
#### Step 3: Create ExternalSecret
See `templates/externalsecret-api7.yaml.example` for a complete example.
```yaml
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: api7-credentials-external
namespace: api7ee
spec:
refreshInterval: 1h
secretStoreRef:
name: vault-backend
kind: SecretStore
target:
name: api7-credentials
creationPolicy: Owner
data:
- secretKey: admin-url
remoteRef:
key: api7/gateway
property: admin_url
- secretKey: admin-key
remoteRef:
key: api7/gateway
property: admin_key
- secretKey: gateway-group
remoteRef:
key: api7/gateway
property: gateway_group
```
#### Step 4: Configure Helm Values
```yaml
api7:
gateway:
existingSecret: "api7-credentials" # Created by ExternalSecret
```
## Secret Keys
The Secret must contain these keys (default names):
| Key | Description | Example |
|-----|-------------|---------|
| `admin-url` | Dashboard admin API URL | `https://api7ee3-0-1759339083-dashboard:7443` |
| `admin-key` | Dashboard admin API key | `edd1c9f034335f136f87ad84b625c8f1` |
| `gateway-group` | Gateway group name | `default` |
### Custom Key Names
If your Secret uses different key names, configure them:
```yaml
api7:
gateway:
existingSecret: "my-custom-secret"
existingSecretKeys:
adminUrl: dashboard_url # Your custom key name
adminKey: api_token # Your custom key name
group: group_name # Your custom key name
```
## Security Best Practices
1. **Never commit secrets to Git**
- Use `.gitignore` for values files containing secrets
- Use secret management tools for production
2. **Use RBAC to restrict secret access**
- Limit which ServiceAccounts can read secrets
- Use namespace isolation
3. **Rotate credentials regularly**
- Update admin keys periodically
- Use External Secrets Operator for automatic rotation
4. **Enable audit logging**
- Monitor secret access in Kubernetes audit logs
- Alert on unauthorized access attempts
5. **Use encryption at rest**
- Enable Kubernetes secret encryption
- Use external KMS for additional security
## Troubleshooting
### Secret not found
```bash
# Check if secret exists
kubectl get secret -n api7ee
# Describe the secret
kubectl describe secret api7-credentials -n api7ee
```
### Invalid credentials
```bash
# View secret data (base64 encoded)
kubectl get secret api7-credentials -n api7ee -o yaml
# Decode and verify values
kubectl get secret api7-credentials -n api7ee \
-o jsonpath='{.data.admin-key}' | base64 -d
```
### ADC sync job fails
```bash
# Check job logs
kubectl logs -n api7ee job/api7ee-demo-api7ee-demo-k8s-adc-sync
# Common issues:
# - Wrong admin URL (check DNS resolution)
# - Invalid admin key (verify key is correct)
# - TLS certificate issues (use tlsSkipVerify: true for self-signed)
```
## References
- [External Secrets Operator Documentation](https://external-secrets.io/)
- [Kubernetes Secrets](https://kubernetes.io/docs/concepts/configuration/secret/)
- [API7 Enterprise Documentation](https://docs.api7.ai/enterprise/)

View File

@@ -0,0 +1,379 @@
# Service Discovery
Learn how API7 Gateway dynamically discovers backend services using Kubernetes service discovery.
## Overview
Service Discovery enables API7 Gateway to automatically discover and track backend Pod endpoints instead of using static upstream node configuration. This provides a dynamic, scalable, and resilient architecture.
## Why Use Service Discovery?
### Traditional Static Configuration
```yaml
upstream:
nodes:
- host: 10.0.1.23
port: 8080
weight: 100
- host: 10.0.1.24
port: 8080
weight: 100
```
**Problems:**
- ❌ Manual updates required when Pods scale
- ❌ Stale Pod IPs after deployments
- ❌ No automatic health checks
- ❌ Downtime during updates
### With Kubernetes Service Discovery
```yaml
upstream:
discovery_type: kubernetes
service_name: my-service
namespace_id: default
```
**Benefits:**
- ✅ Automatic Pod discovery
- ✅ Real-time updates during scaling
- ✅ Health checks built-in
- ✅ Zero downtime deployments
## How It Works
```mermaid
graph LR
A[API7 Gateway] -->|Query Endpoints| B[Kubernetes API]
B -->|Return Pod IPs| A
A -->|Route Traffic| C[Pod 1]
A -->|Route Traffic| D[Pod 2]
A -->|Route Traffic| E[Pod 3]
F[HPA/Deploy] -->|Scale/Update| B
B -->|Notify| A
```
1. **API7 Gateway** watches Kubernetes Endpoints API
2. **Kubernetes API** returns list of healthy Pod IPs
3. **Gateway** automatically adds/removes Pods from upstream
4. **Traffic** is load-balanced across discovered Pods
## Configuration
### Enable in Helm Chart
Edit `values.yaml`:
```yaml
api7:
serviceDiscovery:
enabled: true # Enable Kubernetes service discovery
namespace: "" # Leave empty to use release namespace
```
### ADC Configuration
The Helm chart automatically generates ADC configuration with service discovery:
```yaml
services:
- name: my-service
upstream:
name: my-upstream
type: roundrobin
discovery_type: kubernetes
service_name: my-service # Kubernetes Service name
namespace_id: default # Kubernetes namespace
```
### RBAC Requirements
Service Discovery requires permissions to watch Kubernetes resources. These are automatically configured in `rbac-adc.yaml`:
```yaml
rules:
# Allow reading services and endpoints for service discovery
- apiGroups: [""]
resources: ["services", "endpoints"]
verbs: ["get", "list", "watch"]
```
## Use Cases
### 1. Auto-Scaling Applications
When your application scales:
```bash
# Scale deployment
kubectl scale deployment my-app --replicas=10
# API7 Gateway automatically discovers new Pods
# No configuration changes needed!
```
### 2. Rolling Updates
During deployments:
```bash
# Start rolling update
kubectl rollout restart deployment my-app
# Gateway automatically:
# 1. Discovers new Pods
# 2. Adds them to upstream
# 3. Removes old Pods
# 4. Zero downtime!
```
### 3. Multi-Service Architecture
Discover multiple backend services:
```yaml
services:
- name: web-frontend
upstream:
discovery_type: kubernetes
service_name: web-frontend
namespace_id: production
- name: api-backend
upstream:
discovery_type: kubernetes
service_name: api-backend
namespace_id: production
- name: admin-api
upstream:
discovery_type: kubernetes
service_name: admin-api
namespace_id: admin
```
## Advanced Configuration
### Cross-Namespace Discovery
Discover services in different namespaces:
```yaml
upstream:
discovery_type: kubernetes
service_name: shared-service
namespace_id: shared-services # Different namespace
```
!!! warning "RBAC Permissions"
Ensure the ServiceAccount has permissions to watch Endpoints in the target namespace.
### Load Balancing Algorithms
Combine service discovery with load balancing:
```yaml
upstream:
discovery_type: kubernetes
service_name: my-service
namespace_id: default
type: roundrobin # or chash, least_conn, ewma
```
**Available algorithms:**
- `roundrobin`: Distribute requests evenly (default)
- `chash`: Consistent hashing (sticky sessions)
- `least_conn`: Send to Pod with fewest connections
- `ewma`: Exponentially weighted moving average
### Health Checks
API7 Gateway respects Kubernetes readiness probes:
```yaml
apiVersion: v1
kind: Pod
metadata:
name: my-app
spec:
containers:
- name: app
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5
periodSeconds: 3
```
Only Pods with passing readiness checks receive traffic.
## Monitoring
### Check Discovered Endpoints
View upstream nodes in API7 Dashboard:
```bash
# Port-forward to dashboard
kubectl port-forward -n api7ee svc/api7ee3-0-1759339083-dashboard 7443:7443
# Open browser
open https://localhost:7443
```
Navigate to: **Gateway Groups → Upstreams → View Nodes**
### Debug Service Discovery
Check ADC sync logs:
```bash
# View ADC sync job logs
kubectl logs -n api7ee job/api7ee-demo-api7ee-demo-k8s-adc-sync
# Look for service discovery messages
# [INFO] Discovered 3 endpoints for service my-service
```
## Troubleshooting
### No Endpoints Discovered
**Symptoms:** Gateway shows 0 upstream nodes
**Causes:**
1. Service doesn't exist
2. No Pods match Service selector
3. All Pods are not ready
**Solution:**
```bash
# Check Service exists
kubectl get service my-service -n default
# Check Endpoints
kubectl get endpoints my-service -n default
# Check Pod readiness
kubectl get pods -l app=my-service -n default
```
### RBAC Permission Denied
**Error:** `forbidden: User "system:serviceaccount:..." cannot list endpoints`
**Solution:**
Verify RBAC permissions:
```bash
# Check Role binding
kubectl get rolebinding -n api7ee | grep adc
# Describe permissions
kubectl describe role api7ee-demo-api7ee-demo-k8s-adc -n api7ee
```
### Stale Endpoints
**Symptoms:** Traffic sent to terminated Pods
**Causes:** Kubernetes API not updating fast enough
**Solution:**
Check Kubernetes API health:
```bash
# Verify API server is responsive
kubectl get --raw /healthz
# Check endpoint update timestamps
kubectl get endpoints my-service -o yaml | grep -i time
```
## Best Practices
### 1. Use Service Discovery for All Dynamic Workloads
**Do:**
```yaml
upstream:
discovery_type: kubernetes
service_name: my-dynamic-app
```
**Don't:**
```yaml
upstream:
nodes:
- host: pod-ip # Brittle, breaks on restart
```
### 2. Configure Readiness Probes
Always configure readiness probes on Pods:
```yaml
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
```
### 3. Use Namespaces for Isolation
Separate environments by namespace:
```yaml
# Production
namespace_id: production
# Staging
namespace_id: staging
# Development
namespace_id: development
```
### 4. Monitor Discovery Performance
Set up alerts for:
- Number of discovered endpoints
- Service discovery latency
- RBAC permission errors
### 5. Test Failover Scenarios
Regularly test:
- Pod termination
- Rolling updates
- Scale up/down events
## Comparison: Static vs Service Discovery
| Feature | Static Nodes | Service Discovery |
|---------|--------------|-------------------|
| Setup | Manual | Automatic |
| Scaling | Manual updates | Automatic |
| Health checks | Manual | Built-in |
| Deployments | Downtime risk | Zero downtime |
| Maintenance | High | Low |
| Flexibility | Low | High |
| Production-ready | ❌ | ✅ |
## Next Steps
- [Configure Secret Management](secret-management.md)
- [Set up Ingress Routing](kubernetes-resources.md#ingress)
- [Monitor Gateway Metrics](troubleshooting.md#monitoring)
## References
- [API7 Service Discovery Documentation](https://docs.api7.ai/enterprise/3.2.14.6/best-practice/service-discovery/)
- [Kubernetes Endpoints API](https://kubernetes.io/docs/concepts/services-networking/service/#endpoints)
- [Kubernetes Service Discovery](https://kubernetes.io/docs/concepts/services-networking/service/)

View File

@@ -0,0 +1,457 @@
---
title: Swagger & OpenAPI Documentation
description: Complete guide to API documentation with Swagger UI, ReDoc, and OpenAPI
---
# Swagger & OpenAPI Documentation
Complete documentation for the API7 Enterprise Demo API with interactive Swagger UI.
## Overview
The API provides comprehensive documentation through multiple formats:
- **Swagger UI** - Interactive API documentation and testing
- **ReDoc** - Clean, responsive API documentation
- **OpenAPI JSON** - Machine-readable API specification
## Access Documentation
### Via Web Interface
1. Navigate to [https://commandware.it/](https://commandware.it/)
2. Click **📚 API Docs** in the navigation menu
3. Choose your preferred format:
- **Swagger UI** - For interactive testing
- **ReDoc** - For reading documentation
- **OpenAPI JSON** - For programmatic access
- **API Root** - For API metadata
### Direct Links
| Format | URL | Description |
|--------|-----|-------------|
| **Swagger UI** | [/api/docs](https://commandware.it/api/docs) | Interactive documentation |
| **ReDoc** | [/api/redoc](https://commandware.it/api/redoc) | Clean documentation style |
| **OpenAPI JSON** | [/api/openapi.json](https://commandware.it/api/openapi.json) | Raw OpenAPI schema |
| **API Root** | [/api/](https://commandware.it/api/) | API information |
---
## API Overview
### Architecture
```
Client → Ingress (NGINX) → API7 Gateway → Backend API
• Rate Limiting
• CORS
• Proxy Rewrite (/api → /)
• Service Discovery
```
### Features
-**CRUD Operations** - Items and Users management
-**LLM Integration** - AI-powered chat with OpenAI-compatible API
-**Health Checks** - Kubernetes-ready liveness and readiness probes
-**Rate Limiting** - Standard (100 req/min) and AI-based (100 tokens/min)
-**CORS** - Cross-origin resource sharing enabled
-**Proxy Rewrite** - Automatic /api prefix removal by API7 Gateway
### Rate Limiting
| Endpoint Type | Limit | Window | Strategy |
|---------------|-------|--------|----------|
| Standard API (`/items`, `/users`) | 100 requests | 60 seconds | Per IP address |
| LLM API (`/llm/*`) | 100 tokens | 60 seconds | AI-based (total tokens) |
---
## Endpoint Groups
### 🏠 Root
#### `GET /`
API information with navigation links.
**Response:**
```json
{
"message": "Welcome to API7 Enterprise Demo API",
"version": "1.0.0",
"documentation": {
"swagger": "/docs",
"redoc": "/redoc",
"openapi": "/openapi.json"
},
"endpoints": {
"items": "/items",
"users": "/users",
"llm": "/llm"
},
"timestamp": "2025-10-09T16:00:00"
}
```
---
### 💚 Health
#### `GET /health`
Kubernetes liveness probe endpoint.
**Response:**
```json
{
"status": "healthy",
"service": "api",
"timestamp": "2025-10-09T16:00:00"
}
```
#### `GET /ready`
Kubernetes readiness probe endpoint.
**Response:**
```json
{
"status": "ready",
"service": "api",
"timestamp": "2025-10-09T16:00:00"
}
```
---
### 📦 Items (CRUD)
Full CRUD operations for items inventory management.
#### `GET /items`
List all items in inventory.
**Rate Limit:** 100 requests per 60 seconds per IP
**Response:** `200 OK`
```json
[
{
"id": 1,
"name": "Laptop",
"description": "High-performance laptop",
"price": 999.99,
"in_stock": true
}
]
```
#### `GET /items/{id}`
Get a specific item by ID.
**Parameters:**
- `id` (path, integer, required) - Item ID
**Responses:**
- `200 OK` - Item details
- `404 Not Found` - Item not found
#### `POST /items`
Create a new item.
**Request Body:**
```json
{
"name": "Laptop",
"description": "High-performance laptop",
"price": 999.99,
"in_stock": true
}
```
**Response:** `201 Created`
#### `PUT /items/{id}`
Update an existing item.
**Parameters:**
- `id` (path, integer, required) - Item ID
**Response:** `200 OK`
#### `DELETE /items/{id}`
Delete an item.
**Parameters:**
- `id` (path, integer, required) - Item ID
**Response:** `200 OK`
```json
{
"message": "Item deleted successfully",
"id": 1
}
```
---
### 👥 Users
User management operations.
#### `GET /users`
List all users.
**Rate Limit:** 100 requests per 60 seconds per IP
#### `GET /users/{id}`
Get a specific user by ID.
#### `POST /users`
Create a new user.
**Request Body:**
```json
{
"username": "john_doe",
"email": "john@example.com",
"active": true
}
```
**Response:** `201 Created`
---
### 🤖 LLM (AI Integration)
AI-powered chat endpoints with OpenAI-compatible API.
#### `POST /llm/chat`
Chat completion with AI rate limiting.
**Rate Limit:** 100 tokens per 60 seconds (AI-based)
**Request Body:**
```json
{
"prompt": "What is API7 Enterprise?",
"max_tokens": 150,
"temperature": 0.7,
"model": "videogame-expert"
}
```
**Response:** `200 OK`
```json
{
"response": "API7 Enterprise is...",
"tokens_used": 45,
"model": "videogame-expert",
"timestamp": "2025-10-09T16:00:00"
}
```
**Errors:**
- `429 Too Many Requests` - Rate limit exceeded (100 tokens per 60 seconds)
- `500 Internal Server Error` - LLM service error
#### `GET /llm/models`
List available LLM models.
**Response:**
```json
{
"models": [
{
"id": "videogame-expert",
"name": "Videogame Expert",
"max_tokens": 4096,
"provider": "Open WebUI",
"description": "Specialized model for videogame-related questions"
}
],
"default_model": "videogame-expert",
"provider": "Open WebUI"
}
```
#### `GET /llm/health`
LLM service health check.
**Response:**
```json
{
"status": "healthy",
"service": "llm-api",
"provider": "Open WebUI",
"endpoint": "http://localhost/api",
"default_model": "videogame-expert",
"rate_limit": {
"enabled": true,
"limit": 100,
"window": "60 seconds",
"strategy": "total_tokens",
"managed_by": "API7 Gateway (ai-rate-limiting plugin)"
}
}
```
---
## Using Swagger UI
### Interactive Testing
1. Navigate to [/api/docs](https://commandware.it/api/docs)
2. Browse available endpoints organized by groups
3. Click **"Try it out"** on any endpoint
4. Fill in parameters and request body
5. Click **"Execute"** to test the API
6. View response with status code, headers, and body
### Example: Testing Items API
1. Go to **Items** section
2. Click on `GET /items`
3. Click **"Try it out"**
4. Click **"Execute"**
5. View the response with all items
### Example: Creating an Item
1. Go to **Items** section
2. Click on `POST /items`
3. Click **"Try it out"**
4. Modify the request body:
```json
{
"name": "New Laptop",
"description": "Gaming laptop",
"price": 1499.99,
"in_stock": true
}
```
5. Click **"Execute"**
6. View `201 Created` response with the new item
---
## Using ReDoc
### Clean Documentation
1. Navigate to [/api/redoc](https://commandware.it/api/redoc)
2. Browse the clean, three-column layout:
- **Left**: Navigation menu
- **Center**: Endpoint details
- **Right**: Code examples
3. Search for specific endpoints
4. Download OpenAPI spec
### Features
- ✅ Responsive design
- ✅ Search functionality
- ✅ Code examples in multiple languages
- ✅ Print-friendly
- ✅ Mobile-friendly
---
## OpenAPI Specification
### Download Schema
Access the raw OpenAPI 3.0 specification:
```bash
curl https://commandware.it/api/openapi.json > openapi.json
```
### Use Cases
- **Code Generation** - Generate client SDKs
- **API Testing** - Import into Postman/Insomnia
- **Validation** - Validate requests/responses
- **Documentation** - Generate custom documentation
### Tools Integration
**Postman:**
```bash
# Import OpenAPI spec
File → Import → https://commandware.it/api/openapi.json
```
**Insomnia:**
```bash
# Import OpenAPI spec
Dashboard → Import/Export → https://commandware.it/api/openapi.json
```
**OpenAPI Generator:**
```bash
# Generate Python client
openapi-generator-cli generate \
-i https://commandware.it/api/openapi.json \
-g python \
-o ./api-client
```
---
## API Changes
### Enhanced Documentation
All endpoints now include:
- ✅ Detailed summaries and descriptions
- ✅ Parameter explanations with examples
- ✅ Response descriptions with status codes
- ✅ Error responses (404, 429, 500)
- ✅ Rate limiting information
- ✅ Request/response examples
### Model Enhancements
All Pydantic models include:
- ✅ Field descriptions
- ✅ Validation rules (min_length, gt, ge, le)
- ✅ Schema examples
- ✅ Default values
---
## Next Steps
- [Getting Started](../getting-started.md) - Set up the development environment
- [API7 Configuration](../api7-configuration.md) - Configure API7 Gateway
- [Ingress & Routing](../ingress-routing.md) - Configure traffic routing
- [Troubleshooting](../troubleshooting.md) - Common issues and solutions
---
## References
- [FastAPI Documentation](https://fastapi.tiangolo.com/)
- [OpenAPI Specification](https://swagger.io/specification/)
- [Swagger UI](https://swagger.io/tools/swagger-ui/)
- [ReDoc](https://redocly.com/redoc/)

View File

@@ -1,137 +1,133 @@
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
import uvicorn
import os
import subprocess
import httpx
app = FastAPI(title="Web Demo Application")
# Build MkDocs documentation on startup
def build_docs():
docs_dir = os.path.join(os.path.dirname(__file__), "docs")
site_dir = os.path.join(os.path.dirname(__file__), "site")
# Get the directory where this script is located
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
if os.path.exists(docs_dir):
# API Configuration - can be set via environment variable
API_BASE_URL = os.getenv("API_BASE_URL", "http://localhost:8001")
# Mount static files
static_dir = os.path.join(BASE_DIR, "static")
if os.path.exists(static_dir):
app.mount("/static", StaticFiles(directory=static_dir), name="static")
# Setup templates
templates_dir = os.path.join(BASE_DIR, "templates")
templates = Jinja2Templates(directory=templates_dir)
# HTTP client for API calls
async def api_request(method: str, endpoint: str, **kwargs):
"""Make a request to the API backend"""
url = f"{API_BASE_URL}{endpoint}"
async with httpx.AsyncClient(timeout=30.0) as client:
try:
subprocess.run(
["mkdocs", "build", "-f", os.path.join(docs_dir, "mkdocs.yml"), "-d", site_dir],
check=True,
capture_output=True
)
print(f"✓ Documentation built successfully at {site_dir}")
return True
except subprocess.CalledProcessError as e:
print(f"✗ Failed to build documentation: {e.stderr.decode()}")
return False
except FileNotFoundError:
print("✗ MkDocs not installed. Install with: pip install mkdocs mkdocs-material")
return False
return False
# Build docs on startup
@app.on_event("startup")
async def startup_event():
build_docs()
# Mount static documentation site at /docs
site_dir = os.path.join(os.path.dirname(__file__), "site")
if os.path.exists(site_dir):
app.mount("/docs", StaticFiles(directory=site_dir, html=True), name="docs")
# Simple HTML template inline
HTML_TEMPLATE = """
<!DOCTYPE html>
<html>
<head>
<title>Web Demo</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 50px auto;
padding: 20px;
background-color: #f5f5f5;
}
.container {
background-color: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
h1 {
color: #333;
border-bottom: 2px solid #4CAF50;
padding-bottom: 10px;
}
.info-box {
background-color: #e8f5e9;
padding: 15px;
margin: 20px 0;
border-left: 4px solid #4CAF50;
border-radius: 4px;
}
.metric {
display: inline-block;
margin: 10px 20px 10px 0;
padding: 10px 20px;
background-color: #2196F3;
color: white;
border-radius: 4px;
}
.doc-link {
display: inline-block;
margin: 20px 10px 0 0;
padding: 12px 24px;
background-color: #673AB7;
color: white;
text-decoration: none;
border-radius: 4px;
transition: background-color 0.3s;
}
.doc-link:hover {
background-color: #512DA8;
}
</style>
</head>
<body>
<div class="container">
<h1>Welcome to Web Demo Application</h1>
<div class="info-box">
<h2>Application Information</h2>
<p><strong>Service:</strong> Web Frontend</p>
<p><strong>Status:</strong> ✓ Running</p>
<p><strong>Version:</strong> 1.0.0</p>
</div>
<h2>Metrics Dashboard</h2>
<div>
<span class="metric">Requests: 1,234</span>
<span class="metric">Uptime: 99.9%</span>
<span class="metric">Users: 567</span>
</div>
<div class="info-box">
<h3>About</h3>
<p>This is a demo FastAPI web application serving HTML content.
It demonstrates a simple web interface with metrics and information display.</p>
</div>
<div>
<a href="/docs/" class="doc-link">📚 View Documentation</a>
<a href="/health" class="doc-link">🏥 Health Check</a>
</div>
</div>
</body>
</html>
"""
response = await client.request(method, url, **kwargs)
response.raise_for_status()
return response.json()
except httpx.HTTPStatusError as e:
raise HTTPException(status_code=e.response.status_code, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail=f"API request failed: {str(e)}")
# ===== ROUTES - HTML Pages =====
@app.get("/", response_class=HTMLResponse)
async def root():
"""Serve the main webpage"""
return HTML_TEMPLATE
async def home(request: Request):
"""Serve the home page"""
return templates.TemplateResponse("index.html", {"request": request})
@app.get("/items", response_class=HTMLResponse)
async def items_page(request: Request):
"""Serve the items page"""
return templates.TemplateResponse("items.html", {"request": request})
@app.get("/users", response_class=HTMLResponse)
async def users_page(request: Request):
"""Serve the users page"""
return templates.TemplateResponse("users.html", {"request": request})
@app.get("/llm", response_class=HTMLResponse)
async def llm_page(request: Request):
"""Serve the LLM chat page"""
return templates.TemplateResponse("llm.html", {"request": request})
# ===== API PROXY ENDPOINTS =====
@app.get("/api/items")
async def proxy_get_items():
"""Proxy GET /items to API backend"""
return await api_request("GET", "/items")
@app.get("/api/items/{item_id}")
async def proxy_get_item(item_id: int):
"""Proxy GET /items/{id} to API backend"""
return await api_request("GET", f"/items/{item_id}")
@app.get("/api/users")
async def proxy_get_users():
"""Proxy GET /users to API backend"""
return await api_request("GET", "/users")
@app.get("/api/users/{user_id}")
async def proxy_get_user(user_id: int):
"""Proxy GET /users/{id} to API backend"""
return await api_request("GET", f"/users/{user_id}")
@app.post("/api/llm/chat")
async def proxy_llm_chat(request: Request):
"""Proxy POST /llm/chat to API backend"""
body = await request.json()
return await api_request("POST", "/llm/chat", json=body)
@app.get("/api/llm/models")
async def proxy_llm_models():
"""Proxy GET /llm/models to API backend"""
return await api_request("GET", "/llm/models")
@app.get("/api/llm/health")
async def proxy_llm_health():
"""Proxy GET /llm/health to API backend"""
return await api_request("GET", "/llm/health")
# ===== WEB HEALTH CHECK =====
@app.get("/health")
async def health():
"""Health check endpoint"""
return {"status": "healthy", "service": "web"}
# Try to connect to API backend
api_status = "unknown"
try:
async with httpx.AsyncClient(timeout=5.0) as client:
response = await client.get(f"{API_BASE_URL}/health")
if response.status_code == 200:
api_status = "healthy"
else:
api_status = "unhealthy"
except:
api_status = "unreachable"
return {
"status": "healthy",
"service": "web",
"version": "1.0.0",
"api_backend": API_BASE_URL,
"api_status": api_status
}
# ===== CONFIG ENDPOINT =====
@app.get("/api/config")
async def get_config():
"""Get current API configuration"""
return {
"api_base_url": API_BASE_URL
}
if __name__ == "__main__":
print(f"Starting Web service")
print(f"API Backend: {API_BASE_URL}")
uvicorn.run(app, host="0.0.0.0", port=8000)

View File

@@ -1,7 +1,8 @@
site_name: API7 Enterprise Demo Documentation
site_description: Complete documentation for API7 Enterprise Gateway demo deployment
site_author: CommandWare
site_url: https://demo.commandware.it/docs
site_url: https://commandware.it/docs
docs_dir: docs
theme:
name: material
@@ -23,9 +24,11 @@ theme:
- navigation.sections
- navigation.expand
- navigation.top
- navigation.instant
- search.suggest
- search.highlight
- content.code.copy
- content.code.annotate
icon:
repo: fontawesome/brands/git-alt
@@ -40,15 +43,27 @@ nav:
- Kubernetes Resources: kubernetes-resources.md
- Configuration:
- API7 Gateway: api7-configuration.md
- Ingress & Routing: ingress-routing.md
- Service Discovery: service-discovery.md
- Secret Management: secret-management.md
- ArgoCD Values Guide: argocd-values-guide.md
- CI/CD Pipeline: cicd-pipeline.md
- API Documentation:
- Swagger & OpenAPI: swagger-documentation.md
- Troubleshooting: troubleshooting.md
markdown_extensions:
- admonition
- pymdownx.details
- pymdownx.superfences
- pymdownx.superfences:
custom_fences:
- name: mermaid
class: mermaid
format: !!python/name:pymdownx.superfences.fence_code_format
- pymdownx.highlight:
anchor_linenums: true
line_spans: __span
pygments_lang_class: true
- pymdownx.inlinehilite
- pymdownx.snippets
- pymdownx.tabbed:
@@ -58,15 +73,24 @@ markdown_extensions:
- md_in_html
- toc:
permalink: true
- def_list
- pymdownx.tasklist:
custom_checkbox: true
- pymdownx.emoji:
emoji_index: !!python/name:material.extensions.emoji.twemoji
emoji_generator: !!python/name:material.extensions.emoji.to_svg
plugins:
- search
- tags
extra:
social:
- icon: fontawesome/brands/git-alt
link: https://git.commandware.com/demos/api7-demo
name: Git Repository
version:
provider: mike
generator: false
copyright: Copyright &copy; 2025 CommandWare
copyright: Copyright &copy; 2025 CommandWare | Powered by API7 Enterprise

View File

@@ -1,4 +1,8 @@
fastapi==0.104.1
uvicorn[standard]==0.24.0
fastapi==0.109.0
uvicorn==0.27.0
jinja2==3.1.3
python-multipart==0.0.6
httpx==0.26.0
mkdocs==1.5.3
mkdocs-material==9.5.3
pymdown-extensions==10.7

503
web/static/css/style.css Normal file
View File

@@ -0,0 +1,503 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
Arial, sans-serif;
line-height: 1.6;
color: #333;
background-color: #f5f5f5;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
}
/* Navbar */
.navbar {
background-color: #2c3e50;
color: white;
padding: 1rem 0;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.nav-brand h2 {
display: inline-block;
margin: 0;
}
.nav-menu {
list-style: none;
display: inline-block;
float: right;
}
.nav-menu li {
display: inline-block;
margin-left: 30px;
}
.nav-menu a {
color: white;
text-decoration: none;
transition: color 0.3s;
}
.nav-menu a:hover {
color: #3498db;
}
/* Main content */
main {
min-height: calc(100vh - 200px);
padding: 40px 20px;
}
/* Hero section */
.hero {
text-align: center;
padding: 60px 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 10px;
margin-bottom: 40px;
}
.hero h1 {
font-size: 2.5rem;
margin-bottom: 1rem;
}
.subtitle {
font-size: 1.2rem;
opacity: 0.9;
}
/* Cards grid */
.cards-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 40px;
}
.card {
background: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
text-align: center;
transition:
transform 0.3s,
box-shadow 0.3s;
}
.card:hover {
transform: translateY(-5px);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
}
.card-icon {
font-size: 3rem;
margin-bottom: 1rem;
}
.card h3 {
margin-bottom: 1rem;
color: #2c3e50;
}
/* Buttons */
.btn {
display: inline-block;
padding: 10px 20px;
background-color: #3498db;
color: white;
text-decoration: none;
border-radius: 5px;
border: none;
cursor: pointer;
transition: background-color 0.3s;
}
.btn:hover {
background-color: #2980b9;
}
.btn-primary {
background-color: #667eea;
}
.btn-primary:hover {
background-color: #5568d3;
}
.btn-sm {
padding: 5px 15px;
font-size: 0.9rem;
}
/* Info section */
.info-section {
background: white;
padding: 30px;
border-radius: 10px;
margin-bottom: 40px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.features-list {
list-style: none;
padding-left: 0;
}
.features-list li {
padding: 10px 0;
font-size: 1.1rem;
}
/* Stats */
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-top: 40px;
}
.stat-box {
background: white;
padding: 30px;
border-radius: 10px;
text-align: center;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.stat-box h3 {
font-size: 2.5rem;
color: #667eea;
margin-bottom: 10px;
}
/* Items grid */
.items-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
}
.item-card {
background: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.item-card.out-of-stock {
opacity: 0.6;
}
.item-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.item-description {
color: #666;
margin-bottom: 15px;
}
.item-footer {
display: flex;
justify-content: space-between;
align-items: center;
}
.price {
font-size: 1.5rem;
font-weight: bold;
color: #27ae60;
}
/* Table */
.table-container {
background: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
overflow-x: auto;
}
.data-table {
width: 100%;
border-collapse: collapse;
}
.data-table th,
.data-table td {
padding: 15px;
text-align: left;
border-bottom: 1px solid #ddd;
}
.data-table th {
background-color: #f8f9fa;
font-weight: 600;
}
/* Badges */
.badge {
padding: 5px 10px;
border-radius: 20px;
font-size: 0.85rem;
font-weight: 600;
}
.badge-success {
background-color: #d4edda;
color: #155724;
}
.badge-danger {
background-color: #f8d7da;
color: #721c24;
}
/* Chat */
.chat-container {
background: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
max-width: 800px;
margin: 0 auto;
}
.chat-messages {
height: 400px;
overflow-y: auto;
padding: 20px;
background: #f8f9fa;
border-radius: 10px;
margin-bottom: 20px;
}
.user-message,
.assistant-message,
.system-message {
padding: 10px 15px;
margin-bottom: 10px;
border-radius: 10px;
max-width: 80%;
}
.user-message {
background-color: #667eea;
color: white;
margin-left: auto;
text-align: right;
}
.assistant-message {
background-color: white;
border: 1px solid #ddd;
}
.system-message {
background-color: #e3f2fd;
color: #1976d2;
text-align: center;
max-width: 100%;
font-size: 0.9rem;
}
.chat-input-container {
display: flex;
gap: 10px;
margin-bottom: 10px;
}
.chat-input-container textarea {
flex: 1;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
font-family: inherit;
resize: vertical;
}
.chat-info {
text-align: center;
color: #666;
}
/* Page header */
.page-header {
margin-bottom: 30px;
}
.page-header h1 {
font-size: 2rem;
margin-bottom: 10px;
}
/* Footer */
.footer {
background-color: #2c3e50;
color: white;
text-align: center;
padding: 20px 0;
margin-top: 40px;
}
/* Modal */
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0, 0, 0, 0.5);
}
.modal-content {
background-color: white;
margin: 5% auto;
padding: 0;
border-radius: 10px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
max-width: 600px;
animation: modalFadeIn 0.3s;
}
@keyframes modalFadeIn {
from {
opacity: 0;
transform: translateY(-50px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.modal-header {
padding: 20px 30px;
border-bottom: 1px solid #ddd;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h2 {
margin: 0;
color: #2c3e50;
}
.close {
color: #aaa;
font-size: 28px;
font-weight: bold;
cursor: pointer;
transition: color 0.3s;
}
.close:hover,
.close:focus {
color: #000;
}
.modal-body {
padding: 30px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #333;
}
.form-control {
width: 100%;
padding: 10px 15px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 1rem;
font-family: inherit;
}
.form-control:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.form-hint {
display: block;
margin-top: 8px;
color: #666;
font-size: 0.9rem;
}
.form-hint code {
background-color: #f5f5f5;
padding: 2px 6px;
border-radius: 3px;
font-family: "Courier New", monospace;
font-size: 0.85rem;
}
.config-info {
background-color: #f8f9fa;
padding: 15px;
border-radius: 5px;
border-left: 4px solid #667eea;
}
.config-info strong {
color: #2c3e50;
}
.modal-actions {
margin-top: 30px;
display: flex;
gap: 10px;
justify-content: flex-end;
}
.btn-secondary {
background-color: #6c757d;
color: white;
}
.btn-secondary:hover {
background-color: #5a6268;
}
/* Utility */
.loading {
text-align: center;
padding: 40px;
color: #666;
}
.error {
color: #f44336;
text-align: center;
padding: 20px;
}

123
web/static/js/app.js Normal file
View File

@@ -0,0 +1,123 @@
// Global API configuration
const DEFAULT_API_BASE = "/api";
const API_BASE_KEY = "api_base_url";
// Get API base URL from localStorage or use default
function getApiBaseUrl() {
return localStorage.getItem(API_BASE_KEY) || DEFAULT_API_BASE;
}
// Set API base URL
function setApiBaseUrl(url) {
localStorage.setItem(API_BASE_KEY, url);
}
// Export for global access
window.API_BASE = getApiBaseUrl();
// API Configuration Modal Functions
function openApiConfig(event) {
if (event) event.preventDefault();
const modal = document.getElementById("api-config-modal");
const input = document.getElementById("api-base-url");
const currentUrl = document.getElementById("current-api-url");
input.value = getApiBaseUrl();
currentUrl.textContent = getApiBaseUrl();
modal.style.display = "block";
}
function closeApiConfig() {
const modal = document.getElementById("api-config-modal");
modal.style.display = "none";
}
function saveApiConfig() {
const input = document.getElementById("api-base-url");
const url = input.value.trim();
if (!url) {
alert("Please enter a valid API base URL");
return;
}
// Remove trailing slash if present
const cleanUrl = url.endsWith("/") ? url.slice(0, -1) : url;
setApiBaseUrl(cleanUrl);
window.API_BASE = cleanUrl;
showNotification("API configuration saved. Reloading page...", "success");
setTimeout(() => {
window.location.reload();
}, 1000);
}
function resetApiConfig() {
if (confirm("Reset API configuration to default (/api)?")) {
setApiBaseUrl(DEFAULT_API_BASE);
window.API_BASE = DEFAULT_API_BASE;
showNotification("API configuration reset. Reloading page...", "success");
setTimeout(() => {
window.location.reload();
}, 1000);
}
}
// Close modal when clicking outside
window.onclick = function (event) {
const modal = document.getElementById("api-config-modal");
if (event.target === modal) {
closeApiConfig();
}
};
// Utility functions
function showNotification(message, type = "info") {
console.log(`[${type.toUpperCase()}] ${message}`);
// Could be extended with toast notifications
}
function formatDate(dateString) {
const date = new Date(dateString);
return date.toLocaleDateString() + " " + date.toLocaleTimeString();
}
function formatPrice(price) {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(price);
}
// API call wrapper
async function apiCall(endpoint, options = {}) {
try {
const response = await fetch(`${API_BASE}${endpoint}`, {
...options,
headers: {
"Content-Type": "application/json",
...options.headers,
},
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
} catch (error) {
showNotification(error.message, "error");
throw error;
}
}
// Export for use in templates
window.apiCall = apiCall;
window.showNotification = showNotification;
window.formatDate = formatDate;
window.formatPrice = formatPrice;

147
web/templates/base.html Normal file
View File

@@ -0,0 +1,147 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{% block title %}API Demo{% endblock %}</title>
<link rel="stylesheet" href="/static/css/style.css" />
<style>
/* Dropdown Menu Styles */
.nav-menu {
list-style: none;
display: flex;
gap: 1rem;
margin: 0;
padding: 0;
}
.dropdown {
position: relative;
display: inline-block;
}
.dropdown-content {
display: none;
position: absolute;
background-color: #2c3e50;
min-width: 200px;
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.3);
z-index: 1000;
border-radius: 4px;
margin-top: 0.5rem;
}
.dropdown-content a {
color: white;
padding: 12px 16px;
text-decoration: none;
display: block;
transition: background-color 0.2s;
}
.dropdown-content a:hover {
background-color: #34495e;
}
.dropdown:hover .dropdown-content {
display: block;
}
.dropbtn {
cursor: pointer;
}
</style>
</head>
<body>
<nav class="navbar">
<div class="container">
<div class="nav-brand">
<h2>🚀 API7EE Demo</h2>
</div>
<ul class="nav-menu">
<li><a href="/" title="Home page">🏠 Home</a></li>
<li><a href="/items" title="Manage items inventory">📦 Items</a></li>
<li><a href="/users" title="Manage users">👥 Users</a></li>
<li><a href="/llm" title="AI-powered chat">🤖 LLM Chat</a></li>
<li class="dropdown">
<a href="#" class="dropbtn" title="API Documentation">📚 API Docs ▾</a>
<div class="dropdown-content">
<a href="/api/" target="_blank" title="API root endpoint">API Root</a>
<a href="/api/docs" target="_blank" title="Swagger UI documentation">Swagger UI</a>
<a href="/api/redoc" target="_blank" title="ReDoc documentation">ReDoc</a>
<a href="/api/openapi.json" target="_blank" title="OpenAPI JSON schema">OpenAPI JSON</a>
</div>
</li>
<li>
<a href="#" onclick="openApiConfig(event)" title="Configure API settings">⚙️ Settings</a>
</li>
</ul>
</div>
</nav>
<!-- API Configuration Modal -->
<div id="api-config-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>⚙️ API Configuration</h2>
<span class="close" onclick="closeApiConfig()"
>&times;</span
>
</div>
<div class="modal-body">
<p>Configure the base URL for API requests:</p>
<div class="form-group">
<label for="api-base-url">API Base URL:</label>
<input
type="text"
id="api-base-url"
placeholder="https://commandware.it/api"
class="form-control"
/>
<small class="form-hint">
Examples:
<code>/api</code> (relative),
<code>https://commandware.it/api</code> (absolute),
<code>http://localhost:8001</code> (local)
</small>
</div>
<div class="form-group">
<label>Current Configuration:</label>
<div class="config-info">
<strong>Base URL:</strong>
<span id="current-api-url">-</span>
</div>
</div>
<div class="modal-actions">
<button
class="btn btn-primary"
onclick="saveApiConfig()"
>
Save Configuration
</button>
<button
class="btn btn-secondary"
onclick="resetApiConfig()"
>
Reset to Default
</button>
<button class="btn" onclick="closeApiConfig()">
Cancel
</button>
</div>
</div>
</div>
</div>
<main class="container">{% block content %}{% endblock %}</main>
<footer class="footer">
<div class="container">
<p>&copy; 2025 API7EE Demo | Powered by FastAPI & API7 Enterprise Gateway</p>
</div>
</footer>
<script src="/static/js/app.js"></script>
{% block scripts %}{% endblock %}
</body>
</html>

86
web/templates/index.html Normal file
View File

@@ -0,0 +1,86 @@
{% extends "base.html" %}
{% block title %}Home - API Demo{% endblock %}
{% block content %}
<div class="hero">
<h1>Welcome to API7EE Demo Platform</h1>
<p class="subtitle">Explore our API services with real-time data and AI-powered features</p>
</div>
<div class="cards-grid">
<div class="card">
<div class="card-icon">📦</div>
<h3>Items Management</h3>
<p>Browse and manage products in our catalog</p>
<a href="/items" class="btn btn-primary">View Items</a>
</div>
<div class="card">
<div class="card-icon">👥</div>
<h3>Users</h3>
<p>Manage user accounts and profiles</p>
<a href="/users" class="btn btn-primary">View Users</a>
</div>
<div class="card">
<div class="card-icon">🤖</div>
<h3>AI Chat (LLM)</h3>
<p>Chat with our videogame expert AI assistant</p>
<a href="/llm" class="btn btn-primary">Start Chat</a>
</div>
<div class="card">
<div class="card-icon">📚</div>
<h3>API Documentation</h3>
<p>Explore our OpenAPI/Swagger documentation</p>
<a href="/api/docs" target="_blank" class="btn btn-primary">Open Docs</a>
</div>
</div>
<div class="info-section">
<h2>Features</h2>
<ul class="features-list">
<li>✅ RESTful API with FastAPI</li>
<li>✅ AI Rate Limiting (100 tokens/60s for LLM)</li>
<li>✅ Standard Rate Limiting (100 req/60s per IP)</li>
<li>✅ OpenAI-compatible LLM endpoint</li>
<li>✅ Real-time data management</li>
<li>✅ Swagger/OpenAPI documentation</li>
</ul>
</div>
<div class="stats">
<div class="stat-box">
<h3 id="items-count">-</h3>
<p>Total Items</p>
</div>
<div class="stat-box">
<h3 id="users-count">-</h3>
<p>Active Users</p>
</div>
<div class="stat-box">
<h3>AI Ready</h3>
<p>LLM Service</p>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
// Fetch stats from API
fetch('/api/items')
.then(res => res.json())
.then(data => {
document.getElementById('items-count').textContent = data.length;
})
.catch(err => console.error('Error fetching items:', err));
fetch('/api/users')
.then(res => res.json())
.then(data => {
document.getElementById('users-count').textContent = data.length;
})
.catch(err => console.error('Error fetching users:', err));
</script>
{% endblock %}

55
web/templates/items.html Normal file
View File

@@ -0,0 +1,55 @@
{% extends "base.html" %}
{% block title %}Items - API Demo{% endblock %}
{% block content %}
<div class="page-header">
<h1>📦 Items Catalog</h1>
<p>Browse all available products</p>
</div>
<div id="items-container" class="items-grid">
<div class="loading">Loading items...</div>
</div>
{% endblock %}
{% block scripts %}
<script>
const API_BASE = '/api';
async function loadItems() {
try {
const response = await fetch(`${API_BASE}/items`);
const items = await response.json();
const container = document.getElementById('items-container');
container.innerHTML = items.map(item => `
<div class="item-card ${!item.in_stock ? 'out-of-stock' : ''}">
<div class="item-header">
<h3>${item.name}</h3>
<span class="badge ${item.in_stock ? 'badge-success' : 'badge-danger'}">
${item.in_stock ? 'In Stock' : 'Out of Stock'}
</span>
</div>
<p class="item-description">${item.description || 'No description'}</p>
<div class="item-footer">
<span class="price">$${item.price.toFixed(2)}</span>
<button class="btn btn-sm" onclick="viewItem(${item.id})">View Details</button>
</div>
</div>
`).join('');
} catch (error) {
console.error('Error loading items:', error);
document.getElementById('items-container').innerHTML =
'<div class="error">Failed to load items. Please try again.</div>';
}
}
function viewItem(id) {
alert(`View item details for ID: ${id}\n\nAPI Endpoint: /api/items/${id}`);
}
// Load items on page load
loadItems();
</script>
{% endblock %}

135
web/templates/llm.html Normal file
View File

@@ -0,0 +1,135 @@
{% extends "base.html" %} {% block title %}LLM Chat - API Demo{% endblock %} {%
block content %}
<div class="page-header">
<h1>🤖 AI Chat - Videogame Expert</h1>
<p>Chat with our AI assistant (Rate limited: 100 tokens/60s)</p>
</div>
<div class="chat-container">
<div class="chat-messages" id="chat-messages">
<div class="system-message">
Welcome! Ask me anything about videogames. I'm powered by the
videogame-expert model.
</div>
</div>
<div class="chat-input-container">
<textarea
id="chat-input"
placeholder="Type your message here..."
rows="3"
></textarea>
<button id="send-btn" class="btn btn-primary" onclick="sendMessage()">
Send Message
</button>
</div>
<div class="chat-info">
<small>
Model: <strong>videogame-expert</strong> | Status:
<span id="status">Ready</span> | Rate Limit:
<strong>100 tokens/60s</strong>
</small>
</div>
</div>
{% endblock %} {% block scripts %}
<script src="https://cdn.jsdelivr.net/npm/marked@11.1.1/marked.min.js"></script>
<script>
const API_BASE = "/api";
let isProcessing = false;
function addMessage(content, isUser = false) {
const messagesDiv = document.getElementById("chat-messages");
const messageDiv = document.createElement("div");
messageDiv.className = isUser ? "user-message" : "assistant-message";
if (isUser) {
messageDiv.textContent = content;
} else {
// Parse markdown for assistant messages
messageDiv.innerHTML = marked.parse(content);
}
messagesDiv.appendChild(messageDiv);
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
function setStatus(text, isError = false) {
const statusSpan = document.getElementById("status");
statusSpan.textContent = text;
statusSpan.style.color = isError ? "#f44336" : "#4CAF50";
}
async function sendMessage() {
if (isProcessing) return;
const input = document.getElementById("chat-input");
const prompt = input.value.trim();
if (!prompt) {
alert("Please enter a message");
return;
}
// Add user message
addMessage(prompt, true);
input.value = "";
// Set processing state
isProcessing = true;
document.getElementById("send-btn").disabled = true;
setStatus("Processing...");
try {
const response = await fetch(`${API_BASE}/llm/chat`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
prompt: prompt,
max_tokens: 150,
temperature: 0.7,
model: "videogame-expert",
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || "API request failed");
}
const data = await response.json();
// Add assistant response
addMessage(data.response, false);
// Show tokens used
const tokensInfo = `Tokens used: ${data.tokens_used}`;
const infoDiv = document.createElement("div");
infoDiv.className = "system-message";
infoDiv.textContent = tokensInfo;
document.getElementById("chat-messages").appendChild(infoDiv);
setStatus("Ready");
} catch (error) {
console.error("Error:", error);
addMessage(`Error: ${error.message}`, false);
setStatus("Error", true);
} finally {
isProcessing = false;
document.getElementById("send-btn").disabled = false;
}
}
// Allow Enter to send (Shift+Enter for newline)
document
.getElementById("chat-input")
.addEventListener("keydown", function (e) {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
</script>
{% endblock %}

69
web/templates/users.html Normal file
View File

@@ -0,0 +1,69 @@
{% extends "base.html" %}
{% block title %}Users - API Demo{% endblock %}
{% block content %}
<div class="page-header">
<h1>👥 Users</h1>
<p>Manage user accounts</p>
</div>
<div class="table-container">
<table class="data-table">
<thead>
<tr>
<th>ID</th>
<th>Username</th>
<th>Email</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="users-table-body">
<tr>
<td colspan="5" class="loading">Loading users...</td>
</tr>
</tbody>
</table>
</div>
{% endblock %}
{% block scripts %}
<script>
const API_BASE = '/api';
async function loadUsers() {
try {
const response = await fetch(`${API_BASE}/users`);
const users = await response.json();
const tbody = document.getElementById('users-table-body');
tbody.innerHTML = users.map(user => `
<tr>
<td>${user.id}</td>
<td>${user.username}</td>
<td>${user.email}</td>
<td>
<span class="badge ${user.active ? 'badge-success' : 'badge-danger'}">
${user.active ? 'Active' : 'Inactive'}
</span>
</td>
<td>
<button class="btn btn-sm" onclick="viewUser(${user.id})">View</button>
</td>
</tr>
`).join('');
} catch (error) {
console.error('Error loading users:', error);
document.getElementById('users-table-body').innerHTML =
'<tr><td colspan="5" class="error">Failed to load users</td></tr>';
}
}
function viewUser(id) {
alert(`View user details for ID: ${id}\n\nAPI Endpoint: /api/users/${id}`);
}
loadUsers();
</script>
{% endblock %}