Refactor instance management and configuration handling

This commit is contained in:
2025-07-19 21:10:27 +02:00
parent 3428089bec
commit 301e170974
7 changed files with 102 additions and 78 deletions

View File

@@ -21,7 +21,7 @@ func main() {
} }
// Initialize the instance manager // Initialize the instance manager
instanceManager := llamactl.NewInstanceManager() instanceManager := llamactl.NewInstanceManager(config.Instances)
// Create a new handler with the instance manager // Create a new handler with the instance manager
handler := llamactl.NewHandler(instanceManager) handler := llamactl.NewHandler(instanceManager)
@@ -30,6 +30,6 @@ func main() {
r := llamactl.SetupRouter(handler) r := llamactl.SetupRouter(handler)
// Start the server with the router // Start the server with the router
fmt.Println("Starting llamactl on port 8080...") fmt.Printf("Starting llamactl on port %d...\n", config.Server.Port)
http.ListenAndServe(":8080", r) http.ListenAndServe(fmt.Sprintf("%s:%d", config.Server.Host, config.Server.Port), r)
} }

View File

@@ -28,8 +28,8 @@ type ServerConfig struct {
// InstancesConfig contains instance management configuration // InstancesConfig contains instance management configuration
type InstancesConfig struct { type InstancesConfig struct {
// Port range for instances (e.g., "8000-9000") // Port range for instances (e.g., 8000,9000)
PortRange string `yaml:"port_range"` PortRange [2]int `yaml:"port_range"`
// Directory where instance logs will be stored // Directory where instance logs will be stored
LogDirectory string `yaml:"log_directory"` LogDirectory string `yaml:"log_directory"`
@@ -62,7 +62,7 @@ func LoadConfig(configPath string) (Config, error) {
Port: 8080, Port: 8080,
}, },
Instances: InstancesConfig{ Instances: InstancesConfig{
PortRange: "8000-9000", PortRange: [2]int{8000, 9000},
LogDirectory: "/tmp/llamactl", LogDirectory: "/tmp/llamactl",
MaxInstances: 10, MaxInstances: 10,
LlamaExecutable: "llama-server", LlamaExecutable: "llama-server",
@@ -121,7 +121,9 @@ func loadEnvVars(cfg *Config) {
// Instance config // Instance config
if portRange := os.Getenv("LLAMACTL_INSTANCE_PORT_RANGE"); portRange != "" { if portRange := os.Getenv("LLAMACTL_INSTANCE_PORT_RANGE"); portRange != "" {
cfg.Instances.PortRange = portRange if ports := parsePortRange(portRange); ports != [2]int{0, 0} {
cfg.Instances.PortRange = ports
}
} }
if logDir := os.Getenv("LLAMACTL_LOG_DIR"); logDir != "" { if logDir := os.Getenv("LLAMACTL_LOG_DIR"); logDir != "" {
cfg.Instances.LogDirectory = logDir cfg.Instances.LogDirectory = logDir
@@ -168,6 +170,29 @@ func parseDelaySeconds(s string) (time.Duration, error) {
return time.Duration(seconds * float64(time.Second)), nil return time.Duration(seconds * float64(time.Second)), nil
} }
// parsePortRange parses port range from string formats like "8000-9000" or "8000,9000"
func parsePortRange(s string) [2]int {
var parts []string
// Try both separators
if strings.Contains(s, "-") {
parts = strings.Split(s, "-")
} else if strings.Contains(s, ",") {
parts = strings.Split(s, ",")
}
// Parse the two parts
if len(parts) == 2 {
start, err1 := strconv.Atoi(strings.TrimSpace(parts[0]))
end, err2 := strconv.Atoi(strings.TrimSpace(parts[1]))
if err1 == nil && err2 == nil {
return [2]int{start, end}
}
}
return [2]int{0, 0} // Invalid format
}
// getDefaultConfigLocations returns platform-specific config file locations // getDefaultConfigLocations returns platform-specific config file locations
func getDefaultConfigLocations() []string { func getDefaultConfigLocations() []string {
var locations []string var locations []string

View File

@@ -127,7 +127,7 @@ func (h *Handler) CreateInstance() http.HandlerFunc {
return return
} }
var options InstanceOptions var options CreateInstanceRequest
if err := json.NewDecoder(r.Body).Decode(&options); err != nil { if err := json.NewDecoder(r.Body).Decode(&options); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest) http.Error(w, "Invalid request body", http.StatusBadRequest)
return return
@@ -199,7 +199,7 @@ func (h *Handler) UpdateInstance() http.HandlerFunc {
return return
} }
var options InstanceOptions var options CreateInstanceRequest
if err := json.NewDecoder(r.Body).Decode(&options); err != nil { if err := json.NewDecoder(r.Body).Decode(&options); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest) http.Error(w, "Invalid request body", http.StatusBadRequest)
return return

View File

@@ -18,9 +18,43 @@ import (
"time" "time"
) )
// Duration is a custom type that wraps time.Duration for better JSON/Swagger support
// @description Duration in seconds
type Duration time.Duration
// MarshalJSON implements json.Marshaler for Duration
func (d Duration) MarshalJSON() ([]byte, error) {
return json.Marshal(time.Duration(d).Seconds())
}
// UnmarshalJSON implements json.Unmarshaler for Duration
func (d *Duration) UnmarshalJSON(data []byte) error {
var seconds float64
if err := json.Unmarshal(data, &seconds); err != nil {
return err
}
*d = Duration(time.Duration(seconds * float64(time.Second)))
return nil
}
// ToDuration converts Duration to time.Duration
func (d Duration) ToDuration() time.Duration {
return time.Duration(d)
}
type CreateInstanceRequest struct {
// Auto restart
AutoRestart bool `json:"auto_restart,omitempty"`
MaxRestarts int `json:"max_restarts,omitempty"`
RestartDelay Duration `json:"restart_delay,omitempty"` // Duration in seconds
LlamaServerOptions `json:",inline"`
}
// Instance represents a running instance of the llama server
type Instance struct { type Instance struct {
Name string `json:"name"` Name string `json:"name"`
options *InstanceOptions `json:"-"` // Now unexported - access via GetOptions/SetOptions options *CreateInstanceRequest `json:"-"` // Now unexported - access via GetOptions/SetOptions
// Status // Status
Running bool `json:"running"` Running bool `json:"running"`
@@ -40,12 +74,15 @@ type Instance struct {
proxy *httputil.ReverseProxy `json:"-"` // Reverse proxy for this instance proxy *httputil.ReverseProxy `json:"-"` // Reverse proxy for this instance
} }
func NewInstance(name string, options *InstanceOptions) *Instance { // NewInstance creates a new instance with the given name, log path, and options
func NewInstance(name string, logPath string, options *CreateInstanceRequest) *Instance {
return &Instance{ return &Instance{
Name: name, Name: name,
options: options, options: options,
Running: false, Running: false,
logPath: logPath,
} }
} }
@@ -80,13 +117,13 @@ func (i *Instance) closeLogFiles() {
} }
} }
func (i *Instance) GetOptions() *InstanceOptions { func (i *Instance) GetOptions() *CreateInstanceRequest {
i.mu.Lock() i.mu.Lock()
defer i.mu.Unlock() defer i.mu.Unlock()
return i.options return i.options
} }
func (i *Instance) SetOptions(options *InstanceOptions) { func (i *Instance) SetOptions(options *CreateInstanceRequest) {
i.mu.Lock() i.mu.Lock()
defer i.mu.Unlock() defer i.mu.Unlock()
if options == nil { if options == nil {
@@ -317,9 +354,9 @@ func (i *Instance) MarshalJSON() ([]byte, error) {
// Create a temporary struct with exported fields for JSON marshalling // Create a temporary struct with exported fields for JSON marshalling
temp := struct { temp := struct {
Name string `json:"name"` Name string `json:"name"`
Options *InstanceOptions `json:"options,omitempty"` Options *CreateInstanceRequest `json:"options,omitempty"`
Running bool `json:"running"` Running bool `json:"running"`
}{ }{
Name: i.Name, Name: i.Name,
Options: i.options, Options: i.options,
@@ -333,9 +370,9 @@ func (i *Instance) MarshalJSON() ([]byte, error) {
func (i *Instance) UnmarshalJSON(data []byte) error { func (i *Instance) UnmarshalJSON(data []byte) error {
// Create a temporary struct for unmarshalling // Create a temporary struct for unmarshalling
temp := struct { temp := struct {
Name string `json:"name"` Name string `json:"name"`
Options *InstanceOptions `json:"options,omitempty"` Options *CreateInstanceRequest `json:"options,omitempty"`
Running bool `json:"running"` Running bool `json:"running"`
}{} }{}
if err := json.Unmarshal(data, &temp); err != nil { if err := json.Unmarshal(data, &temp); err != nil {

View File

@@ -5,42 +5,8 @@ import (
"reflect" "reflect"
"strconv" "strconv"
"strings" "strings"
"time"
) )
// Duration is a custom type that wraps time.Duration for better JSON/Swagger support
// @description Duration in seconds
type Duration time.Duration
// MarshalJSON implements json.Marshaler for Duration
func (d Duration) MarshalJSON() ([]byte, error) {
return json.Marshal(time.Duration(d).Seconds())
}
// UnmarshalJSON implements json.Unmarshaler for Duration
func (d *Duration) UnmarshalJSON(data []byte) error {
var seconds float64
if err := json.Unmarshal(data, &seconds); err != nil {
return err
}
*d = Duration(time.Duration(seconds * float64(time.Second)))
return nil
}
// ToDuration converts Duration to time.Duration
func (d Duration) ToDuration() time.Duration {
return time.Duration(d)
}
type InstanceOptions struct {
// Auto restart
AutoRestart bool `json:"auto_restart,omitempty"`
MaxRestarts int `json:"max_restarts,omitempty"`
RestartDelay Duration `json:"restart_delay,omitempty" example:"5"` // Duration in seconds
LlamaServerOptions `json:",inline"`
}
type LlamaServerOptions struct { type LlamaServerOptions struct {
// Common params // Common params
VerbosePrompt bool `json:"verbose_prompt,omitempty"` VerbosePrompt bool `json:"verbose_prompt,omitempty"`
@@ -396,8 +362,3 @@ func (o *LlamaServerOptions) BuildCommandArgs() []string {
return args return args
} }
// BuildCommandArgs converts InstanceOptions to command line arguments by delegating to LlamaServerOptions
func (o *InstanceOptions) BuildCommandArgs() []string {
return o.LlamaServerOptions.BuildCommandArgs()
}

View File

@@ -7,9 +7,9 @@ import (
// InstanceManager defines the interface for managing instances of the llama server. // InstanceManager defines the interface for managing instances of the llama server.
type InstanceManager interface { type InstanceManager interface {
ListInstances() ([]*Instance, error) ListInstances() ([]*Instance, error)
CreateInstance(name string, options *InstanceOptions) (*Instance, error) CreateInstance(name string, options *CreateInstanceRequest) (*Instance, error)
GetInstance(name string) (*Instance, error) GetInstance(name string) (*Instance, error)
UpdateInstance(name string, options *InstanceOptions) (*Instance, error) UpdateInstance(name string, options *CreateInstanceRequest) (*Instance, error)
DeleteInstance(name string) error DeleteInstance(name string) error
StartInstance(name string) (*Instance, error) StartInstance(name string) (*Instance, error)
StopInstance(name string) (*Instance, error) StopInstance(name string) (*Instance, error)
@@ -18,17 +18,17 @@ type InstanceManager interface {
} }
type instanceManager struct { type instanceManager struct {
instances map[string]*Instance instances map[string]*Instance
portRange [][2]int // Range of ports to use for instances ports map[int]bool
ports map[int]bool instancesConfig InstancesConfig
} }
// NewInstanceManager creates a new instance of InstanceManager. // NewInstanceManager creates a new instance of InstanceManager.
func NewInstanceManager() InstanceManager { func NewInstanceManager(instancesConfig InstancesConfig) InstanceManager {
return &instanceManager{ return &instanceManager{
instances: make(map[string]*Instance), instances: make(map[string]*Instance),
portRange: [][2]int{{8000, 9000}}, ports: make(map[int]bool),
ports: make(map[int]bool), instancesConfig: instancesConfig,
} }
} }
@@ -43,7 +43,7 @@ func (im *instanceManager) ListInstances() ([]*Instance, error) {
// CreateInstance creates a new instance with the given options and returns it. // CreateInstance creates a new instance with the given options and returns it.
// The instance is initially in a "stopped" state. // The instance is initially in a "stopped" state.
func (im *instanceManager) CreateInstance(name string, options *InstanceOptions) (*Instance, error) { func (im *instanceManager) CreateInstance(name string, options *CreateInstanceRequest) (*Instance, error) {
if options == nil { if options == nil {
return nil, fmt.Errorf("instance options cannot be nil") return nil, fmt.Errorf("instance options cannot be nil")
} }
@@ -72,7 +72,7 @@ func (im *instanceManager) CreateInstance(name string, options *InstanceOptions)
options.Port = port options.Port = port
} }
instance := NewInstance(name, options) instance := NewInstance(name, im.instancesConfig.LogDirectory, options)
im.instances[instance.Name] = instance im.instances[instance.Name] = instance
return instance, nil return instance, nil
@@ -88,7 +88,7 @@ func (im *instanceManager) GetInstance(name string) (*Instance, error) {
} }
// UpdateInstance updates the options of an existing instance and returns it. // UpdateInstance updates the options of an existing instance and returns it.
func (im *instanceManager) UpdateInstance(name string, options *InstanceOptions) (*Instance, error) { func (im *instanceManager) UpdateInstance(name string, options *CreateInstanceRequest) (*Instance, error) {
instance, exists := im.instances[name] instance, exists := im.instances[name]
if !exists { if !exists {
return nil, fmt.Errorf("instance with name %s not found", name) return nil, fmt.Errorf("instance with name %s not found", name)
@@ -178,13 +178,14 @@ func (im *instanceManager) GetInstanceLogs(name string) (string, error) {
} }
func (im *instanceManager) getNextAvailablePort() (int, error) { func (im *instanceManager) getNextAvailablePort() (int, error) {
for _, portRange := range im.portRange { portRange := im.instancesConfig.PortRange
for port := portRange[0]; port <= portRange[1]; port++ {
if !im.ports[port] { for port := portRange[0]; port <= portRange[1]; port++ {
im.ports[port] = true if !im.ports[port] {
return port, nil im.ports[port] = true
} return port, nil
} }
} }
return 0, fmt.Errorf("no available ports in the specified range") return 0, fmt.Errorf("no available ports in the specified range")
} }

View File

@@ -32,7 +32,7 @@ func validateStringForInjection(value string) error {
} }
// ValidateInstanceOptions performs minimal security validation // ValidateInstanceOptions performs minimal security validation
func ValidateInstanceOptions(options *InstanceOptions) error { func ValidateInstanceOptions(options *CreateInstanceRequest) error {
if options == nil { if options == nil {
return ValidationError(fmt.Errorf("options cannot be nil")) return ValidationError(fmt.Errorf("options cannot be nil"))
} }