Skip to content

Commit 27c80e2

Browse files
jonburdoclaude
andcommitted
Add PVC source support for MCPRegistry
Enables MCPRegistry resources to use PersistentVolumeClaims as a data source in addition to existing ConfigMap, Git, and API sources. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> Signed-off-by: Jon Burdo <[email protected]>
1 parent 5093c91 commit 27c80e2

File tree

11 files changed

+1294
-9
lines changed

11 files changed

+1294
-9
lines changed

cmd/thv-operator/api/v1alpha1/mcpregistry_types.go

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ const (
1616

1717
// RegistrySourceTypeAPI is the type for registry data fetched from API endpoints
1818
RegistrySourceTypeAPI = "api"
19+
20+
// RegistrySourceTypePVC is the type for registry data stored in PersistentVolumeClaims
21+
RegistrySourceTypePVC = "pvc"
1922
)
2023

2124
// Registry formats
@@ -58,20 +61,25 @@ type MCPRegistryConfig struct {
5861
Format string `json:"format,omitempty"`
5962

6063
// ConfigMapRef defines the ConfigMap source configuration
61-
// Mutually exclusive with Git and API
64+
// Mutually exclusive with Git, API, and PVCRef
6265
// +optional
6366
ConfigMapRef *corev1.ConfigMapKeySelector `json:"configMapRef,omitempty"`
6467

6568
// Git defines the Git repository source configuration
66-
// Mutually exclusive with ConfigMapRef and API
69+
// Mutually exclusive with ConfigMapRef, API, and PVCRef
6770
// +optional
6871
Git *GitSource `json:"git,omitempty"`
6972

7073
// API defines the API source configuration
71-
// Mutually exclusive with ConfigMapRef and Git
74+
// Mutually exclusive with ConfigMapRef, Git, and PVCRef
7275
// +optional
7376
API *APISource `json:"api,omitempty"`
7477

78+
// PVCRef defines the PersistentVolumeClaim source configuration
79+
// Mutually exclusive with ConfigMapRef, Git, and API
80+
// +optional
81+
PVCRef *PVCSource `json:"pvcRef,omitempty"`
82+
7583
// SyncPolicy defines the automatic synchronization behavior for this registry.
7684
// If specified, enables automatic synchronization at the given interval.
7785
// Manual synchronization is always supported via annotation-based triggers
@@ -131,6 +139,20 @@ type APISource struct {
131139
Endpoint string `json:"endpoint"`
132140
}
133141

142+
// PVCSource defines PersistentVolumeClaim source configuration
143+
type PVCSource struct {
144+
// ClaimName is the name of the PersistentVolumeClaim
145+
// +kubebuilder:validation:Required
146+
// +kubebuilder:validation:MinLength=1
147+
ClaimName string `json:"claimName"`
148+
149+
// Path is the path to the registry file within the PVC
150+
// +kubebuilder:validation:Pattern=^.*\.json$
151+
// +kubebuilder:default=registry.json
152+
// +optional
153+
Path string `json:"path,omitempty"`
154+
}
155+
134156
// SyncPolicy defines automatic synchronization behavior.
135157
// When specified, enables automatic synchronization at the given interval.
136158
// Manual synchronization via annotation-based triggers is always available

cmd/thv-operator/api/v1alpha1/zz_generated.deepcopy.go

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cmd/thv-operator/pkg/registryapi/config/config.go

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -247,11 +247,16 @@ func validateRegistryNames(registries []mcpv1alpha1.MCPRegistryConfig) error {
247247
}
248248

249249
func buildFilePath(registryName string) *FileConfig {
250+
return buildFilePathWithFilename(registryName, RegistryJSONFileName)
251+
}
252+
253+
func buildFilePathWithFilename(registryName, filename string) *FileConfig {
250254
return &FileConfig{
251-
Path: filepath.Join(RegistryJSONFilePath, registryName, RegistryJSONFileName),
255+
Path: filepath.Join(RegistryJSONFilePath, registryName, filename),
252256
}
253257
}
254258

259+
//nolint:gocyclo // Complexity is acceptable for handling multiple source types
255260
func buildRegistryConfig(registrySpec *mcpv1alpha1.MCPRegistryConfig) (*RegistryConfig, error) {
256261
if registrySpec.Name == "" {
257262
return nil, fmt.Errorf("registry name is required")
@@ -292,12 +297,20 @@ func buildRegistryConfig(registrySpec *mcpv1alpha1.MCPRegistryConfig) (*Registry
292297
}
293298
registryConfig.API = apiConfig
294299
}
300+
if registrySpec.PVCRef != nil {
301+
sourceCount++
302+
filename := RegistryJSONFileName
303+
if registrySpec.PVCRef.Path != "" {
304+
filename = registrySpec.PVCRef.Path
305+
}
306+
registryConfig.File = buildFilePathWithFilename(registrySpec.Name, filename)
307+
}
295308

296309
if sourceCount == 0 {
297-
return nil, fmt.Errorf("exactly one source type (ConfigMapRef, Git, or API) must be specified")
310+
return nil, fmt.Errorf("exactly one source type (ConfigMapRef, Git, API, or PVCRef) must be specified")
298311
}
299312
if sourceCount > 1 {
300-
return nil, fmt.Errorf("only one source type (ConfigMapRef, Git, or API) can be specified")
313+
return nil, fmt.Errorf("only one source type (ConfigMapRef, Git, API, or PVCRef) can be specified")
301314
}
302315

303316
// Build sync policy

cmd/thv-operator/pkg/registryapi/config/config_test.go

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1000,3 +1000,124 @@ func TestBuildConfig_MultipleRegistries(t *testing.T) {
10001000
require.NotNil(t, config.Registries[1].Filter.Names)
10011001
assert.Equal(t, []string{"server-*"}, config.Registries[1].Filter.Names.Include)
10021002
}
1003+
1004+
func TestBuildConfig_PVCSource(t *testing.T) {
1005+
t.Parallel()
1006+
1007+
t.Run("valid pvc source with default path", func(t *testing.T) {
1008+
t.Parallel()
1009+
mcpRegistry := &mcpv1alpha1.MCPRegistry{
1010+
ObjectMeta: metav1.ObjectMeta{
1011+
Name: "test-registry",
1012+
},
1013+
Spec: mcpv1alpha1.MCPRegistrySpec{
1014+
Registries: []mcpv1alpha1.MCPRegistryConfig{
1015+
{
1016+
Name: "pvc-registry",
1017+
Format: mcpv1alpha1.RegistryFormatToolHive,
1018+
PVCRef: &mcpv1alpha1.PVCSource{
1019+
ClaimName: "registry-data-pvc",
1020+
},
1021+
SyncPolicy: &mcpv1alpha1.SyncPolicy{
1022+
Interval: "1h",
1023+
},
1024+
},
1025+
},
1026+
},
1027+
}
1028+
1029+
manager := NewConfigManagerForTesting(mcpRegistry)
1030+
config, err := manager.BuildConfig()
1031+
1032+
require.NoError(t, err)
1033+
require.NotNil(t, config)
1034+
assert.Equal(t, "test-registry", config.RegistryName)
1035+
require.Len(t, config.Registries, 1)
1036+
assert.Equal(t, "pvc-registry", config.Registries[0].Name)
1037+
assert.Equal(t, mcpv1alpha1.RegistryFormatToolHive, config.Registries[0].Format)
1038+
require.NotNil(t, config.Registries[0].File)
1039+
assert.Equal(t, filepath.Join(RegistryJSONFilePath, "pvc-registry", RegistryJSONFileName), config.Registries[0].File.Path)
1040+
})
1041+
1042+
t.Run("valid pvc source with custom path", func(t *testing.T) {
1043+
t.Parallel()
1044+
mcpRegistry := &mcpv1alpha1.MCPRegistry{
1045+
ObjectMeta: metav1.ObjectMeta{
1046+
Name: "test-registry",
1047+
},
1048+
Spec: mcpv1alpha1.MCPRegistrySpec{
1049+
Registries: []mcpv1alpha1.MCPRegistryConfig{
1050+
{
1051+
Name: "custom-pvc",
1052+
Format: mcpv1alpha1.RegistryFormatToolHive,
1053+
PVCRef: &mcpv1alpha1.PVCSource{
1054+
ClaimName: "registry-data-pvc",
1055+
Path: "custom-registry.json",
1056+
},
1057+
SyncPolicy: &mcpv1alpha1.SyncPolicy{
1058+
Interval: "30m",
1059+
},
1060+
},
1061+
},
1062+
},
1063+
}
1064+
1065+
manager := NewConfigManagerForTesting(mcpRegistry)
1066+
config, err := manager.BuildConfig()
1067+
1068+
require.NoError(t, err)
1069+
require.NotNil(t, config)
1070+
require.Len(t, config.Registries, 1)
1071+
assert.Equal(t, "custom-pvc", config.Registries[0].Name)
1072+
require.NotNil(t, config.Registries[0].File)
1073+
// Path uses the custom path specified in PVCRef.Path
1074+
assert.Equal(t, filepath.Join(RegistryJSONFilePath, "custom-pvc", "custom-registry.json"), config.Registries[0].File.Path)
1075+
})
1076+
1077+
t.Run("valid pvc source with filter", func(t *testing.T) {
1078+
t.Parallel()
1079+
mcpRegistry := &mcpv1alpha1.MCPRegistry{
1080+
ObjectMeta: metav1.ObjectMeta{
1081+
Name: "test-registry",
1082+
},
1083+
Spec: mcpv1alpha1.MCPRegistrySpec{
1084+
Registries: []mcpv1alpha1.MCPRegistryConfig{
1085+
{
1086+
Name: "filtered-pvc",
1087+
Format: mcpv1alpha1.RegistryFormatToolHive,
1088+
PVCRef: &mcpv1alpha1.PVCSource{
1089+
ClaimName: "registry-data-pvc",
1090+
Path: "registry.json",
1091+
},
1092+
SyncPolicy: &mcpv1alpha1.SyncPolicy{
1093+
Interval: "15m",
1094+
},
1095+
Filter: &mcpv1alpha1.RegistryFilter{
1096+
NameFilters: &mcpv1alpha1.NameFilter{
1097+
Include: []string{"prod-*"},
1098+
},
1099+
Tags: &mcpv1alpha1.TagFilter{
1100+
Include: []string{"production"},
1101+
},
1102+
},
1103+
},
1104+
},
1105+
},
1106+
}
1107+
1108+
manager := NewConfigManagerForTesting(mcpRegistry)
1109+
config, err := manager.BuildConfig()
1110+
1111+
require.NoError(t, err)
1112+
require.NotNil(t, config)
1113+
require.Len(t, config.Registries, 1)
1114+
assert.Equal(t, "filtered-pvc", config.Registries[0].Name)
1115+
require.NotNil(t, config.Registries[0].File)
1116+
// Verify filter is preserved
1117+
require.NotNil(t, config.Registries[0].Filter)
1118+
require.NotNil(t, config.Registries[0].Filter.Names)
1119+
assert.Equal(t, []string{"prod-*"}, config.Registries[0].Filter.Names.Include)
1120+
require.NotNil(t, config.Registries[0].Filter.Tags)
1121+
assert.Equal(t, []string{"production"}, config.Registries[0].Filter.Tags.Include)
1122+
})
1123+
}

0 commit comments

Comments
 (0)