Skip to content

Commit 5d4ccb4

Browse files
sradcoAI Assistantcursoragent
committed
Add Perses single-cluster dashboard generation
Import dashboard builders from stolostron/ multicluster-observability-addon and transform them for single-cluster CNV deployments. The transformation removes cluster variables, strips cluster label matchers from PromQL, removes cluster table columns, rewrites dashboard links, replaces datasource names, and renames recording rules for OCP 4.22+. Includes: - cmd/generate-perses-dashboards CLI - pkg/dashboards/transform package with tests - Generated YAML for 5 dashboards - CI workflow for verify on PRs - Daily sync workflow for upstream updates - Makefile targets: generate, verify, test The generator supports both multicluster builders (from MCOA, transformed) and local single-cluster builders (no transform) via the multicluster flag on builderEntry. Signed-off-by: Shirly Radco <sradco@redhat.com> Co-authored-by: AI Assistant <noreply@cursor.com> Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 725060d commit 5d4ccb4

14 files changed

Lines changed: 5364 additions & 12 deletions
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
name: Sync Perses Dashboards
2+
3+
on:
4+
schedule:
5+
- cron: '0 6 * * *'
6+
workflow_dispatch:
7+
8+
permissions:
9+
contents: write
10+
pull-requests: write
11+
12+
jobs:
13+
sync:
14+
name: Check for upstream dashboard updates
15+
runs-on: ubuntu-latest
16+
steps:
17+
- uses: actions/checkout@v4
18+
19+
- uses: actions/setup-go@v5
20+
with:
21+
go-version-file: go.mod
22+
23+
- name: Update upstream dependency
24+
run: |
25+
go get github.com/stolostron/multicluster-observability-addon@latest
26+
go mod tidy
27+
28+
- name: Regenerate dashboards
29+
run: make generate-perses-dashboards
30+
31+
- name: Check for changes
32+
id: changes
33+
run: |
34+
if [ -n "$(git status --porcelain dashboards/perses/ go.mod go.sum)" ]; then
35+
echo "changed=true" >> "$GITHUB_OUTPUT"
36+
else
37+
echo "changed=false" >> "$GITHUB_OUTPUT"
38+
fi
39+
40+
- name: Create pull request
41+
if: steps.changes.outputs.changed == 'true'
42+
uses: peter-evans/create-pull-request@v7
43+
with:
44+
branch: automated/sync-perses-dashboards
45+
commit-message: "Update Perses dashboards from upstream"
46+
title: "chore: sync Perses dashboards from upstream"
47+
body: |
48+
Automated update of Perses single-cluster dashboards.
49+
50+
Source: `github.com/stolostron/multicluster-observability-addon/pkg/perses/dashboards/virtualization`
51+
52+
Generated by the daily sync workflow.
53+
labels: automated
54+
delete-branch: true
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
name: Perses Dashboards
2+
3+
on:
4+
push:
5+
branches: [main]
6+
paths:
7+
- '.github/workflows/perses-dashboards.yaml'
8+
- 'Makefile'
9+
- 'cmd/generate-perses-dashboards/**'
10+
- 'pkg/dashboards/**'
11+
- 'dashboards/perses/**'
12+
- 'go.mod'
13+
- 'go.sum'
14+
pull_request:
15+
branches: [main]
16+
paths:
17+
- '.github/workflows/perses-dashboards.yaml'
18+
- 'Makefile'
19+
- 'cmd/generate-perses-dashboards/**'
20+
- 'pkg/dashboards/**'
21+
- 'dashboards/perses/**'
22+
- 'go.mod'
23+
- 'go.sum'
24+
workflow_dispatch:
25+
26+
jobs:
27+
verify:
28+
name: Verify Perses Dashboards
29+
runs-on: ubuntu-latest
30+
steps:
31+
- uses: actions/checkout@v4
32+
33+
- uses: actions/setup-go@v5
34+
with:
35+
go-version-file: go.mod
36+
37+
- name: Run unit tests
38+
run: make test-perses-dashboards
39+
40+
- name: Verify generated dashboards are up-to-date
41+
run: make verify-perses-dashboards

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
**/_out
22
bin/
3+
/generate-perses-dashboards

Makefile

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,20 @@ runbook-sync-downstream: build-runbook-sync-downstream
4848
.PHONY: build-runbook-preview
4949
build-runbook-preview:
5050
cd tools/runbook-sync-downstream && go build -ldflags="-s -w" -o _out/runbook-preview ./cmd/preview/
51+
52+
.PHONY: generate-perses-dashboards
53+
generate-perses-dashboards:
54+
go run ./cmd/generate-perses-dashboards/... --out dashboards/perses
55+
56+
.PHONY: verify-perses-dashboards
57+
verify-perses-dashboards: generate-perses-dashboards
58+
@if [ -n "$$(git status --porcelain dashboards/perses/)" ]; then \
59+
echo "ERROR: dashboards/perses/ is out of date. Run 'make generate-perses-dashboards' and commit the result."; \
60+
git status dashboards/perses/; \
61+
git diff dashboards/perses/; \
62+
exit 1; \
63+
fi
64+
65+
.PHONY: test-perses-dashboards
66+
test-perses-dashboards:
67+
go test ./pkg/dashboards/transform/...
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"flag"
6+
"fmt"
7+
"log"
8+
"os"
9+
"path/filepath"
10+
11+
"github.com/perses/perses/go-sdk/dashboard"
12+
"github.com/stolostron/multicluster-observability-addon/pkg/perses/dashboards/virtualization"
13+
14+
"github.com/kubevirt/monitoring/pkg/dashboards/transform"
15+
16+
k8syaml "sigs.k8s.io/yaml"
17+
)
18+
19+
type persesResource struct {
20+
APIVersion string `json:"apiVersion"`
21+
Kind string `json:"kind"`
22+
Metadata map[string]string `json:"metadata"`
23+
Spec json.RawMessage `json:"spec"`
24+
}
25+
26+
type builderEntry struct {
27+
name string
28+
fn func(string, string) (dashboard.Builder, error)
29+
multicluster bool // true = source is multicluster; apply ToSingleCluster transform
30+
}
31+
32+
func includedDashboardNames(builders []builderEntry, prefix string) []string {
33+
names := make([]string, len(builders))
34+
for i, b := range builders {
35+
names[i] = prefix + "-" + b.name
36+
}
37+
return names
38+
}
39+
40+
func buildDashboardSpec(b builderEntry, project string) ([]byte, error) {
41+
db, err := b.fn(project, "rbac-query-proxy-datasource")
42+
if err != nil {
43+
return nil, fmt.Errorf("building dashboard: %w", err)
44+
}
45+
return json.Marshal(db.Dashboard.Spec)
46+
}
47+
48+
func wrapAsResource(name, namespace string, spec json.RawMessage) ([]byte, error) {
49+
resource := persesResource{
50+
APIVersion: "perses.dev/v1alpha1",
51+
Kind: "PersesDashboard",
52+
Metadata: map[string]string{
53+
"name": name,
54+
"namespace": namespace,
55+
},
56+
Spec: spec,
57+
}
58+
59+
resourceJSON, err := json.Marshal(resource)
60+
if err != nil {
61+
return nil, fmt.Errorf("marshalling resource: %w", err)
62+
}
63+
return k8syaml.JSONToYAML(resourceJSON)
64+
}
65+
66+
func main() {
67+
prefix := flag.String("prefix", "cnv", "dashboard ID prefix")
68+
displayPrefix := flag.String("display-prefix", "OpenShift Virtualization", "display name prefix (replaces 'Virtualization')")
69+
outDir := flag.String("out", "dashboards/perses", "output directory for generated YAML")
70+
namespace := flag.String("namespace", "openshift-cnv", "namespace for PersesDashboard CRs")
71+
flag.Parse()
72+
73+
builders := []builderEntry{
74+
{"node-memory-overview", virtualization.BuildNodeMemoryOverview, true},
75+
{"virtual-machines-inventory", virtualization.BuildVMInventory, true},
76+
{"virtual-machines-utilization", virtualization.BuildVMUtilization, true},
77+
{"virtual-machines-service-level", virtualization.BuildVMServiceLevel, true},
78+
{"virtual-machines-by-time-in-status", virtualization.BuildVMByTimeInStatus, true},
79+
}
80+
81+
cfg := transform.Config{
82+
DashboardPrefix: *prefix,
83+
DisplayNamePrefix: *displayPrefix,
84+
IncludedDashboards: includedDashboardNames(builders, *prefix),
85+
}
86+
87+
if err := os.MkdirAll(*outDir, 0o755); err != nil {
88+
log.Fatalf("Failed to create output directory: %v", err)
89+
}
90+
91+
for _, b := range builders {
92+
specJSON, err := buildDashboardSpec(b, *namespace)
93+
if err != nil {
94+
log.Fatalf("Failed to build %s: %v", b.name, err)
95+
}
96+
97+
var outputSpec json.RawMessage
98+
if b.multicluster {
99+
outputSpec, err = transform.ToSingleCluster(specJSON, cfg)
100+
if err != nil {
101+
log.Fatalf("Failed to transform %s: %v", b.name, err)
102+
}
103+
} else {
104+
outputSpec = specJSON
105+
}
106+
107+
dashboardName := *prefix + "-" + b.name
108+
yamlBytes, err := wrapAsResource(dashboardName, *namespace, outputSpec)
109+
if err != nil {
110+
log.Fatalf("Failed to generate YAML for %s: %v", b.name, err)
111+
}
112+
113+
outFile := filepath.Join(*outDir, dashboardName+".yaml")
114+
if err := os.WriteFile(outFile, yamlBytes, 0o644); err != nil {
115+
log.Fatalf("Failed to write %s: %v", outFile, err)
116+
}
117+
fmt.Printf("Generated: %s\n", outFile)
118+
}
119+
}

0 commit comments

Comments
 (0)