Skip to content
Open
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
2 changes: 1 addition & 1 deletion .github/workflows/dashboard-updater.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:

- name: Check for update
run: |
./hco/automation/dashboard-updater/dashboard-updater.sh "./monitoring/dashboards/openshift" "./hco/assets/dashboards"
./hco/automation/dashboard-updater/dashboard-updater.sh "./monitoring/dashboards/openshift" "./hco/assets/dashboards/grafana"
cd hco
git add --all
if ! git diff HEAD --quiet --exit-code; then
Expand Down
13 changes: 13 additions & 0 deletions assets/dashboards/perses/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Place Perses CR manifests here (YAML files) to be applied by the observability controller.

Expected kinds (apiVersion subject to the Observability Operator installed in the cluster):
- PersesDashboard (e.g. apiVersion: observability.rhobs/v1alpha1)
- PersesDatasource (e.g. apiVersion: observability.rhobs/v1alpha1)

Manifests will be server-side applied into the operator namespace at runtime.

Example: memory-load dashboard
- Source: https://github.com/fabiand/perses-dashboards/blob/main/manifests/memory-load.yaml
- Copy the file here as `memory-load.yaml`. The controller will override `metadata.namespace` to the operator namespace.


5 changes: 5 additions & 0 deletions controllers/observability/observability_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ func (r *Reconciler) Reconcile(ctx context.Context, _ ctrl.Request) (ctrl.Result
errors = append(errors, err)
}

// Apply Perses dashboards/datasources shipped with the operator, if any
if err := r.ReconcilePersesResources(ctx); err != nil {
errors = append(errors, err)
}

if len(errors) > 0 {
err := fmt.Errorf("reconciliation failed: %v", errors)
log.Error(err, "Reconciliation failed")
Expand Down
95 changes: 95 additions & 0 deletions controllers/observability/perses.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package observability

import (
"context"
"encoding/json"
"fmt"
"io/fs"
"os"
"path/filepath"

"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/yaml"
)

// persesAssetsDir is the directory (inside the repo image) containing Perses CR manifests (yaml).
// Each YAML should define a single resource: PersesDashboard or PersesDatasource.
const persesAssetsDir = "assets/dashboards/perses"

// ReconcilePersesResources applies all Perses resources shipped with the operator under assets/perses.
// It uses server-side apply to create/update resources idempotently without relying on cache reads.
func (r *Reconciler) ReconcilePersesResources(ctx context.Context) error {
wd, err := os.Getwd()
if err != nil {
return fmt.Errorf("failed to get working dir: %w", err)
}

d := os.DirFS(wd)
// If the directory does not exist, nothing to do.
if _, err := fs.Stat(d, persesAssetsDir); err != nil {
return nil
}

return fs.WalkDir(d, persesAssetsDir, func(path string, entry fs.DirEntry, err error) error {
if err != nil {
return err
}
if entry.IsDir() {
return nil
}
if ext := filepath.Ext(entry.Name()); ext != ".yml" && ext != ".yaml" {
return nil
}

file, err := d.Open(path)
if err != nil {
return fmt.Errorf("failed to open %s: %w", path, err)
}
defer file.Close()

content, err := fs.ReadFile(d, path)
if err != nil {
return fmt.Errorf("failed to read %s: %w", path, err)
}

// Decode YAML → JSON → map
jsonBytes, err := yaml.YAMLToJSON(content)
if err != nil {
return fmt.Errorf("failed to convert yaml to json for %s: %w", path, err)
}

var objMap map[string]any
if err := json.Unmarshal(jsonBytes, &objMap); err != nil {
return fmt.Errorf("failed to unmarshal json for %s: %w", path, err)
}

// Enforce namespace to operator's namespace to ensure ownership & GC
// If the manifest specifies a namespace, it will be overridden.
// We only support namespaced resources here.
metadata, _ := objMap["metadata"].(map[string]any)
if metadata == nil {
metadata = map[string]any{}
objMap["metadata"] = metadata
}
metadata["namespace"] = r.namespace

// Build Unstructured with GVK populated
apiVersion, _ := objMap["apiVersion"].(string)
kind, _ := objMap["kind"].(string)
if apiVersion == "" || kind == "" {
return fmt.Errorf("missing apiVersion/kind in %s", path)
}

u := &unstructured.Unstructured{Object: objMap}
u.SetGroupVersionKind(schema.FromAPIVersionAndKind(apiVersion, kind))

// Apply with SSA so we don't need to read existing objects
if err := r.Patch(ctx, u, client.Apply, client.FieldOwner("hyperconverged-operator"), client.ForceOwnership); err != nil {
return fmt.Errorf("failed to apply %s: %w", path, err)
}

return nil
})
}