Skip to content

Commit 7501269

Browse files
rdimitrovclaude
andcommitted
Add registry-aware workload creation
Add optional registry and server fields to POST /api/v1beta/workloads so clients can create workloads by referencing a registry server: {"registry": "default", "server": "io.github.stacklok/fetch"} thv resolves the server from the registry, extracts image/transport/ env vars/permissions/etc from metadata, and builds the RunConfig server-side. User-provided fields always override registry defaults. This eliminates client-side field extraction — Studio goes from GET+extract+POST to just POST. Ref: #4199 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent e92dba4 commit 7501269

2 files changed

Lines changed: 89 additions & 0 deletions

File tree

pkg/api/v1/workload_service.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"github.com/stacklok/toolhive/pkg/container/templates"
2323
"github.com/stacklok/toolhive/pkg/groups"
2424
"github.com/stacklok/toolhive/pkg/networking"
25+
"github.com/stacklok/toolhive/pkg/registry"
2526
"github.com/stacklok/toolhive/pkg/runner"
2627
"github.com/stacklok/toolhive/pkg/runner/retriever"
2728
"github.com/stacklok/toolhive/pkg/secrets"
@@ -142,6 +143,16 @@ func (s *WorkloadService) UpdateWorkloadFromRequest(ctx context.Context, name st
142143
func (s *WorkloadService) BuildFullRunConfig(
143144
ctx context.Context, req *createRequest, existingPort int,
144145
) (*runner.RunConfig, error) {
146+
// If registry+server specified, resolve from registry and fill defaults
147+
if req.Registry != "" && req.Server != "" {
148+
if err := resolveRegistryServer(req); err != nil {
149+
return nil, fmt.Errorf("failed to resolve server from registry: %w", err)
150+
}
151+
}
152+
if (req.Registry != "" && req.Server == "") || (req.Registry == "" && req.Server != "") {
153+
return nil, fmt.Errorf("both registry and server must be specified together")
154+
}
155+
145156
// Default proxy mode to streamable-http if not specified (SSE is deprecated)
146157
if !types.IsValidProxyMode(req.ProxyMode) {
147158
if req.ProxyMode == "" {
@@ -557,3 +568,74 @@ func (s *WorkloadService) GetWorkloadNamesFromRequest(ctx context.Context, req b
557568

558569
return workloadNames, nil
559570
}
571+
572+
// resolveRegistryServer resolves a server from the registry and fills in
573+
// default values on the request. User-provided fields are not overwritten.
574+
func resolveRegistryServer(req *createRequest) error {
575+
provider, err := registry.GetDefaultProviderWithConfig(
576+
config.NewProvider(),
577+
registry.WithInteractive(false),
578+
)
579+
if err != nil {
580+
return fmt.Errorf("failed to get registry provider: %w", err)
581+
}
582+
583+
metadata, err := provider.GetServer(req.Server)
584+
if err != nil {
585+
return fmt.Errorf("server %q not found in registry: %w", req.Server, err)
586+
}
587+
588+
applyRegistryDefaults(req, metadata)
589+
return nil
590+
}
591+
592+
func applyRegistryDefaults(req *createRequest, metadata regtypes.ServerMetadata) {
593+
if req.Transport == "" {
594+
req.Transport = metadata.GetTransport()
595+
}
596+
if req.Name == "" {
597+
req.Name = metadata.GetName()
598+
}
599+
600+
switch md := metadata.(type) {
601+
case *regtypes.ImageMetadata:
602+
applyImageDefaults(req, md)
603+
case *regtypes.RemoteServerMetadata:
604+
applyRemoteDefaults(req, md)
605+
}
606+
}
607+
608+
func applyImageDefaults(req *createRequest, md *regtypes.ImageMetadata) {
609+
if req.Image == "" {
610+
req.Image = md.Image
611+
}
612+
if req.TargetPort == 0 && md.TargetPort != 0 {
613+
req.TargetPort = md.TargetPort
614+
}
615+
if len(req.CmdArguments) == 0 && len(md.Args) > 0 {
616+
req.CmdArguments = md.Args
617+
}
618+
if req.PermissionProfile == nil && md.Permissions != nil {
619+
req.PermissionProfile = md.Permissions
620+
}
621+
// Merge env vars: registry defaults first, user overrides take precedence
622+
if req.EnvVars == nil {
623+
req.EnvVars = make(map[string]string)
624+
}
625+
for _, ev := range md.EnvVars {
626+
if ev.Default != "" {
627+
if _, userSet := req.EnvVars[ev.Name]; !userSet {
628+
req.EnvVars[ev.Name] = ev.Default
629+
}
630+
}
631+
}
632+
}
633+
634+
func applyRemoteDefaults(req *createRequest, md *regtypes.RemoteServerMetadata) {
635+
if req.URL == "" {
636+
req.URL = md.URL
637+
}
638+
if len(req.Headers) == 0 && len(md.Headers) > 0 {
639+
req.Headers = md.Headers
640+
}
641+
}

pkg/api/v1/workload_types.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,13 @@ type createRequest struct {
154154
updateRequest
155155
// Name of the workload
156156
Name string `json:"name"`
157+
// Registry is the optional registry name to resolve the server from (e.g. "default").
158+
Registry string `json:"registry,omitempty"`
159+
// Server is the optional server name in the registry (e.g. "io.github.stacklok/fetch").
160+
// When both Registry and Server are set, thv resolves the server metadata
161+
// server-side, filling in image, transport, env vars, permissions, etc.
162+
// User-provided fields always override registry defaults.
163+
Server string `json:"server,omitempty"`
157164
}
158165

159166
// oidcOptions represents OIDC configuration options

0 commit comments

Comments
 (0)