Skip to content

Commit 5093c91

Browse files
authored
adds dedicated podtemplatespec builder (#2754)
* adds podtemplatespec builder Signed-off-by: Chris Burns <[email protected]> * moves more code Signed-off-by: Chris Burns <[email protected]> * podtemplatetest Signed-off-by: Chris Burns <[email protected]> --------- Signed-off-by: Chris Burns <[email protected]>
1 parent 62d4d57 commit 5093c91

File tree

5 files changed

+690
-257
lines changed

5 files changed

+690
-257
lines changed

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

Lines changed: 21 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,7 @@ import (
99
appsv1 "k8s.io/api/apps/v1"
1010
corev1 "k8s.io/api/core/v1"
1111
"k8s.io/apimachinery/pkg/api/errors"
12-
"k8s.io/apimachinery/pkg/api/resource"
1312
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
14-
"k8s.io/apimachinery/pkg/util/intstr"
1513
"sigs.k8s.io/controller-runtime/pkg/client"
1614
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
1715
"sigs.k8s.io/controller-runtime/pkg/log"
@@ -79,11 +77,7 @@ func (m *manager) ensureDeployment(
7977
ctxLogger := log.FromContext(ctx).WithValues("mcpregistry", mcpRegistry.Name)
8078

8179
// Build the desired deployment configuration
82-
deployment, err := m.buildRegistryAPIDeployment(mcpRegistry, configManager)
83-
if err != nil {
84-
ctxLogger.Error(err, "Failed to build deployment configuration")
85-
return nil, fmt.Errorf("failed to build deployment configuration: %w", err)
86-
}
80+
deployment := m.buildRegistryAPIDeployment(mcpRegistry, configManager)
8781
deploymentName := deployment.Name
8882

8983
// Set owner reference for automatic garbage collection
@@ -94,7 +88,7 @@ func (m *manager) ensureDeployment(
9488

9589
// Check if deployment already exists
9690
existing := &appsv1.Deployment{}
97-
err = m.client.Get(ctx, client.ObjectKey{
91+
err := m.client.Get(ctx, client.ObjectKey{
9892
Name: deploymentName,
9993
Namespace: mcpRegistry.Namespace,
10094
}, existing)
@@ -124,16 +118,31 @@ func (m *manager) ensureDeployment(
124118
// buildRegistryAPIDeployment creates and configures a Deployment object for the registry API.
125119
// This function handles all deployment configuration including labels, container specs, probes,
126120
// and storage manager integration. It returns a fully configured deployment ready for Kubernetes API operations.
127-
func (m *manager) buildRegistryAPIDeployment(
121+
func (*manager) buildRegistryAPIDeployment(
128122
mcpRegistry *mcpv1alpha1.MCPRegistry,
129123
configManager config.ConfigManager,
130-
) (*appsv1.Deployment, error) {
124+
) *appsv1.Deployment {
131125
// Generate deployment name using the established pattern
132126
deploymentName := mcpRegistry.GetAPIResourceName()
133127

134128
// Define labels using common function
135129
labels := labelsForRegistryAPI(mcpRegistry, deploymentName)
136130

131+
// Build the PodTemplateSpec using the functional options pattern
132+
// This includes all mount configurations via the builder pattern
133+
builder := NewPodTemplateSpecBuilder()
134+
podTemplateSpec := builder.Apply(
135+
WithLabels(labels),
136+
WithAnnotations(map[string]string{
137+
"toolhive.stacklok.dev/config-hash": "hash-dummy-value",
138+
}),
139+
WithServiceAccountName(DefaultServiceAccountName),
140+
WithContainer(BuildRegistryAPIContainer(getRegistryAPIImage())),
141+
WithRegistryServerConfigMount(registryAPIContainerName, configManager.GetRegistryServerConfigMapName()),
142+
WithRegistrySourceMounts(registryAPIContainerName, mcpRegistry.Spec.Registries),
143+
WithRegistryStorageMount(registryAPIContainerName),
144+
).Build()
145+
137146
// Create basic deployment specification with named container
138147
deployment := &appsv1.Deployment{
139148
ObjectMeta: metav1.ObjectMeta{
@@ -149,82 +158,11 @@ func (m *manager) buildRegistryAPIDeployment(
149158
"app.kubernetes.io/component": "registry-api",
150159
},
151160
},
152-
Template: corev1.PodTemplateSpec{
153-
ObjectMeta: metav1.ObjectMeta{
154-
Labels: labels,
155-
Annotations: map[string]string{
156-
"toolhive.stacklok.dev/config-hash": "hash-dummy-value",
157-
},
158-
},
159-
Spec: corev1.PodSpec{
160-
ServiceAccountName: DefaultServiceAccountName,
161-
Containers: []corev1.Container{
162-
{
163-
Name: registryAPIContainerName,
164-
Image: getRegistryAPIImage(),
165-
Args: []string{
166-
ServeCommand,
167-
},
168-
Ports: []corev1.ContainerPort{
169-
{
170-
ContainerPort: RegistryAPIPort,
171-
Name: RegistryAPIPortName,
172-
Protocol: corev1.ProtocolTCP,
173-
},
174-
},
175-
// Add resource limits and requests for production readiness
176-
Resources: corev1.ResourceRequirements{
177-
Requests: corev1.ResourceList{
178-
corev1.ResourceCPU: resource.MustParse(DefaultCPURequest),
179-
corev1.ResourceMemory: resource.MustParse(DefaultMemoryRequest),
180-
},
181-
Limits: corev1.ResourceList{
182-
corev1.ResourceCPU: resource.MustParse(DefaultCPULimit),
183-
corev1.ResourceMemory: resource.MustParse(DefaultMemoryLimit),
184-
},
185-
},
186-
// Add liveness and readiness probes
187-
LivenessProbe: &corev1.Probe{
188-
ProbeHandler: corev1.ProbeHandler{
189-
HTTPGet: &corev1.HTTPGetAction{
190-
Path: HealthCheckPath,
191-
Port: intstr.FromInt32(RegistryAPIPort),
192-
},
193-
},
194-
InitialDelaySeconds: LivenessInitialDelay,
195-
PeriodSeconds: LivenessPeriod,
196-
},
197-
ReadinessProbe: &corev1.Probe{
198-
ProbeHandler: corev1.ProbeHandler{
199-
HTTPGet: &corev1.HTTPGetAction{
200-
Path: ReadinessCheckPath,
201-
Port: intstr.FromInt32(RegistryAPIPort),
202-
},
203-
},
204-
InitialDelaySeconds: ReadinessInitialDelay,
205-
PeriodSeconds: ReadinessPeriod,
206-
},
207-
},
208-
},
209-
},
210-
},
161+
Template: podTemplateSpec,
211162
},
212163
}
213164

214-
// Configure storage-specific aspects using the new inverted dependency approach
215-
if err := m.configureRegistryServerConfigMounts(deployment, registryAPIContainerName, configManager); err != nil {
216-
return nil, fmt.Errorf("failed to configure registry server config mounts: %w", err)
217-
}
218-
219-
if err := m.configureRegistrySourceMounts(deployment, mcpRegistry, registryAPIContainerName); err != nil {
220-
return nil, fmt.Errorf("failed to configure registry source mounts: %w", err)
221-
}
222-
223-
if err := m.configureRegistryStorageMounts(deployment, registryAPIContainerName); err != nil {
224-
return nil, fmt.Errorf("failed to configure registry storage mounts: %w", err)
225-
}
226-
227-
return deployment, nil
165+
return deployment
228166
}
229167

230168
// getRegistryAPIImage returns the registry API container image to use
@@ -240,33 +178,3 @@ func getRegistryAPIImageWithEnvGetter(envGetter func(string) string) string {
240178
}
241179
return "ghcr.io/stacklok/thv-registry-api:latest"
242180
}
243-
244-
// findContainerByName finds a container by name in a slice of containers
245-
func findContainerByName(containers []corev1.Container, name string) *corev1.Container {
246-
for i := range containers {
247-
if containers[i].Name == name {
248-
return &containers[i]
249-
}
250-
}
251-
return nil
252-
}
253-
254-
// hasVolume checks if a volume with the given name exists in the volumes slice
255-
func hasVolume(volumes []corev1.Volume, name string) bool {
256-
for _, volume := range volumes {
257-
if volume.Name == name {
258-
return true
259-
}
260-
}
261-
return false
262-
}
263-
264-
// hasVolumeMount checks if a volume mount with the given name exists in the volume mounts slice
265-
func hasVolumeMount(volumeMounts []corev1.VolumeMount, name string) bool {
266-
for _, mount := range volumeMounts {
267-
if mount.Name == name {
268-
return true
269-
}
270-
}
271-
return false
272-
}

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,8 +134,7 @@ func TestManagerBuildRegistryAPIDeployment(t *testing.T) {
134134
manager := &manager{}
135135

136136
configManager := config.NewConfigManagerForTesting(tt.mcpRegistry)
137-
deployment, err := manager.buildRegistryAPIDeployment(tt.mcpRegistry, configManager)
138-
require.NoError(t, err)
137+
deployment := manager.buildRegistryAPIDeployment(tt.mcpRegistry, configManager)
139138
tt.validateResult(t, deployment)
140139
})
141140
}

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

Lines changed: 0 additions & 142 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,8 @@ package registryapi
33
import (
44
"context"
55
"fmt"
6-
"path/filepath"
76

87
appsv1 "k8s.io/api/apps/v1"
9-
corev1 "k8s.io/api/core/v1"
108
"k8s.io/apimachinery/pkg/runtime"
119
"sigs.k8s.io/controller-runtime/pkg/client"
1210
"sigs.k8s.io/controller-runtime/pkg/log"
@@ -127,146 +125,6 @@ func (m *manager) IsAPIReady(ctx context.Context, mcpRegistry *mcpv1alpha1.MCPRe
127125
return m.CheckAPIReadiness(ctx, deployment)
128126
}
129127

130-
func (*manager) configureRegistryServerConfigMounts(
131-
deployment *appsv1.Deployment,
132-
containerName string,
133-
configManager config.ConfigManager,
134-
) error {
135-
136-
// Find the container by name
137-
container := findContainerByName(deployment.Spec.Template.Spec.Containers, containerName)
138-
if container == nil {
139-
return fmt.Errorf("container '%s' not found in deployment", containerName)
140-
}
141-
142-
// Replace container args completely with the correct set of arguments
143-
// This ensures idempotent behavior across multiple reconciliations
144-
container.Args = []string{
145-
ServeCommand,
146-
fmt.Sprintf("--config=%s", filepath.Join(config.RegistryServerConfigFilePath, config.RegistryServerConfigFileName)),
147-
}
148-
149-
// Add ConfigMap volume to deployment if not already present
150-
configVolumeName := RegistryServerConfigVolumeName
151-
if !hasVolume(deployment.Spec.Template.Spec.Volumes, configVolumeName) {
152-
configVolume := corev1.Volume{
153-
Name: configVolumeName,
154-
VolumeSource: corev1.VolumeSource{
155-
ConfigMap: &corev1.ConfigMapVolumeSource{
156-
LocalObjectReference: corev1.LocalObjectReference{
157-
Name: configManager.GetRegistryServerConfigMapName(),
158-
},
159-
},
160-
},
161-
}
162-
deployment.Spec.Template.Spec.Volumes = append(deployment.Spec.Template.Spec.Volumes, configVolume)
163-
}
164-
165-
// Add volume mount to the container if not already present
166-
if !hasVolumeMount(container.VolumeMounts, configVolumeName) {
167-
volumeMount := corev1.VolumeMount{
168-
Name: configVolumeName,
169-
MountPath: config.RegistryServerConfigFilePath, // Mount to directory, not the file path
170-
ReadOnly: true, // ConfigMaps are always read-only anyway
171-
}
172-
container.VolumeMounts = append(container.VolumeMounts, volumeMount)
173-
}
174-
175-
return nil
176-
}
177-
178-
func (*manager) configureRegistryStorageMounts(
179-
deployment *appsv1.Deployment,
180-
containerName string,
181-
) error {
182-
// Find the container by name
183-
container := findContainerByName(deployment.Spec.Template.Spec.Containers, containerName)
184-
if container == nil {
185-
return fmt.Errorf("container '%s' not found in deployment", containerName)
186-
}
187-
188-
// Add emptyDir volume for storage data if not already present
189-
storageDataVolumeName := "storage-data"
190-
if !hasVolume(deployment.Spec.Template.Spec.Volumes, storageDataVolumeName) {
191-
storageDataVolume := corev1.Volume{
192-
Name: storageDataVolumeName,
193-
VolumeSource: corev1.VolumeSource{
194-
EmptyDir: &corev1.EmptyDirVolumeSource{},
195-
},
196-
}
197-
deployment.Spec.Template.Spec.Volumes = append(deployment.Spec.Template.Spec.Volumes, storageDataVolume)
198-
}
199-
200-
// Add emptyDir mount to the container if not already present
201-
if !hasVolumeMount(container.VolumeMounts, storageDataVolumeName) {
202-
storageDataVolumeMount := corev1.VolumeMount{
203-
Name: storageDataVolumeName,
204-
MountPath: "/data", // You can modify this path as needed
205-
ReadOnly: false,
206-
}
207-
container.VolumeMounts = append(container.VolumeMounts, storageDataVolumeMount)
208-
}
209-
return nil
210-
}
211-
212-
func (*manager) configureRegistrySourceMounts(
213-
deployment *appsv1.Deployment,
214-
mcpRegistry *mcpv1alpha1.MCPRegistry,
215-
containerName string,
216-
) error {
217-
218-
// Find the container by name
219-
container := findContainerByName(deployment.Spec.Template.Spec.Containers, containerName)
220-
if container == nil {
221-
return fmt.Errorf("container '%s' not found in deployment", containerName)
222-
}
223-
224-
// Iterate through all registry configurations to handle multiple ConfigMap sources
225-
for _, registry := range mcpRegistry.Spec.Registries {
226-
if registry.ConfigMapRef != nil {
227-
// Create unique volume name for each ConfigMap source
228-
volumeName := fmt.Sprintf("registry-data-source-%s", registry.Name)
229-
230-
// Add volume if it doesn't exist
231-
if !hasVolume(deployment.Spec.Template.Spec.Volumes, volumeName) {
232-
volume := corev1.Volume{
233-
Name: volumeName,
234-
VolumeSource: corev1.VolumeSource{
235-
ConfigMap: &corev1.ConfigMapVolumeSource{
236-
LocalObjectReference: corev1.LocalObjectReference{
237-
Name: registry.ConfigMapRef.Name,
238-
},
239-
// Mount only the specified key as registry.json
240-
Items: []corev1.KeyToPath{
241-
{
242-
Key: registry.ConfigMapRef.Key,
243-
Path: "registry.json",
244-
},
245-
},
246-
},
247-
},
248-
}
249-
deployment.Spec.Template.Spec.Volumes = append(deployment.Spec.Template.Spec.Volumes, volume)
250-
}
251-
252-
// Add volume mount if it doesn't exist
253-
if !hasVolumeMount(container.VolumeMounts, volumeName) {
254-
// Mount path follows the pattern /config/registry/{registryName}/
255-
// This matches what buildFilePath expects in config.go
256-
mountPath := filepath.Join(config.RegistryJSONFilePath, registry.Name)
257-
volumeMount := corev1.VolumeMount{
258-
Name: volumeName,
259-
MountPath: mountPath,
260-
ReadOnly: true, // ConfigMaps are always read-only
261-
}
262-
container.VolumeMounts = append(container.VolumeMounts, volumeMount)
263-
}
264-
}
265-
}
266-
267-
return nil
268-
}
269-
270128
// getConfigMapName generates the ConfigMap name for registry storage
271129
// This mirrors the logic in ConfigMapStorageManager to maintain consistency
272130
func getConfigMapName(mcpRegistry *mcpv1alpha1.MCPRegistry) string {

0 commit comments

Comments
 (0)