@@ -8,14 +8,18 @@ import (
88 "errors"
99 "fmt"
1010 "log/slog"
11+ "strings"
1112 "time"
1213
14+ nameref "github.com/google/go-containerregistry/pkg/name"
15+
1316 regtypes "github.com/stacklok/toolhive-core/registry/types"
1417 groupval "github.com/stacklok/toolhive-core/validation/group"
1518 httpval "github.com/stacklok/toolhive-core/validation/http"
1619 "github.com/stacklok/toolhive/pkg/auth/remote"
1720 "github.com/stacklok/toolhive/pkg/config"
1821 "github.com/stacklok/toolhive/pkg/container/runtime"
22+ "github.com/stacklok/toolhive/pkg/container/templates"
1923 "github.com/stacklok/toolhive/pkg/groups"
2024 "github.com/stacklok/toolhive/pkg/networking"
2125 "github.com/stacklok/toolhive/pkg/runner"
@@ -32,6 +36,24 @@ const (
3236 imageRetrievalTimeout = 10 * time .Minute
3337)
3438
39+ func isValidRuntimePackageName (pkg string ) bool {
40+ if pkg == "" {
41+ return false
42+ }
43+ for i , r := range pkg {
44+ switch {
45+ case r >= 'a' && r <= 'z' :
46+ case r >= 'A' && r <= 'Z' :
47+ case r >= '0' && r <= '9' :
48+ case r == '.' , r == '_' :
49+ case (r == '+' || r == '-' ) && i > 0 :
50+ default :
51+ return false
52+ }
53+ }
54+ return true
55+ }
56+
3557// WorkloadService handles business logic for workload operations
3658type WorkloadService struct {
3759 workloadManager workloads.Manager
@@ -165,6 +187,11 @@ func (s *WorkloadService) BuildFullRunConfig(
165187 var imageMetadata * regtypes.ImageMetadata
166188 var serverMetadata regtypes.ServerMetadata
167189 var registryProxyPort int
190+ runtimeConfigOverride := runtimeConfigFromRequest (req )
191+ retrievalRuntimeConfig , err := runtimeConfigForImageBuild (req , runtimeConfigOverride )
192+ if err != nil {
193+ return nil , fmt .Errorf ("%w: %w" , retriever .ErrInvalidRunConfig , err )
194+ }
168195
169196 if req .URL != "" {
170197 // Configure remote authentication if OAuth config is provided
@@ -184,8 +211,8 @@ func (s *WorkloadService) BuildFullRunConfig(
184211 req .Image ,
185212 "" , // We do not let the user specify a CA cert path here.
186213 retriever .VerifyImageWarn ,
187- "" , // TODO Add support for registry groups lookups for API
188- nil , // No runtime override from API (yet)
214+ "" , // TODO Add support for registry groups lookups for API
215+ retrievalRuntimeConfig ,
189216 )
190217 if err != nil {
191218 // Check if the error is due to context timeout
@@ -290,6 +317,11 @@ func (s *WorkloadService) BuildFullRunConfig(
290317 runner .WithRegistryServerName (regServerName ),
291318 }
292319
320+ // Runtime overrides only apply to protocol-scheme image builds.
321+ if runtimeConfigOverride != nil && req .URL == "" {
322+ options = append (options , runner .WithRuntimeConfig (runtimeConfigOverride ))
323+ }
324+
293325 // Add header forward configuration if specified
294326 if req .HeaderForward != nil {
295327 if len (req .HeaderForward .AddPlaintextHeaders ) > 0 {
@@ -398,6 +430,99 @@ func createRequestToRemoteAuthConfig(
398430 return remoteAuthConfig
399431}
400432
433+ func runtimeConfigFromRequest (req * createRequest ) * templates.RuntimeConfig {
434+ if req == nil || req .RuntimeConfig == nil {
435+ return nil
436+ }
437+
438+ runtimeConfig := & templates.RuntimeConfig {}
439+ if builderImage := strings .TrimSpace (req .RuntimeConfig .BuilderImage ); builderImage != "" {
440+ runtimeConfig .BuilderImage = builderImage
441+ }
442+ if len (req .RuntimeConfig .AdditionalPackages ) > 0 {
443+ for _ , pkg := range req .RuntimeConfig .AdditionalPackages {
444+ if trimmedPkg := strings .TrimSpace (pkg ); trimmedPkg != "" {
445+ runtimeConfig .AdditionalPackages = append (runtimeConfig .AdditionalPackages , trimmedPkg )
446+ }
447+ }
448+ }
449+ if runtimeConfig .BuilderImage == "" && len (runtimeConfig .AdditionalPackages ) == 0 {
450+ return nil
451+ }
452+
453+ return runtimeConfig
454+ }
455+
456+ func validateRuntimeConfig (runtimeConfig * templates.RuntimeConfig ) error {
457+ if runtimeConfig == nil {
458+ return nil
459+ }
460+
461+ if runtimeConfig .BuilderImage != "" {
462+ if _ , err := nameref .ParseReference (runtimeConfig .BuilderImage ); err != nil {
463+ return fmt .Errorf ("runtime_config.builder_image must be a valid container image reference" )
464+ }
465+ }
466+
467+ for _ , pkg := range runtimeConfig .AdditionalPackages {
468+ if ! isValidRuntimePackageName (pkg ) {
469+ return fmt .Errorf ("runtime_config.additional_packages contains invalid package name %q" , pkg )
470+ }
471+ }
472+
473+ return nil
474+ }
475+
476+ func runtimeConfigForImageBuild (
477+ req * createRequest ,
478+ runtimeConfigOverride * templates.RuntimeConfig ,
479+ ) (* templates.RuntimeConfig , error ) {
480+ if runtimeConfigOverride == nil || req == nil {
481+ return nil , nil
482+ }
483+ if err := validateRuntimeConfig (runtimeConfigOverride ); err != nil {
484+ return nil , err
485+ }
486+ if req .URL != "" || ! runner .IsImageProtocolScheme (req .Image ) {
487+ return nil , fmt .Errorf ("runtime_config is only supported for protocol-scheme images" )
488+ }
489+
490+ transportType , _ , err := runner .ParseProtocolScheme (req .Image )
491+ if err != nil {
492+ return nil , err
493+ }
494+
495+ baseConfig := getBaseRuntimeConfig (transportType )
496+ merged := & templates.RuntimeConfig {
497+ BuilderImage : baseConfig .BuilderImage ,
498+ AdditionalPackages : append ([]string {}, baseConfig .AdditionalPackages ... ),
499+ }
500+ if runtimeConfigOverride .BuilderImage != "" {
501+ merged .BuilderImage = runtimeConfigOverride .BuilderImage
502+ }
503+ if len (runtimeConfigOverride .AdditionalPackages ) > 0 {
504+ merged .AdditionalPackages = append (merged .AdditionalPackages , runtimeConfigOverride .AdditionalPackages ... )
505+ }
506+
507+ return merged , nil
508+ }
509+
510+ func getBaseRuntimeConfig (transportType templates.TransportType ) * templates.RuntimeConfig {
511+ provider := config .NewProvider ()
512+ if userConfig , err := provider .GetRuntimeConfig (string (transportType )); err == nil && userConfig != nil {
513+ return & templates.RuntimeConfig {
514+ BuilderImage : userConfig .BuilderImage ,
515+ AdditionalPackages : append ([]string {}, userConfig .AdditionalPackages ... ),
516+ }
517+ }
518+
519+ defaultConfig := templates .GetDefaultRuntimeConfig (transportType )
520+ return & templates.RuntimeConfig {
521+ BuilderImage : defaultConfig .BuilderImage ,
522+ AdditionalPackages : append ([]string {}, defaultConfig .AdditionalPackages ... ),
523+ }
524+ }
525+
401526// GetWorkloadNamesFromRequest gets workload names from either the names field or group
402527func (s * WorkloadService ) GetWorkloadNamesFromRequest (ctx context.Context , req bulkOperationRequest ) ([]string , error ) {
403528 if len (req .Names ) > 0 {
0 commit comments