461 lines
22 KiB
Markdown
Executable File
461 lines
22 KiB
Markdown
Executable File
# Cockpit Kata Containers
|
|
|
|
A [Cockpit](https://cockpit-project.org/) module for managing [Kata Containers](https://katacontainers.io/) — lightweight VM-based container sandboxes that provide hardware-level isolation for container workloads.
|
|
|
|
  
|
|
|
|
## What It Does
|
|
|
|
Cockpit Kata Containers brings the same usability-first approach as Cockpit's built-in **machines** module (libvirt VM management) to Kata Containers. It provides a unified interface for:
|
|
|
|
- **Sandbox Management** — List, inspect, start, stop, and restart Kata sandboxes across all VMM backends (QEMU, Cloud Hypervisor, Firecracker, Dragonball)
|
|
- **Image Import** — Import rootfs tarballs, initrd images, and **QCrows** self-describing VM container archives with a multi-step validation wizard that checks for kata-agent, init systems, resolv.conf, size limits, format compliance, and archive integrity
|
|
- **Image Export** — Export QCrows images to **qcow2** disk images, **ISO** for live USB media, or **PXE** for network boot, directly from the Cockpit UI with format-specific configuration options
|
|
- **Kernel Management** — Inspect guest kernel configs, validate Kata-required options, review boot parameters, and regenerate initrd with environment-aware VMM module selection
|
|
- **Initrd Regeneration** — Rebuild initrd with precise awareness of the VMM transport layer, kernel compression support, and rootfs init style — unlike `dracut` or `mkinitramfs`, produces an initrd where kata-agent IS the final process
|
|
- **PXE Push** — Deploy QCrows images to TFTP servers for network boot with a one-click button, automatic dnsmasq configuration, and squashfs rootfs packing
|
|
- **Build Guide** — Step-by-step walkthrough for building compatible custom Kata container images (rootfs and initrd), with copyable commands and progress tracking
|
|
- **Runtime Configuration** — View and edit `configuration.toml` files for each Kata runtime profile with live TOML editing and structured/raw view toggle
|
|
- **Metrics Dashboard** — Real-time resource utilization from kata-monitor's Prometheus endpoint with per-sandbox CPU, memory, and network breakdowns
|
|
|
|
## Architecture
|
|
|
|
```
|
|
cockpit-kata-containers/
|
|
├── README.md # This file
|
|
├── quickstart.md # Quick-start installation guide
|
|
├── installer.sh # Automated OS-aware installer
|
|
├── LICENSE # Apache-2.0
|
|
├── Caddyfile # Reverse proxy config for standalone mode
|
|
├── qcrows-spec.md # QCrows format specification
|
|
├── qcrows-pack # CLI tool to create .qcrows archives
|
|
├── qcrows-verify # CLI tool to verify .qcrows integrity
|
|
├── qcrows-inspect # CLI tool to read .qcrows metadata
|
|
├── qcrows-export # CLI tool to export .qcrows to qcow2, ISO, or PXE
|
|
├── qcrows-initrd-regen # CLI tool to regenerate initrd (env-aware)
|
|
│
|
|
├── cockpit-kata/ # Production Cockpit module (webpack build)
|
|
│ ├── manifest.json # Cockpit package manifest
|
|
│ ├── webpack.config.js # Bundler config — outputs to dist/
|
|
│ ├── package.json # Build dependencies (React 18, webpack 5)
|
|
│ ├── tsconfig.json # TypeScript config
|
|
│ ├── cockpit.d.ts # Type declarations for cockpit.js APIs
|
|
│ └── src/
|
|
│ └── index.tsx # Entry point — mounts KataModule
|
|
│
|
|
├── src/ # Next.js prototype + shared components
|
|
│ ├── app/
|
|
│ │ ├── page.tsx # Renders KataModule
|
|
│ │ ├── layout.tsx # Dark theme root layout
|
|
│ │ ├── globals.css # Tailwind CSS base
|
|
│ │ └── api/kata/
|
|
│ │ ├── validate/route.ts # Pre-upload validation endpoint
|
|
│ │ └── upload/route.ts # File upload with server-side checks
|
|
│ ├── components/kata/
|
|
│ │ ├── KataModule.tsx # Main shell — sidebar + top bar + routing
|
|
│ │ ├── SandboxList.tsx # Sandbox table with summary cards
|
|
│ │ ├── SandboxDetail.tsx # Per-sandbox detail with 4 tabs
|
|
│ │ ├── ImportImage.tsx # 4-step import wizard + QCrows kernel validation
|
|
│ │ ├── ExportImage.tsx # Export to qcow2/ISO/PXE with format-specific config
|
|
│ │ ├── KernelDetail.tsx # Kernel config viewer + boot params + initrd regen
|
|
│ │ ├── BuildGuide.tsx # Rootfs/initrd build instructions
|
|
│ │ ├── Configuration.tsx # Runtime config viewer + TOML editor
|
|
│ │ └── MetricsDashboard.tsx # Aggregate + per-sandbox metrics
|
|
│ ├── lib/
|
|
│ │ ├── kata-mock.ts # Mock data layer + TypeScript interfaces
|
|
│ │ ├── backend/
|
|
│ │ │ ├── adapter.ts # Auto-detect cockpit vs mock backend
|
|
│ │ │ └── cockpit-api.ts # Real cockpit.spawn/http/file calls
|
|
│ │ └── utils.ts # cn() utility, shared helpers
|
|
│ └── hooks/
|
|
│ ├── use-toast.ts # Toast notifications
|
|
│ └── use-mobile.ts # Responsive breakpoint hook
|
|
│
|
|
├── package.json # Next.js project config
|
|
├── tsconfig.json # Root TypeScript config
|
|
├── tailwind.config.ts # Tailwind CSS config
|
|
├── postcss.config.mjs # PostCSS config
|
|
├── next.config.ts # Next.js config
|
|
├── eslint.config.mjs # ESLint config
|
|
└── components.json # shadcn/ui component registry
|
|
```
|
|
|
|
## Two Deployment Modes
|
|
|
|
### Mode 1: Cockpit Module (Production)
|
|
|
|
The `cockpit-kata/` directory builds into a self-contained Cockpit package that drops into `/usr/share/cockpit/kata/`. It uses:
|
|
|
|
- `cockpit.spawn()` — runs `kata-runtime`, `crictl`, `kata-ctl` on the host
|
|
- `cockpit.http()` — queries `kata-monitor` on `127.0.0.1:8090` for Prometheus metrics
|
|
- `cockpit.file()` — reads/writes `configuration.toml` with atomic TOML round-tripping
|
|
|
|
Build and install:
|
|
|
|
```bash
|
|
cd cockpit-kata
|
|
npm install
|
|
npm run build
|
|
sudo cp -r dist/ /usr/share/cockpit/kata/
|
|
```
|
|
|
|
### Mode 2: Standalone Web App (Development/Demo)
|
|
|
|
The Next.js prototype runs independently with mock data, no Kata or Cockpit installation required:
|
|
|
|
```bash
|
|
npm install
|
|
npm run dev
|
|
# Open http://localhost:3000
|
|
```
|
|
|
|
The backend adapter (`src/lib/backend/adapter.ts`) auto-detects whether `cockpit` is available in the global scope and routes API calls to either the real Cockpit backend or the mock data layer. This means the same UI components work in both modes without modification.
|
|
|
|
## QCrows — Self-Describing VM Container Images
|
|
|
|
QCrows (pronounced *cue-crows*) is a self-describing container image format designed for VM-based container runtimes like Kata Containers. A `.qcrows` file bundles everything needed to deploy a VM container into a single, verifiable archive.
|
|
|
|
### What's in a QCrows Archive
|
|
|
|
| Component | Required | Description |
|
|
|-----------|----------|-------------|
|
|
| `metadata.toml` | Yes | Image manifest — name, version, arch, hypervisor compat, kernel info, agent version |
|
|
| `menu.toml` | Yes | Cockpit UI menu entry — label, category, init system, workload tags |
|
|
| `hashes.sha256` | Yes | SHA-256 integrity verification for all files |
|
|
| `rootfs.tar.*` | Yes | Guest OS filesystem (tar.gz, tar.xz, or tar.zst) |
|
|
| `initrd.*` | One of | Initial ramdisk (cpio-gzip, cpio-lz4, cpio-xz, or cramfs) |
|
|
| `kernel/` | **REQUIRED** | Guest kernel binary + config — eliminates kernel-rootfs mismatch |
|
|
| `boot-params.conf` | Optional | Per-image kernel command line parameters |
|
|
| `build.toml` | Recommended | Build provenance — build system, flags, source hashes |
|
|
| `spec.md` | Optional | Build guide for reproducing or modifying the image |
|
|
|
|
### Build System Support
|
|
|
|
The `qcrows-pack` tool auto-detects and supports:
|
|
|
|
| Build System | Package Manager | Typical Use |
|
|
|-------------|----------------|-------------|
|
|
| Gentoo | Portage | Full control, hardened builds |
|
|
| Source Mage (Sorcery) | cast/dispel | Source-based, spell system |
|
|
| Buildroot | make + kconfig | Embedded, cross-compilation |
|
|
| Lunar Linux | lin/lrm | Source-based, rolling |
|
|
| LEDE / OpenWrt | opkg + make | Routers, embedded, IoT |
|
|
| BTC | custom | Niche / custom pipeline |
|
|
|
|
### Creating a QCrows Image
|
|
|
|
```bash
|
|
# From individual build outputs
|
|
qcrows-pack \
|
|
--rootfs ./output/rootfs.tar.gz \
|
|
--initrd ./output/initrd.img \
|
|
--kernel ./output/bzImage \
|
|
--kernel-config ./output/.config \
|
|
--name "alpine-3.20-kata" --version "3.20.1" \
|
|
--build-system buildroot \
|
|
--hypervisors "qemu,cloud-hypervisor" \
|
|
-o alpine-3.20-kata.qcrows
|
|
|
|
# From a Buildroot output directory (auto-discovery)
|
|
qcrows-pack --from-dir ./buildroot/output/ \
|
|
--name "buildroot-kata" --build-system buildroot \
|
|
-o buildroot-kata.qcrows
|
|
|
|
# Verify before importing
|
|
qcrows-verify alpine-3.20-kata.qcrows
|
|
|
|
# Inspect metadata without extraction
|
|
qcrows-inspect alpine-3.20-kata.qcrows
|
|
|
|
# Export to qcow2 for QEMU direct boot
|
|
qcrows-export --format qcow2 alpine-3.20-kata.qcrows -o alpine-3.20-kata.qcow2
|
|
|
|
# Export to bootable ISO for live USB
|
|
qcrows-export --format iso alpine-3.20-kata.qcrows -o alpine-live.iso
|
|
dd if=alpine-live.iso of=/dev/sdX bs=4M status=progress && sync
|
|
```
|
|
|
|
### Importing in Cockpit
|
|
|
|
1. Click **Import Image** in the sidebar
|
|
2. Select **QCrows Image** as the import mode
|
|
3. Drop a `.qcrows` file — the module auto-detects the format
|
|
4. The 11-step validator runs: extract → verify hashes → parse metadata → parse menu → check compatibility → validate kernel binary → validate kernel config → validate rootfs → validate initrd → validate boot-params → register
|
|
5. Review the parsed metadata (name, version, arch, kernel, hypervisors, agent) and the Cockpit menu entry preview
|
|
6. Click **Import QCrows Image**
|
|
|
|
See [qcrows-spec.md](qcrows-spec.md) for the complete format specification.
|
|
|
|
### Exporting QCrows Images
|
|
|
|
The **Export** view in the Cockpit UI and the `qcrows-export` CLI tool convert `.qcrows` archives into bootable formats:
|
|
|
|
**From the Cockpit UI:** Click **Export** in the sidebar → select a QCrows image → choose target format (qcow2, ISO, or PXE) → configure options → click **Export**. The UI provides format-specific options (EFI boot, disk size, partition labels, server IP), a progress indicator, and a usage hint on completion.
|
|
|
|
**From the CLI:**
|
|
|
|
**qcow2** — QEMU disk image with partitioned layout, rootfs installed, kernel in `/boot`, and bootloader configured:
|
|
```bash
|
|
# Auto-sized qcow2 with MBR boot
|
|
qcrows-export --format qcow2 image.qcrows -o disk.qcow2
|
|
|
|
# 2GB disk with EFI partition layout
|
|
qcrows-export --format qcow2 --disk-size 2G --efi image.qcrows -o disk-efi.qcow2
|
|
|
|
# Boot the result
|
|
qemu-system-x86_64 -m 1G -smp 2 -drive file=disk.qcow2,format=qcow2 -enable-kvm
|
|
```
|
|
|
|
**ISO** — Bootable live ISO for USB media, including a `/kata/` directory for host-side deployment:
|
|
```bash
|
|
# MBR-bootable (hybrid — dd-able to USB)
|
|
qcrows-export --format iso image.qcrows -o live.iso
|
|
|
|
# EFI-bootable
|
|
qcrows-export --format iso --efi image.qcrows -o live-efi.iso
|
|
|
|
# Write to USB
|
|
dd if=live.iso of=/dev/sdX bs=4M status=progress && sync
|
|
```
|
|
|
|
**PXE** — Push the image to a TFTP directory for network boot via dnsmasq:
|
|
```bash
|
|
# Deploy to TFTP with PXE configuration
|
|
qcrows-export --format pxe image.qcrows --server-ip 192.168.1.1
|
|
|
|
# Or use the "Push via PXE" button in the Import view
|
|
```
|
|
|
|
| Format | Boot Mode | Use Case | Prerequisites | UI Access |
|
|
|--------|-----------|----------|---------------|-----------|
|
|
| qcow2 | MBR/extlinux or EFI/GRUB | Standalone QEMU VM | qemu-img, parted, mkfs.ext4 | Export view |
|
|
| iso | MBR/isolinux or EFI/GRUB | Live USB, Kata deployment from media | xorriso, syslinux | Export view |
|
|
| pxe | pxelinux + dnsmasq | Network boot, bulk provisioning | dnsmasq, squashfs-tools | Export view + Import view PXE button |
|
|
|
|
### Initrd Regeneration
|
|
|
|
The `qcrows-initrd-regen` tool rebuilds the initrd component of a QCrows bundle with **environment awareness**. Unlike `dracut` or `mkinitramfs`, it produces an initrd where kata-agent IS the final process (no pivot_root) and includes ONLY the kernel modules required by the detected VMM transport layer.
|
|
|
|
**Auto-detection pipeline:**
|
|
1. **VMM** — Reads `kata-runtime` symlink and `configuration.toml` to determine active hypervisor
|
|
2. **Compression** — Inspects kernel `.config` for `CONFIG_RD_LZ4/GZIP/XZ/ZSTD`, selects fastest supported
|
|
3. **Init style** — Examines rootfs for `/sbin/init` (systemd), `/sbin/openrc-init` (OpenRC), or neither (busybox)
|
|
4. **kata-agent** — Scans rootfs for the agent binary at standard paths
|
|
5. **Modules** — Selects kernel modules from the VMM-specific matrix (QEMU: virtio-pci, CLH: virtio-mmio, Firecracker: virtio-mmio minimal, Dragonball: vsock-direct)
|
|
|
|
```bash
|
|
# Auto-detect everything from the host environment
|
|
qcrows-initrd-regen alpine-3.20-kata.qcrows
|
|
|
|
# Target QEMU with lz4 compression
|
|
qcrows-initrd-regen alpine.qcrows --vmm qemu --compress lz4
|
|
|
|
# Update the bundle in-place
|
|
qcrows-initrd-regen alpine.qcrows --in-place --vmm cloud-hypervisor
|
|
|
|
# Or use the "Regen Initrd" button in the Kernel Management view
|
|
```
|
|
|
|
## Features in Detail
|
|
|
|
### Sandbox Management
|
|
|
|
The sandbox list view mirrors the Cockpit machines module's VM list pattern:
|
|
- Summary cards at the top (Total Sandboxes, Running, Total vCPU, Total Memory)
|
|
- Searchable, filterable table with status badges, VMM type, and boot time
|
|
- Action dropdown per sandbox (Start / Stop / Restart / Console)
|
|
- Click-through to detail view with breadcrumb navigation back
|
|
|
|
The detail view provides:
|
|
- **Overview tab** — Identity (name, namespace, UID, runtime class) and Runtime (VMM, kernel, agent, vCPU, memory)
|
|
- **Metrics tab** — CPU/memory usage with sparklines, VMM API call counts, network I/O
|
|
- **Network tab** — vsock connection details for kata-agent communication
|
|
- **Console tab** — Text terminal to the kata-agent inside the guest VM
|
|
|
|
### Image Import
|
|
|
|
A 4-step wizard guides users through importing Kata container images:
|
|
|
|
1. **Select** — Choose image type (Rootfs Tarball or Initrd Image) and drag-and-drop the file
|
|
2. **Configure** — Set image name and hypervisor compatibility (QEMU / CLH / Firecracker / Dragonball)
|
|
3. **Validate** — Automatic checks for format compliance, required files (init, kata-agent, resolv.conf), size limits, and security concerns
|
|
4. **Complete** — Import progress with stage messages and validation summary
|
|
|
|
Validation rules include:
|
|
| Rule | Applies To | Severity |
|
|
|------|-----------|----------|
|
|
| Valid tar archive | rootfs | error |
|
|
| Init system present | rootfs | error |
|
|
| kata-agent binary | rootfs | error |
|
|
| resolv.conf present | rootfs | warning |
|
|
| Valid initrd format | initrd | error |
|
|
| Size limits | both | warning |
|
|
| No setuid binaries | rootfs | warning |
|
|
| Kernel modules | rootfs | warning |
|
|
|
|
### Build Guide
|
|
|
|
Step-by-step instructions for creating compatible custom images:
|
|
|
|
**Rootfs (6 steps):** Choose distro → Install kata-agent → Configure init → Set up networking → Optimize size → Package as tar.gz
|
|
|
|
**Initrd (5 steps):** Choose base → Install kata-agent → Configure init → Optimize → Package as cpio archive
|
|
|
|
Each step includes copyable command blocks, important notes, and warnings. A comparison card explains when to use rootfs vs initrd.
|
|
|
|
### Kernel Management
|
|
|
|
The **Kernel** view provides a dedicated interface for inspecting guest kernels bundled in QCrows images:
|
|
|
|
- **Image selector** — Sidebar listing all imported QCrows images with kernel version, format, and size at a glance
|
|
- **Kernel overview cards** — Version, format (vmlinuz/vmlinux), size, and config validation status
|
|
- **Hypervisor compatibility** — Matrix showing which VMMs support the kernel's format (vmlinuz for QEMU/x86_64, vmlinux for CLH/Firecracker/Dragonball)
|
|
- **Kernel config viewer** — Required Kata options (VSOCKETS, VIRTIO, DEVTMPFS) with pass/fail status, recommended options with coverage metrics, total config statistics (built-in vs module count)
|
|
- **Boot parameters** — Kernel command line from `boot-params.conf`, parameter breakdown, and security warnings for dangerous options (e.g., `module.sig_enforce=0`)
|
|
- **Initrd Regeneration** — Rebuild initrd with VMM-aware module selection, configurable compression, init style, and in-place bundle update
|
|
- **Build provenance** — Compiler, defconfig, build ID, and import file paths
|
|
|
|
The kernel view is especially valuable for source-based distro builds where the kernel is compiled alongside the rootfs — it provides assurance that the kernel config has all required Kata options enabled before deployment.
|
|
|
|
### Configuration
|
|
|
|
View and edit Kata runtime configuration files (`configuration.toml`) for each VMM profile:
|
|
- Config selector sidebar listing all profiles (kata-qemu, kata-clh, kata-fc, kata-dragonball)
|
|
- Structured view showing paths, default resources, and boot mode
|
|
- Raw TOML editor with save/cancel and unsaved changes detection
|
|
- Discard confirmation dialog when navigating away from unsaved edits
|
|
|
|
### Metrics Dashboard
|
|
|
|
Real-time monitoring powered by `kata-monitor`'s Prometheus endpoint:
|
|
- Aggregate stats with sparklines (CPU, Memory, Network RX/TX)
|
|
- Per-sandbox metric cards with progress bars
|
|
- Empty state with setup instructions when kata-monitor is not running
|
|
|
|
## Backend Adapter Pattern
|
|
|
|
The module uses a unified API layer that works in both modes:
|
|
|
|
```
|
|
UI Components
|
|
│
|
|
▼
|
|
adapter.ts ──→ isCockpitAvailable() ?
|
|
│ │
|
|
▼ YES ▼ NO
|
|
cockpit-api.ts kata-mock.ts
|
|
│ │
|
|
▼ ▼
|
|
cockpit.spawn() simulateDelay()
|
|
cockpit.http() MOCK_SANDBOXES
|
|
cockpit.file() MOCK_METRICS
|
|
```
|
|
|
|
All functions return the same TypeScript types (`Sandbox`, `ImportedImage`, `RuntimeConfig`, `SandboxMetrics`), so UI components never need to know which backend is active.
|
|
|
|
## Prerequisites
|
|
|
|
### For Cockpit Module (Production)
|
|
|
|
| Component | Minimum Version | Purpose |
|
|
|-----------|----------------|---------|
|
|
| Cockpit | 286+ | Web management framework |
|
|
| kata-runtime | 2.5+ | Kata Containers runtime |
|
|
| containerd | 1.6+ | CRI container runtime |
|
|
| crictl | 1.25+ | CRI CLI tool |
|
|
| kata-monitor | (optional) | Prometheus metrics endpoint |
|
|
| KVM | enabled | Hardware virtualization |
|
|
|
|
### For Standalone Mode (Development)
|
|
|
|
| Component | Minimum Version | Purpose |
|
|
|-----------|----------------|---------|
|
|
| Node.js | 18+ | JavaScript runtime |
|
|
| npm or bun | latest | Package manager |
|
|
|
|
## Quick Install
|
|
|
|
The fastest way to get running:
|
|
|
|
```bash
|
|
# Clone and enter the project
|
|
git clone <repo-url> cockpit-kata-containers
|
|
cd cockpit-kata-containers
|
|
|
|
# Run the automated installer
|
|
chmod +x installer.sh
|
|
sudo ./installer.sh
|
|
```
|
|
|
|
The installer detects your OS, installs all dependencies, installs QCrows CLI tools to `/usr/local/bin/`, builds the Cockpit module, and starts required services. See [quickstart.md](quickstart.md) for the manual approach.
|
|
|
|
## Development
|
|
|
|
### Standalone Mode (Mock Data)
|
|
|
|
```bash
|
|
npm install
|
|
npm run dev
|
|
# → http://localhost:3000
|
|
```
|
|
|
|
### Cockpit Module Development
|
|
|
|
```bash
|
|
cd cockpit-kata
|
|
npm install
|
|
npm run watch # Auto-rebuild on changes
|
|
npm run devel-install # Symlink to ~/.local/share/cockpit/kata
|
|
```
|
|
|
|
Then open Cockpit in your browser and navigate to the "Kata Containers" tool.
|
|
|
|
### Building for Production
|
|
|
|
```bash
|
|
cd cockpit-kata
|
|
npm run build
|
|
sudo cp -r dist/ /usr/share/cockpit/kata/
|
|
|
|
# Verify installation
|
|
cockpit-bridge --packages | grep kata
|
|
```
|
|
|
|
## CSP Requirements
|
|
|
|
The `manifest.json` includes a Content Security Policy that allows:
|
|
|
|
- `connect-src 'self' http://127.0.0.1:8090 ws:` — kata-monitor HTTP and WebSocket
|
|
- `style-src 'self' 'unsafe-inline'` — React inline styles
|
|
- `img-src 'self' data:` — inline SVG icons and data URIs
|
|
|
|
If kata-monitor binds to a different address, update the CSP in `cockpit-kata/manifest.json`.
|
|
|
|
## Comparison with Cockpit Machines Module
|
|
|
|
| Aspect | Machines Module | Kata Module |
|
|
|--------|----------------|-------------|
|
|
| Object model | Persistent libvirt VMs | Ephemeral Kata sandboxes (per-pod lifecycle) |
|
|
| Creation flow | In-module wizard (disk, RAM, OS) | Via Kubernetes RuntimeClass |
|
|
| Import feature | Disk images (qcow2, raw) | Rootfs tarballs, initrd images, QCrows bundles |
|
|
| Export feature | — | qcow2, ISO, PXE (UI + CLI) |
|
|
| Initrd regen | — | Environment-aware VMM module selection |
|
|
| Config format | libvirt XML | TOML (configuration.toml) |
|
|
| Console access | VNC (graphical) | vsock text terminal |
|
|
| Metrics source | libvirt API | kata-monitor Prometheus endpoint |
|
|
| List view | VM table + status | Sandbox table + status (same pattern) |
|
|
| Action menus | Start/Stop/Restart/Console | Start/Stop/Restart/Console (same pattern) |
|
|
| Status badges | Color-coded running/stopped | Color-coded running/stopped/error |
|
|
|
|
## Contributing
|
|
|
|
1. Fork the repository
|
|
2. Create a feature branch (`git checkout -b feature/my-feature`)
|
|
3. Commit your changes (`git commit -am 'Add my feature'`)
|
|
4. Push to the branch (`git push origin feature/my-feature`)
|
|
5. Open a Pull Request
|
|
|
|
## License
|
|
|
|
Apache-2.0 — see [LICENSE](LICENSE) for details.
|