-
Notifications
You must be signed in to change notification settings - Fork 277
Expand file tree
/
Copy pathruntime_config_drift_test.go
More file actions
126 lines (115 loc) · 4.54 KB
/
Copy pathruntime_config_drift_test.go
File metadata and controls
126 lines (115 loc) · 4.54 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
// SPDX-FileCopyrightText: Copyright 2025 Stacklok, Inc.
// SPDX-License-Identifier: Apache-2.0
package spectoconfig
import (
"reflect"
"sort"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stacklok/toolhive/cmd/thv-operator/internal/testutil"
vmcpconfig "github.com/stacklok/toolhive/pkg/vmcp/config"
vmcpruntimeconfig "github.com/stacklok/toolhive/pkg/vmcp/config/runtime"
)
// runtimeOnlyLeafJustifications enumerates leaf paths that exist on
// runtime.Config but NOT on vmcpconfig.Config. Each entry must include a
// justification explaining why the field is operator-resolved and therefore
// must not appear in the public CRD schema.
//
// Today runtime.Config adds nothing extra, so this map is empty. The drift
// test below fails if anyone adds a field to runtime.Config without
// allowlisting it here, which is the forcing function that turns "I'll
// just stick this on the runtime wrapper" into a deliberate review
// conversation.
//
// When future operator-resolved fields land (e.g. a backendHeaderForward
// map populated from MCPServerEntry.spec.headerForward), add the leaf path
// here with a justification that names the operator code that populates it.
var runtimeOnlyLeafJustifications = map[string]string{}
// TestRuntimeConfigSeam guards the wrapper relationship between
// vmcpconfig.Config (the public CRD-visible type) and runtime.Config (the
// operator-write surface):
//
// - runtime.Config MUST contain every vmcpconfig.Config leaf. runtime.Config
// embeds vmcpconfig.Config inline, so today this is automatic — but the
// test pins it so a future refactor that, say, replaces the embed with a
// reference would fail loudly instead of silently dropping fields from
// the ConfigMap.
//
// - Every leaf on runtime.Config that is NOT on vmcpconfig.Config must
// appear in runtimeOnlyLeafJustifications with an explanation. These
// are operator-resolved sidecars (secret-identifier maps, resolved file
// paths, etc.) that travel through the ConfigMap to the vMCP runtime
// without becoming public API surface.
//
// - Conversely, no vmcpconfig.Config leaf may be allowlisted in
// runtimeOnlyLeafJustifications — that would be a contradiction.
//
// The test catches the mistake the wrapper exists to prevent: adding an
// operator-resolved field directly onto vmcpconfig.Config (where it leaks
// into the CRD) instead of onto runtime.Config.
func TestRuntimeConfigSeam(t *testing.T) {
t.Parallel()
configLeaves := setOf(testutil.FlattenJSONLeafFields(reflect.TypeOf(vmcpconfig.Config{})))
runtimeLeaves := setOf(testutil.FlattenJSONLeafFields(reflect.TypeOf(vmcpruntimeconfig.Config{})))
t.Run("RuntimeConfig is a superset of Config", func(t *testing.T) {
t.Parallel()
var missing []string
for leaf := range configLeaves {
if _, ok := runtimeLeaves[leaf]; !ok {
missing = append(missing, leaf)
}
}
sort.Strings(missing)
assert.Empty(
t, missing,
"every vmcpconfig.Config leaf must also be a leaf on runtime.Config (runtime.Config embeds vmcpconfig.Config inline). "+
"Missing: %v", missing,
)
})
t.Run("RuntimeConfig extras are justified", func(t *testing.T) {
t.Parallel()
for leaf := range runtimeLeaves {
if _, onConfig := configLeaves[leaf]; onConfig {
continue
}
reason, ok := runtimeOnlyLeafJustifications[leaf]
if !ok {
t.Errorf(
"runtime.Config leaf %q is not present on vmcpconfig.Config and is not allowlisted "+
"in runtimeOnlyLeafJustifications.\n"+
"Action: if this field is operator-resolved (must NOT appear in the CRD), "+
"add it to runtimeOnlyLeafJustifications with a justification. "+
"Otherwise, move it onto Config so the CRD picks it up.",
leaf,
)
continue
}
require.NotEmptyf(t, reason, "runtimeOnlyLeafJustifications[%q] must include a justification", leaf)
}
})
t.Run("allowlist has no stale or self-contradicting entries", func(t *testing.T) {
t.Parallel()
for leaf := range runtimeOnlyLeafJustifications {
if _, onConfig := configLeaves[leaf]; onConfig {
t.Errorf(
"%q is allowlisted as runtime-only but also exists on Config — contradicting classification",
leaf,
)
}
if _, onRuntime := runtimeLeaves[leaf]; !onRuntime {
t.Errorf(
"%q is allowlisted as runtime-only but is not a live leaf on runtime.Config — stale entry?",
leaf,
)
}
}
})
}
func setOf(s []string) map[string]struct{} {
out := make(map[string]struct{}, len(s))
for _, v := range s {
out[v] = struct{}{}
}
return out
}