Skip to content

Commit f66592a

Browse files
authored
Fix runtime_config API semantics and validation (#4435)
Reopening closed PR #3772 and carries over the original runtime_config workload API work with the review feedback addressed. The previous version changed runtime config merge behavior for all callers, including CLI, and allowed some invalid runtime_config cases to be ignored or fail later during image build. This update keeps the behavior scoped to the workload API, adds validation, and tightens the response/update round-trip behavior. Fixes #3676
1 parent 1be5e35 commit f66592a

9 files changed

Lines changed: 537 additions & 9 deletions

File tree

docs/server/docs.go

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

docs/server/swagger.json

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

docs/server/swagger.yaml

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

pkg/api/v1/workload_service.go

Lines changed: 127 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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
3658
type 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
402527
func (s *WorkloadService) GetWorkloadNamesFromRequest(ctx context.Context, req bulkOperationRequest) ([]string, error) {
403528
if len(req.Names) > 0 {

0 commit comments

Comments
 (0)