Skip to content

Commit 4024285

Browse files
authored
Introduce builder for RunConfig (#1024)
1 parent 4a55afb commit 4024285

File tree

3 files changed

+641
-235
lines changed

3 files changed

+641
-235
lines changed

pkg/runner/config.go

Lines changed: 22 additions & 235 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import (
2121
"github.com/stacklok/toolhive/pkg/registry"
2222
"github.com/stacklok/toolhive/pkg/secrets"
2323
"github.com/stacklok/toolhive/pkg/telemetry"
24-
"github.com/stacklok/toolhive/pkg/transport"
2524
"github.com/stacklok/toolhive/pkg/transport/types"
2625
)
2726

@@ -139,7 +138,6 @@ func NewRunConfig() *RunConfig {
139138
}
140139

141140
// NewRunConfigFromFlags creates a new RunConfig with values from command-line flags
142-
// TODO: We may want to use some sort of builder pattern here to make it more readable.
143141
func NewRunConfigFromFlags(
144142
ctx context.Context,
145143
runtime rt.Runtime,
@@ -177,132 +175,28 @@ func NewRunConfigFromFlags(
177175
envVarValidator EnvVarValidator,
178176
proxyMode types.ProxyMode,
179177
) (*RunConfig, error) {
180-
// Ensure default values for host and targetHost
181-
if host == "" {
182-
host = transport.LocalhostIPv4
183-
}
184-
if targetHost == "" {
185-
targetHost = transport.LocalhostIPv4
186-
}
187-
188-
config := &RunConfig{
189-
Runtime: runtime,
190-
CmdArgs: cmdArgs,
191-
Name: name,
192-
Image: imageURL,
193-
Debug: debug,
194-
Volumes: volumes,
195-
Secrets: secretsList,
196-
AuthzConfigPath: authzConfigPath,
197-
AuditConfigPath: auditConfigPath,
198-
PermissionProfileNameOrPath: permissionProfile,
199-
TargetHost: targetHost,
200-
ContainerLabels: make(map[string]string),
201-
EnvVars: make(map[string]string),
202-
Host: host,
203-
IsolateNetwork: isolateNetwork,
204-
K8sPodTemplatePatch: k8sPodPatch,
205-
ProxyMode: proxyMode,
206-
}
207-
208-
// Configure audit if enabled
209-
configureAudit(config, enableAudit, auditConfigPath)
210-
211-
// Configure OIDC if any values are provided
212-
configureOIDC(config, oidcIssuer, oidcAudience, oidcJwksURL, oidcClientID, oidcAllowOpaqueTokens)
213-
214-
// Configure telemetry if endpoint or metrics port is provided
215-
configureTelemetry(config, otelEndpoint, otelEnablePrometheusMetricsPath, otelServiceName,
216-
otelSamplingRate, otelHeaders, otelInsecure, otelEnvironmentVariables)
217-
218-
// When using the CLI validation strategy, this is where the prompting for
219-
// missing environment variables will happen.
220-
processedEnvVars, err := envVarValidator.Validate(ctx, imageMetadata, config, envVars)
221-
if err != nil {
222-
return nil, fmt.Errorf("failed to validate environment variables: %v", err)
223-
}
224-
225-
// Do some final validation which can only be done after everything else is set.
226-
// Apply image metadata overrides if needed.
227-
if err := config.validateConfig(imageMetadata, mcpTransport, port, targetPort); err != nil {
228-
return nil, fmt.Errorf("failed to validate run config: %v", err)
229-
}
230-
231-
// Now set environment variables with the correct transport and ports resolved
232-
if _, err := config.WithEnvironmentVariables(processedEnvVars); err != nil {
233-
return nil, fmt.Errorf("failed to set environment variables: %v", err)
234-
}
235-
236-
return config, nil
237-
}
238-
239-
// configureAudit sets up audit configuration if enabled
240-
func configureAudit(config *RunConfig, enableAudit bool, auditConfigPath string) {
241-
if enableAudit && auditConfigPath == "" {
242-
config.AuditConfig = audit.DefaultConfig()
243-
}
244-
}
245-
246-
// configureOIDC sets up OIDC configuration if any values are provided
247-
func configureOIDC(config *RunConfig, oidcIssuer, oidcAudience, oidcJwksURL, oidcClientID string, oidcAllowOpaqueTokens bool) {
248-
if oidcIssuer != "" || oidcAudience != "" || oidcJwksURL != "" || oidcClientID != "" {
249-
config.OIDCConfig = &auth.TokenValidatorConfig{
250-
Issuer: oidcIssuer,
251-
Audience: oidcAudience,
252-
JWKSURL: oidcJwksURL,
253-
ClientID: oidcClientID,
254-
AllowOpaqueTokens: oidcAllowOpaqueTokens,
255-
}
256-
}
257-
}
258-
259-
// configureTelemetry sets up telemetry configuration if endpoint or metrics port is provided
260-
func configureTelemetry(config *RunConfig, otelEndpoint string, otelEnablePrometheusMetricsPath bool,
261-
otelServiceName string, otelSamplingRate float64, otelHeaders []string, otelInsecure bool,
262-
otelEnvironmentVariables []string) {
263-
264-
if otelEndpoint == "" && !otelEnablePrometheusMetricsPath {
265-
return
266-
}
267-
268-
// Parse headers from key=value format
269-
headers := make(map[string]string)
270-
for _, header := range otelHeaders {
271-
parts := strings.SplitN(header, "=", 2)
272-
if len(parts) == 2 {
273-
headers[parts[0]] = parts[1]
274-
}
275-
}
276-
277-
// Use provided service name or default
278-
serviceName := otelServiceName
279-
if serviceName == "" {
280-
serviceName = telemetry.DefaultConfig().ServiceName
281-
}
282-
283-
// Process environment variables - split comma-separated values
284-
var processedEnvVars []string
285-
for _, envVarEntry := range otelEnvironmentVariables {
286-
// Split by comma and trim whitespace
287-
envVars := strings.Split(envVarEntry, ",")
288-
for _, envVar := range envVars {
289-
trimmed := strings.TrimSpace(envVar)
290-
if trimmed != "" {
291-
processedEnvVars = append(processedEnvVars, trimmed)
292-
}
293-
}
294-
}
295-
296-
config.TelemetryConfig = &telemetry.Config{
297-
Endpoint: otelEndpoint,
298-
ServiceName: serviceName,
299-
ServiceVersion: telemetry.DefaultConfig().ServiceVersion,
300-
SamplingRate: otelSamplingRate,
301-
Headers: headers,
302-
Insecure: otelInsecure,
303-
EnablePrometheusMetricsPath: otelEnablePrometheusMetricsPath,
304-
EnvironmentVariables: processedEnvVars,
305-
}
178+
return NewRunConfigBuilder().
179+
WithRuntime(runtime).
180+
WithCmdArgs(cmdArgs).
181+
WithName(name).
182+
WithImage(imageURL).
183+
WithHost(host).
184+
WithTargetHost(targetHost).
185+
WithDebug(debug).
186+
WithVolumes(volumes).
187+
WithSecrets(secretsList).
188+
WithAuthzConfigPath(authzConfigPath).
189+
WithAuditConfigPath(auditConfigPath).
190+
WithPermissionProfileNameOrPath(permissionProfile).
191+
WithNetworkIsolation(isolateNetwork).
192+
WithK8sPodPatch(k8sPodPatch).
193+
WithProxyMode(proxyMode).
194+
WithTransportAndPorts(mcpTransport, port, targetPort).
195+
WithAuditEnabled(enableAudit, auditConfigPath).
196+
WithOIDCConfig(oidcIssuer, oidcAudience, oidcJwksURL, oidcClientID, oidcAllowOpaqueTokens).
197+
WithTelemetryConfig(otelEndpoint, otelEnablePrometheusMetricsPath, otelServiceName,
198+
otelSamplingRate, otelHeaders, otelInsecure, otelEnvironmentVariables).
199+
Build(ctx, imageMetadata, envVars, envVarValidator)
306200
}
307201

308202
// WithAuthz adds authorization configuration to the RunConfig
@@ -525,110 +419,3 @@ func (c *RunConfig) ProcessVolumeMounts() error {
525419

526420
return nil
527421
}
528-
529-
// validateConfig ensures the RunConfig is valid and sets up some of the final
530-
// configuration details which can only be applied after all other flags are added.
531-
// This function also handles setting missing values based on the image metadata (if present).
532-
//
533-
//nolint:gocyclo // This function needs to be refactored to reduce cyclomatic complexity.
534-
func (c *RunConfig) validateConfig(
535-
imageMetadata *registry.ImageMetadata,
536-
mcpTransport string,
537-
port int,
538-
targetPort int,
539-
) error {
540-
var err error
541-
542-
// The old logic claimed to override the name with the name from the registry
543-
// but didn't. Instead, it used the name passed in from the CLI.
544-
// See: https://github.com/stacklok/toolhive/blob/2873152b62bf61698cbcdd0aba1707a046151e67/cmd/thv/app/run.go#L425
545-
// The following code implements what I believe was the intended behavior:
546-
if c.Name == "" && imageMetadata != nil {
547-
c.Name = imageMetadata.Name
548-
}
549-
550-
// Check to see if the mcpTransport is defined in the metadata.
551-
// Use this value if it was not set by the user.
552-
// Else, default to stdio.
553-
if mcpTransport == "" {
554-
if imageMetadata != nil && imageMetadata.Transport != "" {
555-
logger.Debugf("Using registry mcpTransport: %s", imageMetadata.Transport)
556-
mcpTransport = imageMetadata.Transport
557-
} else {
558-
logger.Debugf("Defaulting mcpTransport to stdio")
559-
mcpTransport = types.TransportTypeStdio.String()
560-
}
561-
}
562-
// Set mcpTransport
563-
if _, err = c.WithTransport(mcpTransport); err != nil {
564-
return err
565-
}
566-
567-
// Use registry target port if not overridden and if the mcpTransport is HTTP-based.
568-
if imageMetadata != nil {
569-
isHTTPServer := mcpTransport == types.TransportTypeSSE.String() ||
570-
mcpTransport == types.TransportTypeStreamableHTTP.String()
571-
if targetPort == 0 && isHTTPServer && imageMetadata.TargetPort > 0 {
572-
logger.Debugf("Using registry target port: %d", imageMetadata.TargetPort)
573-
targetPort = imageMetadata.TargetPort
574-
}
575-
}
576-
// Configure ports and target host
577-
if _, err = c.WithPorts(port, targetPort); err != nil {
578-
return err
579-
}
580-
581-
// If we are missing the permission profile, attempt to load one from the image metadata.
582-
if c.PermissionProfileNameOrPath == "" && imageMetadata != nil {
583-
permProfilePath, err := CreatePermissionProfileFile(c.Name, imageMetadata.Permissions)
584-
if err != nil {
585-
// Just log the error and continue with the default permission profile
586-
logger.Warnf("Warning: failed to create permission profile file: %v", err)
587-
} else {
588-
// Update the permission profile path
589-
c.PermissionProfileNameOrPath = permProfilePath
590-
}
591-
}
592-
// Set permission profile (mandatory)
593-
if _, err = c.ParsePermissionProfile(); err != nil {
594-
return err
595-
}
596-
597-
// Process volume mounts
598-
if err = c.ProcessVolumeMounts(); err != nil {
599-
return err
600-
}
601-
602-
// Generate container name if not already set
603-
c.WithContainerName()
604-
605-
// Add standard labels
606-
c.WithStandardLabels()
607-
608-
// Add authorization configuration if provided
609-
if c.AuthzConfigPath != "" {
610-
authzConfig, err := authz.LoadConfig(c.AuthzConfigPath)
611-
if err != nil {
612-
return fmt.Errorf("failed to load authorization configuration: %v", err)
613-
}
614-
c.WithAuthz(authzConfig)
615-
}
616-
617-
// Add audit configuration if provided
618-
if c.AuditConfigPath != "" {
619-
auditConfig, err := audit.LoadFromFile(c.AuditConfigPath)
620-
if err != nil {
621-
return fmt.Errorf("failed to load audit configuration: %v", err)
622-
}
623-
c.WithAudit(auditConfig)
624-
}
625-
// Note: AuditConfig is already set from --enable-audit flag if provided
626-
627-
// Prepend registry args to command-line args if available
628-
if imageMetadata != nil && len(imageMetadata.Args) > 0 {
629-
logger.Debugf("Prepending registry args: %v", imageMetadata.Args)
630-
c.CmdArgs = append(c.CmdArgs, imageMetadata.Args...)
631-
}
632-
633-
return nil
634-
}

0 commit comments

Comments
 (0)