Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/verify-generated.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ jobs:
- name: Verify RBAC is up-to-date
run: make verify-rbac

- name: Verify feature status is up-to-date
run: make verify-feature-status

# Save PR number so the comment workflow (running with write access in the
# base-repo context) can look it up — artifact is the only safe channel
# across the pull_request / workflow_run boundary for fork PRs.
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Binaries for programs and plugins
/feature-status-gen
*.exe
*.exe~
*.dll
Expand Down
10 changes: 10 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,16 @@ verify-rbac: ## Verify RBAC matches generated (for CI)
@rm -f /tmp/generated-rbac.yaml
@echo "✓ RBAC is up-to-date"

.PHONY: generate-feature-status
generate-feature-status: ## Generate feature status table and JSON from metadata
@echo "Generating feature status from metadata..."
@go run cmd/feature-status-gen/main.go

.PHONY: verify-feature-status
verify-feature-status: ## Verify feature status matches generated (for CI)
@echo "Verifying feature status is up-to-date..."
@go run cmd/feature-status-gen/main.go --dry-run

.PHONY: update-crds
update-crds: ## Update CRD collection from upstream
hack/update-crds.sh
Expand Down
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,27 @@ The autopilot applies opinionated best practices and manages your platform autom

**Install once. Run VMs. Customize via GitOps when needed.**

## Features

<!-- BEGIN FEATURE STATUS -->
> **Note:** The autopilot framework is currently **DP** and requires `platform.kubevirt.io/autopilot=true` on the HCO CR. Feature maturity levels below are relative to an enabled autopilot.

| Feature | Maturity | Install | Opt-in | Requires | Recommended |
|---------|----------|---------|--------|----------|-------------|
| In-Flight Operations | TP | always | - | - | - |
| Kubelet Performance | TP | always | - | - | - |
| Load-Aware Descheduler | TP | always | - | Kube Descheduler Operator | - |
| Observability | TP | always | - | Cluster Observability Operator | - |
| SWAP | TP | always | - | - | - |
| CPU Manager | DP | opt-in | `featureGate:CPUManager` | - | - |
| Kernel Samepage Merging (KSM) zero pages only | DP | opt-in | `platform.kubevirt.io/enable-ksm-zero-only=true, hcoUnconfigured:spec.virtualization.ksmConfiguration` | - | - |
| KubeVirt Metrics Exporter | DP | opt-in | `platform.kubevirt.io/enable-metrics-exporter=true` | - | Cluster Observability Operator |
| MTV Operator | DP | opt-in | `platform.kubevirt.io/enable-mtv=true` | - | - |
| MetalLB Operator | DP | opt-in | `platform.kubevirt.io/enable-metallb=true` | - | - |
| PCI Passthrough | DP | opt-in | `platform.kubevirt.io/openshift=true, platform.kubevirt.io/enable-pci-passthrough=true` | - | - |
| Transparent Huge Pages (THP) Tuning | DP | opt-in | `platform.kubevirt.io/enable-thp-tuning=true` | - | - |
<!-- END FEATURE STATUS -->

## Quick Start

### Prerequisites
Expand Down
87 changes: 87 additions & 0 deletions assets/active/metadata.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ assets:
value: "true"
- type: hardware-detection
detector: pciDevicesPresent
- type: annotation
key: platform.kubevirt.io/enable-pci-passthrough
value: "true"

- name: psi-enable
group: descheduler-loadaware
Expand Down Expand Up @@ -738,3 +741,87 @@ assets:
# - node-maintenance-operator (operators/node-maintenance.yaml.tpl)
# - fence-agents-operator (operators/fence-agents.yaml.tpl)
# - usb-passthrough (machine-config/05-usb-passthrough.yaml.tpl)

# Framework-level maturity. Remove opt_in once the autopilot graduates to GA.
framework:
maturity: dp
opt_in: platform.kubevirt.io/autopilot=true

# Feature catalog: maps user-facing features to assets/groups.
# - maturity is required only for opt-in features ("dp" or "tp")
# - features whose assets are all install: always are auto-classified as GA
# - opt-in conditions are derived from the referenced assets (no duplication)
# - every asset must be covered by a feature entry or listed in excluded_assets

# Assets intentionally excluded from feature tracking (e.g. internal-only,
# not yet user-facing, or managed externally).
excluded_assets:
- hco-golden-config

features:
- name: SWAP
description: OpenShift worker node swap support
maturity: tp
assets: [swap-enable]

- name: Kubelet Performance
description: Optimized kubelet settings for virtualization workloads
maturity: tp
assets: [kubelet-perf-settings]

- name: Observability
description: Prometheus metrics, alerting rules, and Perses dashboards in the OpenShift console
maturity: tp
assets: [metrics-service, metrics-servicemonitor, prometheus-alerts, monitoring-ui-plugin, autopilot-dashboard]
requires:
- Cluster Observability Operator

- name: Load-Aware Descheduler
description: Load-aware VM balancing based on CPU/memory utilization and pressure stall metrics
maturity: tp
assets: [descheduler-loadaware, psi-enable, psi-enable-master]
requires:
- Kube Descheduler Operator

- name: PCI Passthrough
description: GPU/PCI device passthrough via VFIO
maturity: dp
assets: [pci-passthrough]

- name: CPU Manager
description: Dedicated CPU pinning for guaranteed QoS workloads
maturity: dp
assets: [kubelet-cpu-manager]

- name: MTV Operator
description: Migration Toolkit for Virtualization
maturity: dp
assets: [mtv-operator]

- name: MetalLB Operator
description: Bare-metal load balancer for services
maturity: dp
assets: [metallb-operator]

- name: KubeVirt Metrics Exporter
description: Per-node VM storage I/O latency collection via QMP, QGA, and eBPF, and detailed KVM and memory statistics
maturity: dp
groups: [metrics-exporter]
recommended:
- Cluster Observability Operator

- name: In-Flight Operations
description: OperationRuleSet-based coordination for safe concurrent operations
maturity: tp
groups: [inflightoperations]

- name: Kernel Samepage Merging (KSM) zero pages only
description: Node-level KSM tuning that enables zero-pages-only deduplication with adaptive scan rate
maturity: dp
groups: [ksm-zero-only]

- name: Transparent Huge Pages (THP) Tuning
description: Node-level THP tuning that sets madvise mode and khugepaged scan rate for KVM guest memory
maturity: dp
groups: [thp-tuning]

231 changes: 231 additions & 0 deletions cmd/feature-status-gen/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
/*
Copyright 2026 The KubeVirt Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

// feature-status-gen reads the features catalog from assets/active/metadata.yaml,
// derives maturity levels, and generates:
// - docs/generated/feature-status.json (structured data for CI)
// - README.md section between sentinel comments (human-readable table)
//
// Run via 'make generate-feature-status'.
package main

import (
"encoding/json"
"flag"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"

"sigs.k8s.io/yaml"

"github.com/kubevirt/virt-platform-autopilot/pkg/assets"
)

const (
metadataPath = "assets/active/metadata.yaml"
jsonOutput = "docs/generated/feature-status.json"
readmePath = "README.md"
beginMarker = "<!-- BEGIN FEATURE STATUS -->"
endMarker = "<!-- END FEATURE STATUS -->"
)

func main() {
dryRun := flag.Bool("dry-run", false, "Write to /tmp and diff against committed files instead of writing in place")
flag.Parse()

catalog, err := loadCatalog()
if err != nil {
fmt.Fprintf(os.Stderr, "Error loading metadata: %v\n", err)
os.Exit(1)
}

if err := assets.ValidateFeatureCoverage(catalog); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}

output, err := assets.DeriveFeatureCatalog(catalog)
if err != nil {
fmt.Fprintf(os.Stderr, "Error deriving feature statuses: %v\n", err)
os.Exit(1)
}

jsonData, err := json.MarshalIndent(output, "", " ")
if err != nil {
fmt.Fprintf(os.Stderr, "Error marshalling JSON: %v\n", err)
os.Exit(1)
}
jsonData = append(jsonData, '\n')

table := renderTable(output.Framework, output.Features)

if *dryRun {
failed := false

tmpJSON := "/tmp/generated-feature-status.json"
if err := os.WriteFile(tmpJSON, jsonData, 0644); err != nil {
fmt.Fprintf(os.Stderr, "Error writing %s: %v\n", tmpJSON, err)
os.Exit(1)
}
defer os.Remove(tmpJSON)

if diffErr := runDiff(jsonOutput, tmpJSON); diffErr != nil {
failed = true
}

tmpReadme := "/tmp/generated-readme-features.md"
if err := os.WriteFile(tmpReadme, []byte(table), 0644); err != nil {
fmt.Fprintf(os.Stderr, "Error writing %s: %v\n", tmpReadme, err)
os.Exit(1)
}
defer os.Remove(tmpReadme)

currentSection, err := extractSentinelSection(readmePath)
if err != nil {
fmt.Fprintf(os.Stderr, "Error extracting README section: %v\n", err)
failed = true
} else {
tmpCurrent := "/tmp/current-readme-features.md"
if err := os.WriteFile(tmpCurrent, []byte(currentSection), 0644); err != nil {
fmt.Fprintf(os.Stderr, "Error writing %s: %v\n", tmpCurrent, err)
os.Exit(1)
}
defer os.Remove(tmpCurrent)

if diffErr := runDiff(tmpCurrent, tmpReadme); diffErr != nil {
failed = true
}
}

if failed {
fmt.Fprintln(os.Stderr, "")
fmt.Fprintln(os.Stderr, "Feature status is out of date.")
fmt.Fprintln(os.Stderr, " 1. Run: make generate-feature-status")
fmt.Fprintln(os.Stderr, " 2. Commit the updated files")
os.Exit(1)
}

fmt.Println("Feature status is up-to-date.")
return
}

if err := os.MkdirAll(filepath.Dir(jsonOutput), 0755); err != nil {
fmt.Fprintf(os.Stderr, "Error creating directory: %v\n", err)
os.Exit(1)
}
if err := os.WriteFile(jsonOutput, jsonData, 0644); err != nil {
fmt.Fprintf(os.Stderr, "Error writing %s: %v\n", jsonOutput, err)
os.Exit(1)
}
fmt.Printf("Wrote %s\n", jsonOutput)

if err := injectIntoReadme(readmePath, table); err != nil {
fmt.Fprintf(os.Stderr, "Error updating %s: %v\n", readmePath, err)
os.Exit(1)
}
fmt.Printf("Updated %s\n", readmePath)
}

func loadCatalog() (*assets.AssetCatalog, error) {
data, err := os.ReadFile(metadataPath)
if err != nil {
return nil, err
}
catalog := &assets.AssetCatalog{}
if err := yaml.Unmarshal(data, catalog); err != nil {
return nil, err
}
return catalog, nil
}

func renderTable(fw assets.FrameworkStatus, statuses []assets.FeatureStatus) string {
var b strings.Builder

if fw.Maturity != "GA" && fw.OptIn != nil {
fmt.Fprintf(&b, "> **Note:** The autopilot framework is currently **%s** and requires `%s` on the HCO CR. Feature maturity levels below are relative to an enabled autopilot.\n\n", fw.Maturity, *fw.OptIn)
}

b.WriteString("| Feature | Maturity | Install | Opt-in | Requires | Recommended |\n")
b.WriteString("|---------|----------|---------|--------|----------|-------------|\n")

for _, s := range statuses {
optIn := "-"
if s.OptIn != nil {
optIn = "`" + *s.OptIn + "`"
}
requires := "-"
if len(s.Requires) > 0 {
requires = strings.Join(s.Requires, ", ")
}
recommended := "-"
if len(s.Recommended) > 0 {
recommended = strings.Join(s.Recommended, ", ")
}
fmt.Fprintf(&b, "| %s | %s | %s | %s | %s | %s |\n", s.Name, s.Maturity, s.Install, optIn, requires, recommended)
}

return b.String()
}

func extractSentinelSection(path string) (string, error) {
data, err := os.ReadFile(path)
if err != nil {
return "", err
}
content := string(data)

beginIdx := strings.Index(content, beginMarker)
endIdx := strings.Index(content, endMarker)
if beginIdx < 0 || endIdx < 0 || endIdx <= beginIdx {
return "", fmt.Errorf("sentinel markers not found in %s", path)
}

section := content[beginIdx+len(beginMarker) : endIdx]
section = strings.TrimPrefix(section, "\n")
section = strings.TrimSuffix(section, "\n")
return section + "\n", nil
}

func injectIntoReadme(path, table string) error {
data, err := os.ReadFile(path)
if err != nil {
return err
}
content := string(data)

beginIdx := strings.Index(content, beginMarker)
endIdx := strings.Index(content, endMarker)
if beginIdx < 0 || endIdx < 0 || endIdx <= beginIdx {
return fmt.Errorf("sentinel markers not found in %s", path)
}

var b strings.Builder
b.WriteString(content[:beginIdx+len(beginMarker)])
b.WriteString("\n")
b.WriteString(table)
b.WriteString(content[endIdx:])

return os.WriteFile(path, []byte(b.String()), 0644)
}

func runDiff(fileA, fileB string) error {
cmd := exec.Command("diff", "-u", fileA, fileB)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
2 changes: 1 addition & 1 deletion cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ func newRunCommand() *cobra.Command {
cmd.Flags().DurationVar(&crdValidationTimeout, "crd-validation-timeout", 10*time.Second,
"Timeout for validating that required CRDs exist at startup.")
cmd.Flags().BoolVar(&enableDebugServer, "enable-debug-server", true,
"Enable debug HTTP server with /debug/render and /debug/exclusions endpoints.")
"Enable debug HTTP server with /debug/render, /debug/exclusions, and /debug/features endpoints.")
cmd.Flags().BoolVar(&development, "development", true,
"Enable development mode logging.")

Expand Down
Loading