diff --git a/server/docs/docs.go b/server/docs/docs.go index c285a02..9f83d5e 100644 --- a/server/docs/docs.go +++ b/server/docs/docs.go @@ -19,6 +19,327 @@ const docTemplate = `{ "host": "{{.Host}}", "basePath": "{{.BasePath}}", "paths": { + "/instances": { + "get": { + "description": "Returns a list of all instances managed by the server", + "produces": [ + "application/json" + ], + "tags": [ + "instances" + ], + "summary": "List all instances", + "responses": { + "200": { + "description": "List of instances", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/llamactl.Instance" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + }, + "post": { + "description": "Creates a new instance with the provided configuration options", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "instances" + ], + "summary": "Create and start a new instance", + "parameters": [ + { + "description": "Instance configuration options", + "name": "options", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/llamactl.InstanceOptions" + } + } + ], + "responses": { + "201": { + "description": "Created instance details", + "schema": { + "$ref": "#/definitions/llamactl.Instance" + } + }, + "400": { + "description": "Invalid request body", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + } + }, + "/instances/{name}": { + "get": { + "description": "Returns the details of a specific instance by name", + "tags": [ + "instances" + ], + "summary": "Get details of a specific instance", + "parameters": [ + { + "type": "string", + "description": "Instance Name", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Instance details", + "schema": { + "$ref": "#/definitions/llamactl.Instance" + } + }, + "400": { + "description": "Invalid name format", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + }, + "put": { + "description": "Updates the configuration of a specific instance by name", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "instances" + ], + "summary": "Update an instance's configuration", + "parameters": [ + { + "type": "string", + "description": "Instance Name", + "name": "name", + "in": "path", + "required": true + }, + { + "description": "Instance configuration options", + "name": "options", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/llamactl.InstanceOptions" + } + } + ], + "responses": { + "200": { + "description": "Updated instance details", + "schema": { + "$ref": "#/definitions/llamactl.Instance" + } + }, + "400": { + "description": "Invalid name format", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + }, + "delete": { + "description": "Stops and removes a specific instance by name", + "produces": [ + "application/json" + ], + "tags": [ + "instances" + ], + "summary": "Delete an instance", + "parameters": [ + { + "type": "string", + "description": "Instance Name", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Invalid name format", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + } + }, + "/instances/{name}/restart": { + "post": { + "description": "Restarts a specific instance by name", + "produces": [ + "application/json" + ], + "tags": [ + "instances" + ], + "summary": "Restart a running instance", + "parameters": [ + { + "type": "string", + "description": "Instance Name", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Restarted instance details", + "schema": { + "$ref": "#/definitions/llamactl.Instance" + } + }, + "400": { + "description": "Invalid name format", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + } + }, + "/instances/{name}/start": { + "post": { + "description": "Starts a specific instance by name", + "produces": [ + "application/json" + ], + "tags": [ + "instances" + ], + "summary": "Start a stopped instance", + "parameters": [ + { + "type": "string", + "description": "Instance Name", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Started instance details", + "schema": { + "$ref": "#/definitions/llamactl.Instance" + } + }, + "400": { + "description": "Invalid name format", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + } + }, + "/instances/{name}/stop": { + "post": { + "description": "Stops a specific instance by name", + "produces": [ + "application/json" + ], + "tags": [ + "instances" + ], + "summary": "Stop a running instance", + "parameters": [ + { + "type": "string", + "description": "Instance Name", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Stopped instance details", + "schema": { + "$ref": "#/definitions/llamactl.Instance" + } + }, + "400": { + "description": "Invalid name format", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + } + }, "/server/devices": { "get": { "description": "Returns a list of available devices for the llama server", @@ -88,6 +409,31 @@ const docTemplate = `{ } } } + }, + "definitions": { + "llamactl.Instance": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "running": { + "description": "Status", + "type": "boolean" + }, + "stdErrChan": { + "description": "Channel for sending error messages", + "type": "object" + }, + "stdOutChan": { + "description": "Output channels", + "type": "object" + } + } + }, + "llamactl.InstanceOptions": { + "type": "object" + } } }` @@ -97,8 +443,8 @@ var SwaggerInfo = &swag.Spec{ Host: "", BasePath: "/api/v1", Schemes: []string{}, - Title: "Llama Server Control", - Description: "This is a control server for managing Llama Server instances.", + Title: "llamactl API", + Description: "llamactl is a control server for managing Llama Server instances.", InfoInstanceName: "swagger", SwaggerTemplate: docTemplate, LeftDelim: "{{", diff --git a/server/docs/swagger.json b/server/docs/swagger.json index 7554aa0..62116f2 100644 --- a/server/docs/swagger.json +++ b/server/docs/swagger.json @@ -1,8 +1,8 @@ { "swagger": "2.0", "info": { - "description": "This is a control server for managing Llama Server instances.", - "title": "Llama Server Control", + "description": "llamactl is a control server for managing Llama Server instances.", + "title": "llamactl API", "contact": {}, "license": { "name": "MIT License", @@ -12,6 +12,327 @@ }, "basePath": "/api/v1", "paths": { + "/instances": { + "get": { + "description": "Returns a list of all instances managed by the server", + "produces": [ + "application/json" + ], + "tags": [ + "instances" + ], + "summary": "List all instances", + "responses": { + "200": { + "description": "List of instances", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/llamactl.Instance" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + }, + "post": { + "description": "Creates a new instance with the provided configuration options", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "instances" + ], + "summary": "Create and start a new instance", + "parameters": [ + { + "description": "Instance configuration options", + "name": "options", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/llamactl.InstanceOptions" + } + } + ], + "responses": { + "201": { + "description": "Created instance details", + "schema": { + "$ref": "#/definitions/llamactl.Instance" + } + }, + "400": { + "description": "Invalid request body", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + } + }, + "/instances/{name}": { + "get": { + "description": "Returns the details of a specific instance by name", + "tags": [ + "instances" + ], + "summary": "Get details of a specific instance", + "parameters": [ + { + "type": "string", + "description": "Instance Name", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Instance details", + "schema": { + "$ref": "#/definitions/llamactl.Instance" + } + }, + "400": { + "description": "Invalid name format", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + }, + "put": { + "description": "Updates the configuration of a specific instance by name", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "instances" + ], + "summary": "Update an instance's configuration", + "parameters": [ + { + "type": "string", + "description": "Instance Name", + "name": "name", + "in": "path", + "required": true + }, + { + "description": "Instance configuration options", + "name": "options", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/llamactl.InstanceOptions" + } + } + ], + "responses": { + "200": { + "description": "Updated instance details", + "schema": { + "$ref": "#/definitions/llamactl.Instance" + } + }, + "400": { + "description": "Invalid name format", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + }, + "delete": { + "description": "Stops and removes a specific instance by name", + "produces": [ + "application/json" + ], + "tags": [ + "instances" + ], + "summary": "Delete an instance", + "parameters": [ + { + "type": "string", + "description": "Instance Name", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Invalid name format", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + } + }, + "/instances/{name}/restart": { + "post": { + "description": "Restarts a specific instance by name", + "produces": [ + "application/json" + ], + "tags": [ + "instances" + ], + "summary": "Restart a running instance", + "parameters": [ + { + "type": "string", + "description": "Instance Name", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Restarted instance details", + "schema": { + "$ref": "#/definitions/llamactl.Instance" + } + }, + "400": { + "description": "Invalid name format", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + } + }, + "/instances/{name}/start": { + "post": { + "description": "Starts a specific instance by name", + "produces": [ + "application/json" + ], + "tags": [ + "instances" + ], + "summary": "Start a stopped instance", + "parameters": [ + { + "type": "string", + "description": "Instance Name", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Started instance details", + "schema": { + "$ref": "#/definitions/llamactl.Instance" + } + }, + "400": { + "description": "Invalid name format", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + } + }, + "/instances/{name}/stop": { + "post": { + "description": "Stops a specific instance by name", + "produces": [ + "application/json" + ], + "tags": [ + "instances" + ], + "summary": "Stop a running instance", + "parameters": [ + { + "type": "string", + "description": "Instance Name", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Stopped instance details", + "schema": { + "$ref": "#/definitions/llamactl.Instance" + } + }, + "400": { + "description": "Invalid name format", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + } + }, "/server/devices": { "get": { "description": "Returns a list of available devices for the llama server", @@ -81,5 +402,30 @@ } } } + }, + "definitions": { + "llamactl.Instance": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "running": { + "description": "Status", + "type": "boolean" + }, + "stdErrChan": { + "description": "Channel for sending error messages", + "type": "object" + }, + "stdOutChan": { + "description": "Output channels", + "type": "object" + } + } + }, + "llamactl.InstanceOptions": { + "type": "object" + } } } \ No newline at end of file diff --git a/server/docs/swagger.yaml b/server/docs/swagger.yaml index a3127cc..20ce023 100644 --- a/server/docs/swagger.yaml +++ b/server/docs/swagger.yaml @@ -1,13 +1,242 @@ basePath: /api/v1 +definitions: + llamactl.Instance: + properties: + name: + type: string + running: + description: Status + type: boolean + stdErrChan: + description: Channel for sending error messages + type: object + stdOutChan: + description: Output channels + type: object + type: object + llamactl.InstanceOptions: + type: object info: contact: {} - description: This is a control server for managing Llama Server instances. + description: llamactl is a control server for managing Llama Server instances. license: name: MIT License url: https://opensource.org/license/mit/ - title: Llama Server Control + title: llamactl API version: "1.0" paths: + /instances: + get: + description: Returns a list of all instances managed by the server + produces: + - application/json + responses: + "200": + description: List of instances + schema: + items: + $ref: '#/definitions/llamactl.Instance' + type: array + "500": + description: Internal Server Error + schema: + type: string + summary: List all instances + tags: + - instances + post: + consumes: + - application/json + description: Creates a new instance with the provided configuration options + parameters: + - description: Instance configuration options + in: body + name: options + required: true + schema: + $ref: '#/definitions/llamactl.InstanceOptions' + produces: + - application/json + responses: + "201": + description: Created instance details + schema: + $ref: '#/definitions/llamactl.Instance' + "400": + description: Invalid request body + schema: + type: string + "500": + description: Internal Server Error + schema: + type: string + summary: Create and start a new instance + tags: + - instances + /instances/{name}: + delete: + description: Stops and removes a specific instance by name + parameters: + - description: Instance Name + in: path + name: name + required: true + type: string + produces: + - application/json + responses: + "204": + description: No Content + "400": + description: Invalid name format + schema: + type: string + "500": + description: Internal Server Error + schema: + type: string + summary: Delete an instance + tags: + - instances + get: + description: Returns the details of a specific instance by name + parameters: + - description: Instance Name + in: path + name: name + required: true + type: string + responses: + "200": + description: Instance details + schema: + $ref: '#/definitions/llamactl.Instance' + "400": + description: Invalid name format + schema: + type: string + "500": + description: Internal Server Error + schema: + type: string + summary: Get details of a specific instance + tags: + - instances + put: + consumes: + - application/json + description: Updates the configuration of a specific instance by name + parameters: + - description: Instance Name + in: path + name: name + required: true + type: string + - description: Instance configuration options + in: body + name: options + required: true + schema: + $ref: '#/definitions/llamactl.InstanceOptions' + produces: + - application/json + responses: + "200": + description: Updated instance details + schema: + $ref: '#/definitions/llamactl.Instance' + "400": + description: Invalid name format + schema: + type: string + "500": + description: Internal Server Error + schema: + type: string + summary: Update an instance's configuration + tags: + - instances + /instances/{name}/restart: + post: + description: Restarts a specific instance by name + parameters: + - description: Instance Name + in: path + name: name + required: true + type: string + produces: + - application/json + responses: + "200": + description: Restarted instance details + schema: + $ref: '#/definitions/llamactl.Instance' + "400": + description: Invalid name format + schema: + type: string + "500": + description: Internal Server Error + schema: + type: string + summary: Restart a running instance + tags: + - instances + /instances/{name}/start: + post: + description: Starts a specific instance by name + parameters: + - description: Instance Name + in: path + name: name + required: true + type: string + produces: + - application/json + responses: + "200": + description: Started instance details + schema: + $ref: '#/definitions/llamactl.Instance' + "400": + description: Invalid name format + schema: + type: string + "500": + description: Internal Server Error + schema: + type: string + summary: Start a stopped instance + tags: + - instances + /instances/{name}/stop: + post: + description: Stops a specific instance by name + parameters: + - description: Instance Name + in: path + name: name + required: true + type: string + produces: + - application/json + responses: + "200": + description: Stopped instance details + schema: + $ref: '#/definitions/llamactl.Instance' + "400": + description: Invalid name format + schema: + type: string + "500": + description: Internal Server Error + schema: + type: string + summary: Stop a running instance + tags: + - instances /server/devices: get: description: Returns a list of available devices for the llama server diff --git a/server/go.mod b/server/go.mod index 72a7b0d..ef37235 100644 --- a/server/go.mod +++ b/server/go.mod @@ -4,7 +4,6 @@ go 1.24.5 require ( github.com/go-chi/chi/v5 v5.2.2 - github.com/google/uuid v1.6.0 github.com/swaggo/http-swagger v1.3.4 github.com/swaggo/swag v1.16.5 ) diff --git a/server/go.sum b/server/go.sum index 6188b22..05d17b6 100644 --- a/server/go.sum +++ b/server/go.sum @@ -14,8 +14,6 @@ github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZ github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= diff --git a/server/pkg/handlers.go b/server/pkg/handlers.go index d92c128..269be60 100644 --- a/server/pkg/handlers.go +++ b/server/pkg/handlers.go @@ -6,7 +6,6 @@ import ( "os/exec" "github.com/go-chi/chi/v5" - "github.com/google/uuid" ) type Handler struct { @@ -82,25 +81,82 @@ func (h *Handler) ListDevicesHandler() http.HandlerFunc { } } -// GetInstance godoc -// @Summary Get details of a specific instance -// @Description Returns the details of a specific instance by ID +// ListInstances godoc +// @Summary List all instances +// @Description Returns a list of all instances managed by the server // @Tags instances -// @Param id path string true "Instance ID" -// @Success 200 {object} Instance "Instance details" -// @Failure 400 {string} string "Invalid UUID format" +// @Produce json +// @Success 200 {array} Instance "List of instances" // @Failure 500 {string} string "Internal Server Error" -// @Router /instances/{id} [get] -func (h *Handler) GetInstance() http.HandlerFunc { +// @Router /instances [get] +func (h *Handler) ListInstances() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - id := chi.URLParam(r, "id") - uuid, err := uuid.Parse(id) + instances, err := h.InstanceManager.ListInstances() if err != nil { - http.Error(w, "Invalid UUID format", http.StatusBadRequest) + http.Error(w, "Failed to list instances: "+err.Error(), http.StatusInternalServerError) return } - instance, err := h.InstanceManager.GetInstance(uuid) + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(instances); err != nil { + http.Error(w, "Failed to encode instances: "+err.Error(), http.StatusInternalServerError) + return + } + } +} + +// CreateInstance godoc +// @Summary Create and start a new instance +// @Description Creates a new instance with the provided configuration options +// @Tags instances +// @Accept json +// @Produce json +// @Param options body InstanceOptions true "Instance configuration options" +// @Success 201 {object} Instance "Created instance details" +// @Failure 400 {string} string "Invalid request body" +// @Failure 500 {string} string "Internal Server Error" +// @Router /instances [post] +func (h *Handler) CreateInstance() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var options InstanceOptions + if err := json.NewDecoder(r.Body).Decode(&options); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + + instance, err := h.InstanceManager.CreateInstance(&options) + if err != nil { + http.Error(w, "Failed to create instance: "+err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + if err := json.NewEncoder(w).Encode(instance); err != nil { + http.Error(w, "Failed to encode instance: "+err.Error(), http.StatusInternalServerError) + return + } + } +} + +// GetInstance godoc +// @Summary Get details of a specific instance +// @Description Returns the details of a specific instance by name +// @Tags instances +// @Param name path string true "Instance Name" +// @Success 200 {object} Instance "Instance details" +// @Failure 400 {string} string "Invalid name format" +// @Failure 500 {string} string "Internal Server Error" +// @Router /instances/{name} [get] +func (h *Handler) GetInstance() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + name := chi.URLParam(r, "name") + if name == "" { + http.Error(w, "Instance name cannot be empty", http.StatusBadRequest) + return + } + + instance, err := h.InstanceManager.GetInstance(name) if err != nil { http.Error(w, "Failed to get instance: "+err.Error(), http.StatusInternalServerError) return @@ -116,22 +172,21 @@ func (h *Handler) GetInstance() http.HandlerFunc { // UpdateInstance godoc // @Summary Update an instance's configuration -// @Description Updates the configuration of a specific instance by ID +// @Description Updates the configuration of a specific instance by name // @Tags instances // @Accept json // @Produce json -// @Param id path string true "Instance ID" +// @Param name path string true "Instance Name" // @Param options body InstanceOptions true "Instance configuration options" // @Success 200 {object} Instance "Updated instance details" -// @Failure 400 {string} string "Invalid UUID format" +// @Failure 400 {string} string "Invalid name format" // @Failure 500 {string} string "Internal Server Error" -// @Router /instances/{id} [put] +// @Router /instances/{name} [put] func (h *Handler) UpdateInstance() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - id := chi.URLParam(r, "id") - uuid, err := uuid.Parse(id) - if err != nil { - http.Error(w, "Invalid UUID format", http.StatusBadRequest) + name := chi.URLParam(r, "name") + if name == "" { + http.Error(w, "Instance name cannot be empty", http.StatusBadRequest) return } @@ -141,13 +196,13 @@ func (h *Handler) UpdateInstance() http.HandlerFunc { return } - instance, err := h.InstanceManager.UpdateInstance(uuid, &options) + instance, err := h.InstanceManager.UpdateInstance(name, &options) if err != nil { http.Error(w, "Failed to update instance: "+err.Error(), http.StatusInternalServerError) return } - instance, err = h.InstanceManager.RestartInstance(uuid) + instance, err = h.InstanceManager.RestartInstance(name) if err != nil { http.Error(w, "Failed to restart instance: "+err.Error(), http.StatusInternalServerError) return @@ -163,24 +218,23 @@ func (h *Handler) UpdateInstance() http.HandlerFunc { // StartInstance godoc // @Summary Start a stopped instance -// @Description Starts a specific instance by ID +// @Description Starts a specific instance by name // @Tags instances // @Produce json -// @Param id path string true "Instance ID" +// @Param name path string true "Instance Name" // @Success 200 {object} Instance "Started instance details" -// @Failure 400 {string} string "Invalid UUID format" +// @Failure 400 {string} string "Invalid name format" // @Failure 500 {string} string "Internal Server Error" -// @Router /instances/{id}/start [post] +// @Router /instances/{name}/start [post] func (h *Handler) StartInstance() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - id := chi.URLParam(r, "id") - uuid, err := uuid.Parse(id) - if err != nil { - http.Error(w, "Invalid UUID format", http.StatusBadRequest) + name := chi.URLParam(r, "name") + if name == "" { + http.Error(w, "Instance name cannot be empty", http.StatusBadRequest) return } - instance, err := h.InstanceManager.StartInstance(uuid) + instance, err := h.InstanceManager.StartInstance(name) if err != nil { http.Error(w, "Failed to start instance: "+err.Error(), http.StatusInternalServerError) return @@ -196,24 +250,23 @@ func (h *Handler) StartInstance() http.HandlerFunc { // StopInstance godoc // @Summary Stop a running instance -// @Description Stops a specific instance by ID +// @Description Stops a specific instance by name // @Tags instances // @Produce json -// @Param id path string true "Instance ID" +// @Param name path string true "Instance Name" // @Success 200 {object} Instance "Stopped instance details" -// @Failure 400 {string} string "Invalid UUID format" +// @Failure 400 {string} string "Invalid name format" // @Failure 500 {string} string "Internal Server Error" -// @Router /instances/{id}/stop [post] +// @Router /instances/{name}/stop [post] func (h *Handler) StopInstance() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - id := chi.URLParam(r, "id") - uuid, err := uuid.Parse(id) - if err != nil { - http.Error(w, "Invalid UUID format", http.StatusBadRequest) + name := chi.URLParam(r, "name") + if name == "" { + http.Error(w, "Instance name cannot be empty", http.StatusBadRequest) return } - instance, err := h.InstanceManager.StopInstance(uuid) + instance, err := h.InstanceManager.StopInstance(name) if err != nil { http.Error(w, "Failed to stop instance: "+err.Error(), http.StatusInternalServerError) return @@ -229,24 +282,23 @@ func (h *Handler) StopInstance() http.HandlerFunc { // RestartInstance godoc // @Summary Restart a running instance -// @Description Restarts a specific instance by ID +// @Description Restarts a specific instance by name // @Tags instances // @Produce json -// @Param id path string true "Instance ID" +// @Param name path string true "Instance Name" // @Success 200 {object} Instance "Restarted instance details" -// @Failure 400 {string} string "Invalid UUID format" +// @Failure 400 {string} string "Invalid name format" // @Failure 500 {string} string "Internal Server Error" -// @Router /instances/{id}/restart [post] +// @Router /instances/{name}/restart [post] func (h *Handler) RestartInstance() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - id := chi.URLParam(r, "id") - uuid, err := uuid.Parse(id) - if err != nil { - http.Error(w, "Invalid UUID format", http.StatusBadRequest) + name := chi.URLParam(r, "name") + if name == "" { + http.Error(w, "Instance name cannot be empty", http.StatusBadRequest) return } - instance, err := h.InstanceManager.RestartInstance(uuid) + instance, err := h.InstanceManager.RestartInstance(name) if err != nil { http.Error(w, "Failed to restart instance: "+err.Error(), http.StatusInternalServerError) return @@ -262,24 +314,23 @@ func (h *Handler) RestartInstance() http.HandlerFunc { // DeleteInstance godoc // @Summary Delete an instance -// @Description Stops and removes a specific instance by ID +// @Description Stops and removes a specific instance by name // @Tags instances // @Produce json -// @Param id path string true "Instance ID" +// @Param name path string true "Instance Name" // @Success 204 "No Content" -// @Failure 400 {string} string "Invalid UUID format" +// @Failure 400 {string} string "Invalid name format" // @Failure 500 {string} string "Internal Server Error" -// @Router /instances/{id} [delete] +// @Router /instances/{name} [delete] func (h *Handler) DeleteInstance() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - id := chi.URLParam(r, "id") - uuid, err := uuid.Parse(id) - if err != nil { - http.Error(w, "Invalid UUID format", http.StatusBadRequest) + name := chi.URLParam(r, "name") + if name == "" { + http.Error(w, "Instance name cannot be empty", http.StatusBadRequest) return } - if err := h.InstanceManager.DeleteInstance(uuid); err != nil { + if err := h.InstanceManager.DeleteInstance(name); err != nil { http.Error(w, "Failed to delete instance: "+err.Error(), http.StatusInternalServerError) return } diff --git a/server/pkg/instance.go b/server/pkg/instance.go index 060d8ff..e00dc60 100644 --- a/server/pkg/instance.go +++ b/server/pkg/instance.go @@ -9,35 +9,33 @@ import ( "os/exec" "sync" "time" - - "github.com/google/uuid" ) type Instance struct { - ID uuid.UUID - options *InstanceOptions + Name string `json:"name"` + Options *InstanceOptions `json:"options,omitempty"` // Status - Running bool + Running bool `json:"running"` // Output channels - StdOutChan chan string // Channel for sending output messages - StdErrChan chan string // Channel for sending error messages + StdOutChan chan string `json:"-"` // Channel for sending output messages + StdErrChan chan string `json:"-"` // Channel for sending error messages // internal - cmd *exec.Cmd // Command to run the instance - ctx context.Context // Context for managing the instance lifecycle - cancel context.CancelFunc // Function to cancel the context - stdout io.ReadCloser // Standard output stream - stderr io.ReadCloser // Standard error stream - mu sync.Mutex // Mutex for synchronizing access to the instance - restarts int // Number of restarts + cmd *exec.Cmd `json:"-"` // Command to run the instance + ctx context.Context `json:"-"` // Context for managing the instance lifecycle + cancel context.CancelFunc `json:"-"` // Function to cancel the context + stdout io.ReadCloser `json:"-"` // Standard output stream + stderr io.ReadCloser `json:"-"` // Standard error stream + mu sync.Mutex `json:"-"` // Mutex for synchronizing access to the instance + restarts int `json:"-"` // Number of restarts } -func NewInstance(id uuid.UUID, options *InstanceOptions) *Instance { +func NewInstance(name string, options *InstanceOptions) *Instance { return &Instance{ - ID: id, - options: options, + Name: name, + Options: options, Running: false, @@ -49,17 +47,17 @@ func NewInstance(id uuid.UUID, options *InstanceOptions) *Instance { func (i *Instance) GetOptions() *InstanceOptions { i.mu.Lock() defer i.mu.Unlock() - return i.options + return i.Options } func (i *Instance) SetOptions(options *InstanceOptions) { i.mu.Lock() defer i.mu.Unlock() if options == nil { - log.Println("Warning: Attempted to set nil options on instance", i.ID) + log.Println("Warning: Attempted to set nil options on instance", i.Name) return } - i.options = options + i.Options = options } func (i *Instance) Start() error { @@ -67,10 +65,10 @@ func (i *Instance) Start() error { defer i.mu.Unlock() if i.Running { - return fmt.Errorf("instance %s is already running", i.ID) + return fmt.Errorf("instance %s is already running", i.Name) } - args := i.options.BuildCommandArgs() + args := i.Options.BuildCommandArgs() i.ctx, i.cancel = context.WithCancel(context.Background()) i.cmd = exec.CommandContext(i.ctx, "llama-server", args...) @@ -86,7 +84,7 @@ func (i *Instance) Start() error { } if err := i.cmd.Start(); err != nil { - return fmt.Errorf("failed to start instance %s: %w", i.ID, err) + return fmt.Errorf("failed to start instance %s: %w", i.Name, err) } i.Running = true @@ -105,7 +103,7 @@ func (i *Instance) Stop() error { defer i.mu.Unlock() if !i.Running { - return fmt.Errorf("instance %s is not running", i.ID) + return fmt.Errorf("instance %s is not running", i.Name) } // Cancel the context to signal termination @@ -147,7 +145,7 @@ func (i *Instance) readOutput(reader io.ReadCloser, ch chan string, streamType s case ch <- line: default: // Channel is full, drop the line - log.Printf("Dropped %s line for instance %s: %s", streamType, i.ID, line) + log.Printf("Dropped %s line for instance %s: %s", streamType, i.Name, line) } } } @@ -166,30 +164,30 @@ func (i *Instance) monitorProcess() { // Log the exit if err != nil { - log.Printf("Instance %s crashed with error: %v", i.ID, err) + log.Printf("Instance %s crashed with error: %v", i.Name, err) } else { - log.Printf("Instance %s exited cleanly", i.ID) + log.Printf("Instance %s exited cleanly", i.Name) } // Handle restart if process crashed and auto-restart is enabled - if err != nil && i.options.AutoRestart && i.restarts < i.options.MaxRestarts { + if err != nil && i.Options.AutoRestart && i.restarts < i.Options.MaxRestarts { i.restarts++ log.Printf("Auto-restarting instance %s (attempt %d/%d) in %v", - i.ID, i.restarts, i.options.MaxRestarts, i.options.RestartDelay) + i.Name, i.restarts, i.Options.MaxRestarts, i.Options.RestartDelay.ToDuration()) // Unlock mutex during sleep to avoid blocking other operations i.mu.Unlock() - time.Sleep(i.options.RestartDelay) + time.Sleep(i.Options.RestartDelay.ToDuration()) i.mu.Lock() // Attempt restart if err := i.Start(); err != nil { - log.Printf("Failed to restart instance %s: %v", i.ID, err) + log.Printf("Failed to restart instance %s: %v", i.Name, err) } else { - log.Printf("Successfully restarted instance %s", i.ID) + log.Printf("Successfully restarted instance %s", i.Name) i.restarts = 0 // Reset restart count on successful restart } - } else if i.restarts >= i.options.MaxRestarts { - log.Printf("Instance %s exceeded max restart attempts (%d)", i.ID, i.options.MaxRestarts) + } else if i.restarts >= i.Options.MaxRestarts { + log.Printf("Instance %s exceeded max restart attempts (%d)", i.Name, i.Options.MaxRestarts) } } diff --git a/server/pkg/manager.go b/server/pkg/manager.go index a675eb1..1558022 100644 --- a/server/pkg/manager.go +++ b/server/pkg/manager.go @@ -2,25 +2,23 @@ package llamactl import ( "fmt" - - "github.com/google/uuid" ) // InstanceManager defines the interface for managing instances of the llama server. type InstanceManager interface { ListInstances() ([]*Instance, error) CreateInstance(options *InstanceOptions) (*Instance, error) - GetInstance(id uuid.UUID) (*Instance, error) - UpdateInstance(id uuid.UUID, options *InstanceOptions) (*Instance, error) - DeleteInstance(id uuid.UUID) error - StartInstance(id uuid.UUID) (*Instance, error) - StopInstance(id uuid.UUID) (*Instance, error) - RestartInstance(id uuid.UUID) (*Instance, error) - GetInstanceLogs(id uuid.UUID) (string, error) + GetInstance(name string) (*Instance, error) + UpdateInstance(name string, options *InstanceOptions) (*Instance, error) + DeleteInstance(name string) error + StartInstance(name string) (*Instance, error) + StopInstance(name string) (*Instance, error) + RestartInstance(name string) (*Instance, error) + GetInstanceLogs(name string) (string, error) } type instanceManager struct { - instances map[uuid.UUID]*Instance + instances map[string]*Instance portRange [][2]int // Range of ports to use for instances ports map[int]bool } @@ -28,7 +26,7 @@ type instanceManager struct { // NewInstanceManager creates a new instance of InstanceManager. func NewInstanceManager() InstanceManager { return &instanceManager{ - instances: make(map[uuid.UUID]*Instance), + instances: make(map[string]*Instance), portRange: [][2]int{{8000, 9000}}, ports: make(map[int]bool), } @@ -50,10 +48,14 @@ func (im *instanceManager) CreateInstance(options *InstanceOptions) (*Instance, return nil, fmt.Errorf("instance options cannot be nil") } - // Generate a unique ID for the new instance - id := uuid.New() - for im.instances[id] != nil { - id = uuid.New() // Ensure unique ID + // Check if name is provided + if options.Name == "" { + return nil, fmt.Errorf("instance name cannot be empty") + } + + // Check if instance with this name already exists + if im.instances[options.Name] != nil { + return nil, fmt.Errorf("instance with name %s already exists", options.Name) } // Assign a port if not specified @@ -65,101 +67,101 @@ func (im *instanceManager) CreateInstance(options *InstanceOptions) (*Instance, options.Port = port } - instance := NewInstance(id, options) - im.instances[instance.ID] = instance + instance := NewInstance(options.Name, options) + im.instances[instance.Name] = instance return instance, nil } -// GetInstance retrieves an instance by its ID. -func (im *instanceManager) GetInstance(id uuid.UUID) (*Instance, error) { - instance, exists := im.instances[id] +// GetInstance retrieves an instance by its name. +func (im *instanceManager) GetInstance(name string) (*Instance, error) { + instance, exists := im.instances[name] if !exists { - return nil, fmt.Errorf("instance with ID %s not found", id) + return nil, fmt.Errorf("instance with name %s not found", name) } return instance, nil } // UpdateInstance updates the options of an existing instance and returns it. -func (im *instanceManager) UpdateInstance(id uuid.UUID, options *InstanceOptions) (*Instance, error) { - instance, exists := im.instances[id] +func (im *instanceManager) UpdateInstance(name string, options *InstanceOptions) (*Instance, error) { + instance, exists := im.instances[name] if !exists { - return nil, fmt.Errorf("instance with ID %s not found", id) + return nil, fmt.Errorf("instance with name %s not found", name) } instance.SetOptions(options) return instance, nil } -// DeleteInstance removes stopped instance by its ID. -func (im *instanceManager) DeleteInstance(id uuid.UUID) error { - _, exists := im.instances[id] +// DeleteInstance removes stopped instance by its name. +func (im *instanceManager) DeleteInstance(name string) error { + _, exists := im.instances[name] if !exists { - return fmt.Errorf("instance with ID %s not found", id) + return fmt.Errorf("instance with name %s not found", name) } - if im.instances[id].Running { - return fmt.Errorf("instance with ID %s is still running, stop it before deleting", id) + if im.instances[name].Running { + return fmt.Errorf("instance with name %s is still running, stop it before deleting", name) } - delete(im.instances, id) + delete(im.instances, name) return nil } // StartInstance starts a stopped instance and returns it. // If the instance is already running, it returns an error. -func (im *instanceManager) StartInstance(id uuid.UUID) (*Instance, error) { - instance, exists := im.instances[id] +func (im *instanceManager) StartInstance(name string) (*Instance, error) { + instance, exists := im.instances[name] if !exists { - return nil, fmt.Errorf("instance with ID %s not found", id) + return nil, fmt.Errorf("instance with name %s not found", name) } if instance.Running { - return instance, fmt.Errorf("instance with ID %s is already running", id) + return instance, fmt.Errorf("instance with name %s is already running", name) } if err := instance.Start(); err != nil { - return nil, fmt.Errorf("failed to start instance %s: %w", id, err) + return nil, fmt.Errorf("failed to start instance %s: %w", name, err) } return instance, nil } // StopInstance stops a running instance and returns it. -func (im *instanceManager) StopInstance(id uuid.UUID) (*Instance, error) { - instance, exists := im.instances[id] +func (im *instanceManager) StopInstance(name string) (*Instance, error) { + instance, exists := im.instances[name] if !exists { - return nil, fmt.Errorf("instance with ID %s not found", id) + return nil, fmt.Errorf("instance with name %s not found", name) } if !instance.Running { - return instance, fmt.Errorf("instance with ID %s is already stopped", id) + return instance, fmt.Errorf("instance with name %s is already stopped", name) } if err := instance.Stop(); err != nil { - return nil, fmt.Errorf("failed to stop instance %s: %w", id, err) + return nil, fmt.Errorf("failed to stop instance %s: %w", name, err) } return instance, nil } // RestartInstance stops and then starts an instance, returning the updated instance. -func (im *instanceManager) RestartInstance(id uuid.UUID) (*Instance, error) { - instance, err := im.StopInstance(id) +func (im *instanceManager) RestartInstance(name string) (*Instance, error) { + instance, err := im.StopInstance(name) if err != nil { return nil, err } - return im.StartInstance(instance.ID) + return im.StartInstance(instance.Name) } -// GetInstanceLogs retrieves the logs for a specific instance by its ID. -func (im *instanceManager) GetInstanceLogs(id uuid.UUID) (string, error) { - _, exists := im.instances[id] +// GetInstanceLogs retrieves the logs for a specific instance by its name. +func (im *instanceManager) GetInstanceLogs(name string) (string, error) { + _, exists := im.instances[name] if !exists { - return "", fmt.Errorf("instance with ID %s not found", id) + return "", fmt.Errorf("instance with name %s not found", name) } // TODO: Implement actual log retrieval logic - return fmt.Sprintf("Logs for instance %s", id), nil + return fmt.Sprintf("Logs for instance %s", name), nil } func (im *instanceManager) getNextAvailablePort() (int, error) { diff --git a/server/pkg/options.go b/server/pkg/options.go index 44970cb..d18c40f 100644 --- a/server/pkg/options.go +++ b/server/pkg/options.go @@ -8,13 +8,37 @@ import ( "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 { Name string `json:"name,omitempty"` // Display name // Auto restart - AutoRestart bool `json:"auto_restart,omitempty"` - MaxRestarts int `json:"max_restarts,omitempty"` - RestartDelay time.Duration `json:"restart_delay,omitempty"` // in seconds + AutoRestart bool `json:"auto_restart,omitempty"` + MaxRestarts int `json:"max_restarts,omitempty"` + RestartDelay Duration `json:"restart_delay,omitempty" example:"5"` // Duration in seconds *LlamaServerOptions } @@ -24,7 +48,7 @@ func (o *InstanceOptions) UnmarshalJSON(data []byte) error { // Set defaults first o.AutoRestart = true o.MaxRestarts = 3 - o.RestartDelay = 5 + o.RestartDelay = Duration(5 * time.Second) // 5 seconds // Create a temporary struct to avoid recursion type tempInstanceOptions InstanceOptions diff --git a/server/pkg/routes.go b/server/pkg/routes.go index 0a0a365..5aaf943 100644 --- a/server/pkg/routes.go +++ b/server/pkg/routes.go @@ -26,10 +26,10 @@ func SetupRouter(handler *Handler) *chi.Mux { // Instance management endpoints r.Route("/instances", func(r chi.Router) { - // r.Get("/", handler.ListInstances()) // List all instances - // r.Post("/", handler.CreateInstance()) // Create and start new instance + r.Get("/", handler.ListInstances()) // List all instances + r.Post("/", handler.CreateInstance()) // Create and start new instance - r.Route("/{id}", func(r chi.Router) { + r.Route("/{name}", func(r chi.Router) { // Instance management r.Get("/", handler.GetInstance()) // Get instance details r.Put("/", handler.UpdateInstance()) // Update instance configuration