608 lines
14 KiB
Markdown
608 lines
14 KiB
Markdown
|
|
# Extension Registry Service
|
||
|
|
|
||
|
|
A high-performance Rust microservice that provides a unified REST API for extension discovery, versioning, and download from multiple sources (Gitea releases and OCI registries).
|
||
|
|
|
||
|
|
## Features
|
||
|
|
|
||
|
|
- **Multi-Backend Support**: Fetch extensions from Gitea releases and OCI registries
|
||
|
|
- **Unified REST API**: Single API for all extension operations
|
||
|
|
- **Smart Caching**: LRU cache with TTL to reduce backend API calls
|
||
|
|
- **Prometheus Metrics**: Built-in metrics for monitoring
|
||
|
|
- **Health Monitoring**: Health checks for all backends
|
||
|
|
- **Type-Safe**: Strong typing for extension metadata
|
||
|
|
- **Async/Await**: High-performance async operations with Tokio
|
||
|
|
- **Docker Support**: Production-ready containerization
|
||
|
|
|
||
|
|
## Architecture
|
||
|
|
|
||
|
|
```
|
||
|
|
┌─────────────────────────────────────────────────────────────┐
|
||
|
|
│ Extension Registry API │
|
||
|
|
│ (axum) │
|
||
|
|
├─────────────────────────────────────────────────────────────┤
|
||
|
|
│ │
|
||
|
|
│ ┌────────────────┐ ┌────────────────┐ ┌──────────────┐ │
|
||
|
|
│ │ Gitea Client │ │ OCI Client │ │ LRU Cache │ │
|
||
|
|
│ │ (reqwest) │ │ (reqwest) │ │ (parking) │ │
|
||
|
|
│ └────────────────┘ └────────────────┘ └──────────────┘ │
|
||
|
|
│ │ │ │ │
|
||
|
|
└─────────┼────────────────────┼────────────────────┼─────────┘
|
||
|
|
│ │ │
|
||
|
|
▼ ▼ ▼
|
||
|
|
┌──────────┐ ┌──────────┐ ┌──────────┐
|
||
|
|
│ Gitea │ │ OCI │ │ Memory │
|
||
|
|
│ Releases │ │ Registry │ │ │
|
||
|
|
└──────────┘ └──────────┘ └──────────┘
|
||
|
|
```
|
||
|
|
|
||
|
|
## Installation
|
||
|
|
|
||
|
|
### Building from Source
|
||
|
|
|
||
|
|
```bash
|
||
|
|
cd provisioning/platform/extension-registry
|
||
|
|
cargo build --release
|
||
|
|
```
|
||
|
|
|
||
|
|
### Docker Build
|
||
|
|
|
||
|
|
```bash
|
||
|
|
docker build -t extension-registry:latest .
|
||
|
|
```
|
||
|
|
|
||
|
|
### Running with Cargo
|
||
|
|
|
||
|
|
```bash
|
||
|
|
cargo run -- --config config.toml --port 8082
|
||
|
|
```
|
||
|
|
|
||
|
|
### Running with Docker
|
||
|
|
|
||
|
|
```bash
|
||
|
|
docker run -d \
|
||
|
|
-p 8082:8082 \
|
||
|
|
-v $(pwd)/config.toml:/app/config.toml:ro \
|
||
|
|
-v $(pwd)/tokens:/app/tokens:ro \
|
||
|
|
extension-registry:latest
|
||
|
|
```
|
||
|
|
|
||
|
|
## Configuration
|
||
|
|
|
||
|
|
Create a `config.toml` file (see `config.example.toml`):
|
||
|
|
|
||
|
|
```toml
|
||
|
|
[server]
|
||
|
|
host = "0.0.0.0"
|
||
|
|
port = 8082
|
||
|
|
workers = 4
|
||
|
|
enable_cors = true
|
||
|
|
enable_compression = true
|
||
|
|
|
||
|
|
# Gitea backend (optional)
|
||
|
|
[gitea]
|
||
|
|
url = "https://gitea.example.com"
|
||
|
|
organization = "provisioning-extensions"
|
||
|
|
token_path = "/path/to/gitea-token.txt"
|
||
|
|
timeout_seconds = 30
|
||
|
|
verify_ssl = true
|
||
|
|
|
||
|
|
# OCI registry backend (optional)
|
||
|
|
[oci]
|
||
|
|
registry = "registry.example.com"
|
||
|
|
namespace = "provisioning"
|
||
|
|
auth_token_path = "/path/to/oci-token.txt"
|
||
|
|
timeout_seconds = 30
|
||
|
|
verify_ssl = true
|
||
|
|
|
||
|
|
# Cache configuration
|
||
|
|
[cache]
|
||
|
|
capacity = 1000
|
||
|
|
ttl_seconds = 300
|
||
|
|
enable_metadata_cache = true
|
||
|
|
enable_list_cache = true
|
||
|
|
```
|
||
|
|
|
||
|
|
**Note**: At least one backend (Gitea or OCI) must be configured.
|
||
|
|
|
||
|
|
## API Endpoints
|
||
|
|
|
||
|
|
### Extension Operations
|
||
|
|
|
||
|
|
#### List Extensions
|
||
|
|
|
||
|
|
```bash
|
||
|
|
GET /api/v1/extensions
|
||
|
|
```
|
||
|
|
|
||
|
|
Query parameters:
|
||
|
|
- `type` (optional): Filter by extension type (`provider`, `taskserv`, `cluster`)
|
||
|
|
- `source` (optional): Filter by source (`gitea`, `oci`)
|
||
|
|
- `limit` (optional): Maximum results (default: 100)
|
||
|
|
- `offset` (optional): Pagination offset (default: 0)
|
||
|
|
|
||
|
|
Example:
|
||
|
|
```bash
|
||
|
|
curl http://localhost:8082/api/v1/extensions?type=provider&limit=10
|
||
|
|
```
|
||
|
|
|
||
|
|
Response:
|
||
|
|
```json
|
||
|
|
[
|
||
|
|
{
|
||
|
|
"name": "aws",
|
||
|
|
"type": "provider",
|
||
|
|
"version": "1.2.0",
|
||
|
|
"description": "AWS provider for provisioning",
|
||
|
|
"author": "provisioning-team",
|
||
|
|
"repository": "https://gitea.example.com/org/aws_prov",
|
||
|
|
"source": "gitea",
|
||
|
|
"published_at": "2025-10-06T12:00:00Z",
|
||
|
|
"download_url": "https://gitea.example.com/org/aws_prov/releases/download/1.2.0/aws_prov.tar.gz",
|
||
|
|
"size": 1024000
|
||
|
|
}
|
||
|
|
]
|
||
|
|
```
|
||
|
|
|
||
|
|
#### Get Extension
|
||
|
|
|
||
|
|
```bash
|
||
|
|
GET /api/v1/extensions/{type}/{name}
|
||
|
|
```
|
||
|
|
|
||
|
|
Example:
|
||
|
|
```bash
|
||
|
|
curl http://localhost:8082/api/v1/extensions/provider/aws
|
||
|
|
```
|
||
|
|
|
||
|
|
#### List Versions
|
||
|
|
|
||
|
|
```bash
|
||
|
|
GET /api/v1/extensions/{type}/{name}/versions
|
||
|
|
```
|
||
|
|
|
||
|
|
Example:
|
||
|
|
```bash
|
||
|
|
curl http://localhost:8082/api/v1/extensions/provider/aws/versions
|
||
|
|
```
|
||
|
|
|
||
|
|
Response:
|
||
|
|
```json
|
||
|
|
[
|
||
|
|
{
|
||
|
|
"version": "1.2.0",
|
||
|
|
"published_at": "2025-10-06T12:00:00Z",
|
||
|
|
"download_url": "https://gitea.example.com/org/aws_prov/releases/download/1.2.0/aws_prov.tar.gz",
|
||
|
|
"size": 1024000
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"version": "1.1.0",
|
||
|
|
"published_at": "2025-09-15T10:30:00Z",
|
||
|
|
"download_url": "https://gitea.example.com/org/aws_prov/releases/download/1.1.0/aws_prov.tar.gz",
|
||
|
|
"size": 980000
|
||
|
|
}
|
||
|
|
]
|
||
|
|
```
|
||
|
|
|
||
|
|
#### Download Extension
|
||
|
|
|
||
|
|
```bash
|
||
|
|
GET /api/v1/extensions/{type}/{name}/{version}
|
||
|
|
```
|
||
|
|
|
||
|
|
Example:
|
||
|
|
```bash
|
||
|
|
curl -O http://localhost:8082/api/v1/extensions/provider/aws/1.2.0
|
||
|
|
```
|
||
|
|
|
||
|
|
Returns binary data with `Content-Type: application/octet-stream`.
|
||
|
|
|
||
|
|
#### Search Extensions
|
||
|
|
|
||
|
|
```bash
|
||
|
|
GET /api/v1/extensions/search?q={query}
|
||
|
|
```
|
||
|
|
|
||
|
|
Query parameters:
|
||
|
|
- `q` (required): Search query
|
||
|
|
- `type` (optional): Filter by extension type
|
||
|
|
- `limit` (optional): Maximum results (default: 50)
|
||
|
|
|
||
|
|
Example:
|
||
|
|
```bash
|
||
|
|
curl http://localhost:8082/api/v1/extensions/search?q=kubernetes&type=taskserv
|
||
|
|
```
|
||
|
|
|
||
|
|
### System Endpoints
|
||
|
|
|
||
|
|
#### Health Check
|
||
|
|
|
||
|
|
```bash
|
||
|
|
GET /api/v1/health
|
||
|
|
```
|
||
|
|
|
||
|
|
Example:
|
||
|
|
```bash
|
||
|
|
curl http://localhost:8082/api/v1/health
|
||
|
|
```
|
||
|
|
|
||
|
|
Response:
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"status": "healthy",
|
||
|
|
"version": "0.1.0",
|
||
|
|
"uptime": 3600,
|
||
|
|
"backends": {
|
||
|
|
"gitea": {
|
||
|
|
"enabled": true,
|
||
|
|
"healthy": true
|
||
|
|
},
|
||
|
|
"oci": {
|
||
|
|
"enabled": true,
|
||
|
|
"healthy": true
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### Metrics
|
||
|
|
|
||
|
|
```bash
|
||
|
|
GET /api/v1/metrics
|
||
|
|
```
|
||
|
|
|
||
|
|
Returns Prometheus-formatted metrics:
|
||
|
|
```
|
||
|
|
# HELP http_requests_total Total HTTP requests
|
||
|
|
# TYPE http_requests_total counter
|
||
|
|
http_requests_total 1234
|
||
|
|
|
||
|
|
# HELP cache_hits_total Total cache hits
|
||
|
|
# TYPE cache_hits_total counter
|
||
|
|
cache_hits_total 567
|
||
|
|
|
||
|
|
# HELP cache_misses_total Total cache misses
|
||
|
|
# TYPE cache_misses_total counter
|
||
|
|
cache_misses_total 123
|
||
|
|
```
|
||
|
|
|
||
|
|
#### Cache Statistics
|
||
|
|
|
||
|
|
```bash
|
||
|
|
GET /api/v1/cache/stats
|
||
|
|
```
|
||
|
|
|
||
|
|
Response:
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"list_entries": 45,
|
||
|
|
"metadata_entries": 120,
|
||
|
|
"version_entries": 80,
|
||
|
|
"total_entries": 245
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Extension Naming Conventions
|
||
|
|
|
||
|
|
### Gitea Repositories
|
||
|
|
|
||
|
|
Extensions in Gitea follow specific naming patterns:
|
||
|
|
|
||
|
|
- **Providers**: `{name}_prov` (e.g., `aws_prov`, `upcloud_prov`)
|
||
|
|
- **Task Services**: `{name}_taskserv` (e.g., `kubernetes_taskserv`, `postgres_taskserv`)
|
||
|
|
- **Clusters**: `{name}_cluster` (e.g., `buildkit_cluster`, `ci_cluster`)
|
||
|
|
|
||
|
|
### OCI Artifacts
|
||
|
|
|
||
|
|
Extensions in OCI registries follow these patterns:
|
||
|
|
|
||
|
|
- **Providers**: `{namespace}/{name}-provider` (e.g., `provisioning/aws-provider`)
|
||
|
|
- **Task Services**: `{namespace}/{name}-taskserv` (e.g., `provisioning/kubernetes-taskserv`)
|
||
|
|
- **Clusters**: `{namespace}/{name}-cluster` (e.g., `provisioning/buildkit-cluster`)
|
||
|
|
|
||
|
|
## Caching Strategy
|
||
|
|
|
||
|
|
The service implements a multi-level LRU cache with TTL:
|
||
|
|
|
||
|
|
1. **List Cache**: Caches extension lists (filtered by type/source)
|
||
|
|
2. **Metadata Cache**: Caches individual extension metadata
|
||
|
|
3. **Version Cache**: Caches version lists per extension
|
||
|
|
|
||
|
|
Cache behavior:
|
||
|
|
- **Capacity**: Configurable (default: 1000 entries)
|
||
|
|
- **TTL**: Configurable (default: 5 minutes)
|
||
|
|
- **Eviction**: LRU (Least Recently Used)
|
||
|
|
- **Invalidation**: Automatic on TTL expiration
|
||
|
|
|
||
|
|
Cache keys:
|
||
|
|
- List: `list:{type}:{source}`
|
||
|
|
- Metadata: `{type}/{name}`
|
||
|
|
- Versions: `{type}/{name}/versions`
|
||
|
|
|
||
|
|
## Error Handling
|
||
|
|
|
||
|
|
The API uses standard HTTP status codes:
|
||
|
|
|
||
|
|
- `200 OK`: Successful operation
|
||
|
|
- `400 Bad Request`: Invalid input (e.g., invalid extension type)
|
||
|
|
- `401 Unauthorized`: Authentication failed
|
||
|
|
- `404 Not Found`: Extension not found
|
||
|
|
- `429 Too Many Requests`: Rate limit exceeded
|
||
|
|
- `500 Internal Server Error`: Server error
|
||
|
|
|
||
|
|
Error response format:
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"error": "not_found",
|
||
|
|
"message": "Extension provider/nonexistent not found"
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Metrics and Monitoring
|
||
|
|
|
||
|
|
### Prometheus Metrics
|
||
|
|
|
||
|
|
Available metrics:
|
||
|
|
- `http_requests_total`: Total HTTP requests
|
||
|
|
- `http_request_duration_seconds`: Request duration histogram
|
||
|
|
- `cache_hits_total`: Total cache hits
|
||
|
|
- `cache_misses_total`: Total cache misses
|
||
|
|
- `extensions_total`: Total extensions served
|
||
|
|
|
||
|
|
### Health Checks
|
||
|
|
|
||
|
|
The health endpoint checks:
|
||
|
|
- Service uptime
|
||
|
|
- Gitea backend connectivity
|
||
|
|
- OCI backend connectivity
|
||
|
|
- Overall service status
|
||
|
|
|
||
|
|
## Development
|
||
|
|
|
||
|
|
### Project Structure
|
||
|
|
|
||
|
|
```
|
||
|
|
extension-registry/
|
||
|
|
├── Cargo.toml # Rust dependencies
|
||
|
|
├── src/
|
||
|
|
│ ├── main.rs # Entry point
|
||
|
|
│ ├── lib.rs # Library exports
|
||
|
|
│ ├── config.rs # Configuration management
|
||
|
|
│ ├── error.rs # Error types
|
||
|
|
│ ├── api/
|
||
|
|
│ │ ├── handlers.rs # HTTP handlers
|
||
|
|
│ │ └── routes.rs # Route definitions
|
||
|
|
│ ├── gitea/
|
||
|
|
│ │ ├── client.rs # Gitea API client
|
||
|
|
│ │ └── models.rs # Gitea data models
|
||
|
|
│ ├── oci/
|
||
|
|
│ │ ├── client.rs # OCI registry client
|
||
|
|
│ │ └── models.rs # OCI data models
|
||
|
|
│ ├── cache/
|
||
|
|
│ │ └── lru_cache.rs # LRU caching
|
||
|
|
│ └── models/
|
||
|
|
│ └── extension.rs # Extension models
|
||
|
|
├── tests/
|
||
|
|
│ └── integration_test.rs # Integration tests
|
||
|
|
├── Dockerfile # Docker build
|
||
|
|
└── README.md # This file
|
||
|
|
```
|
||
|
|
|
||
|
|
### Running Tests
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Run all tests
|
||
|
|
cargo test
|
||
|
|
|
||
|
|
# Run with output
|
||
|
|
cargo test -- --nocapture
|
||
|
|
|
||
|
|
# Run specific test
|
||
|
|
cargo test test_health_check
|
||
|
|
```
|
||
|
|
|
||
|
|
### Code Quality
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Format code
|
||
|
|
cargo fmt
|
||
|
|
|
||
|
|
# Run clippy
|
||
|
|
cargo clippy
|
||
|
|
|
||
|
|
# Check for security vulnerabilities
|
||
|
|
cargo audit
|
||
|
|
```
|
||
|
|
|
||
|
|
## Deployment
|
||
|
|
|
||
|
|
### Systemd Service
|
||
|
|
|
||
|
|
Create `/etc/systemd/system/extension-registry.service`:
|
||
|
|
|
||
|
|
```ini
|
||
|
|
[Unit]
|
||
|
|
Description=Extension Registry Service
|
||
|
|
After=network.target
|
||
|
|
|
||
|
|
[Service]
|
||
|
|
Type=simple
|
||
|
|
User=registry
|
||
|
|
WorkingDirectory=/opt/extension-registry
|
||
|
|
ExecStart=/usr/local/bin/extension-registry --config /etc/extension-registry/config.toml
|
||
|
|
Restart=on-failure
|
||
|
|
RestartSec=5s
|
||
|
|
|
||
|
|
[Install]
|
||
|
|
WantedBy=multi-user.target
|
||
|
|
```
|
||
|
|
|
||
|
|
Enable and start:
|
||
|
|
```bash
|
||
|
|
sudo systemctl enable extension-registry
|
||
|
|
sudo systemctl start extension-registry
|
||
|
|
sudo systemctl status extension-registry
|
||
|
|
```
|
||
|
|
|
||
|
|
### Docker Compose
|
||
|
|
|
||
|
|
```yaml
|
||
|
|
version: '3.8'
|
||
|
|
|
||
|
|
services:
|
||
|
|
extension-registry:
|
||
|
|
image: extension-registry:latest
|
||
|
|
ports:
|
||
|
|
- "8082:8082"
|
||
|
|
volumes:
|
||
|
|
- ./config.toml:/app/config.toml:ro
|
||
|
|
- ./tokens:/app/tokens:ro
|
||
|
|
restart: unless-stopped
|
||
|
|
healthcheck:
|
||
|
|
test: ["CMD", "curl", "-f", "http://localhost:8082/api/v1/health"]
|
||
|
|
interval: 30s
|
||
|
|
timeout: 3s
|
||
|
|
retries: 3
|
||
|
|
start_period: 5s
|
||
|
|
```
|
||
|
|
|
||
|
|
### Kubernetes Deployment
|
||
|
|
|
||
|
|
```yaml
|
||
|
|
apiVersion: apps/v1
|
||
|
|
kind: Deployment
|
||
|
|
metadata:
|
||
|
|
name: extension-registry
|
||
|
|
spec:
|
||
|
|
replicas: 3
|
||
|
|
selector:
|
||
|
|
matchLabels:
|
||
|
|
app: extension-registry
|
||
|
|
template:
|
||
|
|
metadata:
|
||
|
|
labels:
|
||
|
|
app: extension-registry
|
||
|
|
spec:
|
||
|
|
containers:
|
||
|
|
- name: extension-registry
|
||
|
|
image: extension-registry:latest
|
||
|
|
ports:
|
||
|
|
- containerPort: 8082
|
||
|
|
volumeMounts:
|
||
|
|
- name: config
|
||
|
|
mountPath: /app/config.toml
|
||
|
|
subPath: config.toml
|
||
|
|
- name: tokens
|
||
|
|
mountPath: /app/tokens
|
||
|
|
livenessProbe:
|
||
|
|
httpGet:
|
||
|
|
path: /api/v1/health
|
||
|
|
port: 8082
|
||
|
|
initialDelaySeconds: 5
|
||
|
|
periodSeconds: 10
|
||
|
|
readinessProbe:
|
||
|
|
httpGet:
|
||
|
|
path: /api/v1/health
|
||
|
|
port: 8082
|
||
|
|
initialDelaySeconds: 5
|
||
|
|
periodSeconds: 5
|
||
|
|
volumes:
|
||
|
|
- name: config
|
||
|
|
configMap:
|
||
|
|
name: extension-registry-config
|
||
|
|
- name: tokens
|
||
|
|
secret:
|
||
|
|
secretName: extension-registry-tokens
|
||
|
|
---
|
||
|
|
apiVersion: v1
|
||
|
|
kind: Service
|
||
|
|
metadata:
|
||
|
|
name: extension-registry
|
||
|
|
spec:
|
||
|
|
selector:
|
||
|
|
app: extension-registry
|
||
|
|
ports:
|
||
|
|
- port: 8082
|
||
|
|
targetPort: 8082
|
||
|
|
type: ClusterIP
|
||
|
|
```
|
||
|
|
|
||
|
|
## Security
|
||
|
|
|
||
|
|
### Authentication
|
||
|
|
|
||
|
|
- Gitea: Token-based authentication via `token_path`
|
||
|
|
- OCI: Optional token authentication via `auth_token_path`
|
||
|
|
|
||
|
|
### Best Practices
|
||
|
|
|
||
|
|
1. **Store tokens securely**: Use file permissions (600) for token files
|
||
|
|
2. **Enable SSL verification**: Set `verify_ssl = true` in production
|
||
|
|
3. **Use HTTPS**: Always use HTTPS for Gitea and OCI registries
|
||
|
|
4. **Limit CORS**: Configure CORS appropriately for production
|
||
|
|
5. **Rate limiting**: Consider adding rate limiting for public APIs
|
||
|
|
6. **Network isolation**: Run service in isolated network segments
|
||
|
|
|
||
|
|
## Performance
|
||
|
|
|
||
|
|
### Benchmarks
|
||
|
|
|
||
|
|
Typical performance characteristics:
|
||
|
|
- **Cached requests**: <5ms response time
|
||
|
|
- **Uncached requests**: 50-200ms (depends on backend latency)
|
||
|
|
- **Cache hit ratio**: ~85-95% in typical workloads
|
||
|
|
- **Throughput**: 1000+ req/s on modern hardware
|
||
|
|
|
||
|
|
### Optimization Tips
|
||
|
|
|
||
|
|
1. **Increase cache capacity**: For large extension catalogs
|
||
|
|
2. **Tune TTL**: Balance freshness vs. performance
|
||
|
|
3. **Use multiple workers**: Scale with CPU cores
|
||
|
|
4. **Enable compression**: Reduce bandwidth usage
|
||
|
|
5. **Connection pooling**: Reuse HTTP connections to backends
|
||
|
|
|
||
|
|
## Troubleshooting
|
||
|
|
|
||
|
|
### Common Issues
|
||
|
|
|
||
|
|
#### Service won't start
|
||
|
|
|
||
|
|
- Check configuration file syntax
|
||
|
|
- Verify token files exist and are readable
|
||
|
|
- Check network connectivity to backends
|
||
|
|
|
||
|
|
#### Extensions not found
|
||
|
|
|
||
|
|
- Verify backend configuration (URL, organization, namespace)
|
||
|
|
- Check backend connectivity with health endpoint
|
||
|
|
- Review logs for authentication errors
|
||
|
|
|
||
|
|
#### Slow responses
|
||
|
|
|
||
|
|
- Check backend latency
|
||
|
|
- Increase cache capacity or TTL
|
||
|
|
- Review Prometheus metrics for bottlenecks
|
||
|
|
|
||
|
|
### Logging
|
||
|
|
|
||
|
|
Enable debug logging:
|
||
|
|
```bash
|
||
|
|
extension-registry --log-level debug
|
||
|
|
```
|
||
|
|
|
||
|
|
Enable JSON logging for structured logs:
|
||
|
|
```bash
|
||
|
|
extension-registry --json-log
|
||
|
|
```
|
||
|
|
|
||
|
|
## License
|
||
|
|
|
||
|
|
Part of the Provisioning Project.
|
||
|
|
|
||
|
|
## Contributing
|
||
|
|
|
||
|
|
See main project documentation for contribution guidelines.
|
||
|
|
|
||
|
|
## Support
|
||
|
|
|
||
|
|
For issues and questions, please refer to the main provisioning project documentation.
|