mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2026-08-31 00:47:15 +02:00
The shipped examples had drifted from the code that parses them: - configs/cluster.json was still in the pre-v3 format: no top-level "name", "numberOfNodes" instead of the required "nodes" node-list expression, plain numbers instead of MetricValue objects for flopRateScalar/flopRateSimd/ memoryBandwidth, unit strings instead of unit objects, "aggregation": null, and hwthread 72 missing from the core topology. It neither validated against cluster.schema.json nor decoded into schema.Cluster. Rewritten in the current format and extended with the accelerator metrics the GPU subclusters need. - archive.retention.location is not a field of taskmanager.Retention. That decode is not strict, so the key was silently dropped and a "move" policy ran with an empty target path. Replaced with target-kind/target-path. - "ui-file" pointed at ui-config.json while the shipped file is uiConfig.json, so ccConfig logged a load error and the UI fell back to defaults. - Added the options introduced since the examples were written: checkpoint file-format/interval/max-wal-size, api-subjects concurrency, db-config, nodestate-retention, archive retention/compression, smoothing-window and the resample policy/algo defaults. The config written by -init had no metric-store section, so a server started right after -init aborted with "missing metricstore configuration". internal/configexample guards all of this: it resolves "-file" references the way ccConfig does, strict-decodes every section into the struct that actually parses it, and validates the cluster configs against the cc-lib schema. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
157 lines
5.1 KiB
Go
157 lines
5.1 KiB
Go
// Copyright (C) NHR@FAU, University Erlangen-Nuremberg.
|
|
// All rights reserved. This file is part of cc-backend.
|
|
// Use of this source code is governed by a MIT-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
// Package configexample guards the shipped example configuration files in
|
|
// ./configs against drift: every section must still be accepted by the code
|
|
// that parses it at startup. The test lives in its own package because it has
|
|
// to import every config-consuming package at once.
|
|
package configexample
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/ClusterCockpit/cc-backend/internal/config"
|
|
"github.com/ClusterCockpit/cc-backend/internal/metricdispatch"
|
|
"github.com/ClusterCockpit/cc-backend/internal/taskmanager"
|
|
"github.com/ClusterCockpit/cc-backend/pkg/metricstore"
|
|
"github.com/ClusterCockpit/cc-backend/web"
|
|
ccschema "github.com/ClusterCockpit/cc-lib/v2/schema"
|
|
)
|
|
|
|
const configDir = "../../configs"
|
|
|
|
// knownSections mirrors the list in cmd/cc-backend/main.go.
|
|
var knownSections = map[string]bool{
|
|
"main": true, "auth": true, "nats": true, "archive": true,
|
|
"metric-store": true, "metric-store-external": true, "cron": true, "ui": true,
|
|
}
|
|
|
|
// archiveConfig covers the union of all archive backend options plus the
|
|
// retention/compression keys consumed by the task manager.
|
|
type archiveConfig struct {
|
|
Kind string `json:"kind"`
|
|
Path string `json:"path"`
|
|
DBPath string `json:"db-path"`
|
|
Endpoint string `json:"endpoint"`
|
|
Bucket string `json:"bucket"`
|
|
Region string `json:"region"`
|
|
UsePathStyle bool `json:"use-path-style"`
|
|
AccessKey string `json:"access-key"`
|
|
SecretKey string `json:"secret-key"`
|
|
Retention taskmanager.Retention `json:"retention"`
|
|
Compression int `json:"compression"`
|
|
}
|
|
|
|
func decodeStrict(t *testing.T, name string, raw json.RawMessage, target any) {
|
|
t.Helper()
|
|
dec := json.NewDecoder(bytes.NewReader(raw))
|
|
dec.DisallowUnknownFields()
|
|
if err := dec.Decode(target); err != nil {
|
|
t.Errorf("%s: %v", name, err)
|
|
}
|
|
}
|
|
|
|
func TestExampleConfigs(t *testing.T) {
|
|
for _, name := range []string{"config.json", "config-demo.json", "config-large.json"} {
|
|
t.Run(name, func(t *testing.T) {
|
|
raw, err := os.ReadFile(filepath.Join(configDir, name))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var sections map[string]json.RawMessage
|
|
if err := json.Unmarshal(raw, §ions); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
for key, value := range sections {
|
|
// ccConfig resolves a "<name>-file" key by loading the
|
|
// referenced file and storing it under "<name>".
|
|
section := key
|
|
if base, ok := strings.CutSuffix(key, "-file"); ok {
|
|
section = base
|
|
var path string
|
|
if err := json.Unmarshal(value, &path); err != nil {
|
|
t.Errorf("%s: value of %q is not a file path: %v", name, key, err)
|
|
continue
|
|
}
|
|
b, err := os.ReadFile(filepath.Join(configDir, path))
|
|
if err != nil {
|
|
t.Errorf("%s: %q references %s: %v", name, key, path, err)
|
|
continue
|
|
}
|
|
value = json.RawMessage(b)
|
|
}
|
|
|
|
if !knownSections[section] {
|
|
t.Errorf("%s: unknown top-level section %q", name, key)
|
|
continue
|
|
}
|
|
|
|
switch section {
|
|
case "main":
|
|
var cfg config.ProgramConfig
|
|
decodeStrict(t, key, value, &cfg)
|
|
case "cron":
|
|
var cfg taskmanager.CronFrequency
|
|
decodeStrict(t, key, value, &cfg)
|
|
case "archive":
|
|
var cfg archiveConfig
|
|
decodeStrict(t, key, value, &cfg)
|
|
case "metric-store":
|
|
var cfg metricstore.MetricStoreConfig
|
|
decodeStrict(t, key, value, &cfg)
|
|
case "metric-store-external":
|
|
var cfg []metricdispatch.CCMetricStoreConfig
|
|
decodeStrict(t, key, value, &cfg)
|
|
case "ui":
|
|
var cfg web.WebConfig
|
|
decodeStrict(t, key, value, &cfg)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExampleClusterConfig(t *testing.T) {
|
|
raw, err := os.ReadFile(filepath.Join(configDir, "cluster.json"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := ccschema.Validate(ccschema.ClusterCfg, bytes.NewReader(raw)); err != nil {
|
|
t.Errorf("cluster.json does not validate: %v", err)
|
|
}
|
|
var cluster ccschema.Cluster
|
|
dec := json.NewDecoder(bytes.NewReader(raw))
|
|
dec.DisallowUnknownFields()
|
|
if err := dec.Decode(&cluster); err != nil {
|
|
t.Errorf("cluster.json: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestArchiveTestdataClusterConfigs keeps the cluster.json fixtures valid
|
|
// against the cc-lib schema. They are copied into real deployments, and an
|
|
// invalid one only fails at runtime with main.validate enabled.
|
|
func TestArchiveTestdataClusterConfigs(t *testing.T) {
|
|
for _, path := range []string{
|
|
"../../pkg/archive/testdata/archive/alex/cluster.json",
|
|
"../../pkg/archive/testdata/archive/fritz/cluster.json",
|
|
"../../pkg/archive/testdata/archive/emmy/cluster.json",
|
|
"../../internal/importer/testdata/cluster-fritz.json",
|
|
} {
|
|
raw, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := ccschema.Validate(ccschema.ClusterCfg, bytes.NewReader(raw)); err != nil {
|
|
t.Errorf("%s does not validate: %v", path, err)
|
|
}
|
|
}
|
|
}
|