diff --git a/CHANGELOG.md b/CHANGELOG.md index fa32a86..fae7ad7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,22 @@ All notable changes to this project will be documented in this file. +## [v2.2.1] - 2026-04-14 + +### Features + +- **Multi-Method Support**: Functions now support `GET`, `POST`, `PUT`, `PATCH`, and `DELETE` methods for invocations. The request method is passed to the function handler via `req.method`. +- **Automatic Pool Cleanup**: Warm container pools are now immediately drained and containers are stopped when a function is deleted. + +### Bug Fixes & Improvements + +- **Resource Management**: Fixed a temporary file leak in the deployment process where zip files were not cleaned up from `/tmp`. +- **Handler Robustness**: Improved the standard test function to handle null headers and gracefully echo request metadata. +- **CI/CD**: Added integration test steps to the GitHub Actions workflow to ensure multi-method and pool management stability. +- **API Reliability**: Corrected response status handling and updated internal versioning variables. + +--- + ## [v2.2.0] - 2026-04-13 ### Features diff --git a/internal/api/delete.go b/internal/api/delete.go index 7332f8e..2f1bbb4 100644 --- a/internal/api/delete.go +++ b/internal/api/delete.go @@ -50,6 +50,9 @@ func deleteFuncHandler(c *gin.Context) { // We don't return here because files and metadata are already gone } + // Clean up container pool + config.PoolManager.DeletePool(c, config.DockerClient, name) + c.JSON(http.StatusOK, gin.H{ "deleted": name, }) diff --git a/internal/api/invoke.go b/internal/api/invoke.go index c5b0da3..0c635eb 100644 --- a/internal/api/invoke.go +++ b/internal/api/invoke.go @@ -15,7 +15,9 @@ import ( ) func registerInvokeRoutes(router *gin.Engine) { - router.POST("/invoke/:name", invokeHandler) + for _, method := range []string{"GET", "POST", "PUT", "PATCH", "DELETE"} { + router.Handle(method, "/invoke/:name", invokeHandler) + } } func invokeHandler(c *gin.Context) { @@ -45,17 +47,22 @@ func invokeHandler(c *gin.Context) { } // Read request body - bodyBytes, err := c.GetRawData() - if err != nil { - log.Println("ERROR: " + err.Error()) - c.JSON(http.StatusBadRequest, gin.H{ - "error": "Failed to read request", - }) - return + var bodyBytes []byte + if c.Request.Body != nil { + var err error + bodyBytes, err = c.GetRawData() + if err != nil { + log.Println("ERROR: " + err.Error()) + c.JSON(http.StatusBadRequest, gin.H{ + "error": "Failed to read request", + }) + return + } } // Build request object req := functions.InvokeRequest{ + Method: c.Request.Method, Headers: headers, Body: string(bodyBytes), } diff --git a/internal/functions/invoke.go b/internal/functions/invoke.go index 8975618..834a499 100644 --- a/internal/functions/invoke.go +++ b/internal/functions/invoke.go @@ -21,6 +21,7 @@ import ( var ErrRateLimited = errors.New("rate limit exceeded") type InvokeRequest struct { + Method string `json:"method"` Headers map[string]string `json:"headers"` Body string `json:"body"` } diff --git a/internal/functions/invoke_test.go b/internal/functions/invoke_test.go index ad2ded3..8fec781 100644 --- a/internal/functions/invoke_test.go +++ b/internal/functions/invoke_test.go @@ -64,6 +64,7 @@ func TestInvoke_HappyPath(t *testing.T) { } req := functions.InvokeRequest{ + Method: "POST", Headers: map[string]string{ "Content-Type": "application/json", }, @@ -94,4 +95,51 @@ func TestInvoke_HappyPath(t *testing.T) { if _, ok := body["json"]; !ok { t.Fatalf("expected json field in response body") } + + if body["method"] != "POST" { + t.Errorf("expected default method POST, got %s", body["method"]) + } +} + +func TestInvoke_Methods(t *testing.T) { + cleanup := setupInvokeEnv(t) + defer cleanup() + + funcName := "methods-func" + if err := functions.Deploy(validZipFile, funcName, 0); err != nil { + t.Fatalf("deploy failed: %v", err) + } + + d := &docker.Docker{ + WorkerPath: config.WorkerPath, + } + defer d.Close() + ctx := t.Context() + defer config.PoolManager.DeleteAllContainers(ctx, d) + + methods := []string{"GET", "POST", "PUT", "PATCH", "DELETE"} + for _, method := range methods { + t.Run(method, func(t *testing.T) { + req := functions.InvokeRequest{ + Method: method, + } + res, err := functions.Invoke(ctx, d, funcName, req) + if err != nil { + t.Fatalf("invoke failed for %s: %v", method, err) + } + + if res.StatusCode != 200 { + t.Fatalf("expected status 200, got %d", res.StatusCode) + } + + var body map[string]any + if err := json.Unmarshal(res.Body, &body); err != nil { + t.Fatalf("invalid response body: %v", err) + } + + if body["method"] != method { + t.Errorf("expected method %s, got %s", method, body["method"]) + } + }) + } } diff --git a/internal/pool/manager.go b/internal/pool/manager.go index 4419f85..c36dfb5 100644 --- a/internal/pool/manager.go +++ b/internal/pool/manager.go @@ -49,8 +49,24 @@ func parseRateLimit(rateLimit int) (rate.Limit, int) { return rate.Limit(rateLimit), burst } -func (pm *PoolManager) Delete(funcName string) { - pm.pools.Delete(funcName) +func (pm *PoolManager) DeletePool(ctx context.Context, d *docker.Docker, funcName string) { + if val, ok := pm.pools.Load(funcName); ok { + p := val.(*ContainerPool) + // Drain the pool and remove containers + for { + select { + case e := <-p.Idle: + err := d.ContainerRemove(ctx, e.ContainerID) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to delete container (%s) on pool deletion: %s\n", e.ContainerID[:12], err) + } + os.RemoveAll(e.SocketPath) + default: + pm.pools.Delete(funcName) + return + } + } + } } func (pm *PoolManager) DeleteAllContainers(ctx context.Context, d *docker.Docker) { diff --git a/internal/pool/manager_test.go b/internal/pool/manager_test.go index 2c6d4be..7181ed7 100644 --- a/internal/pool/manager_test.go +++ b/internal/pool/manager_test.go @@ -1,6 +1,7 @@ package pool import ( + "context" "testing" "golang.org/x/time/rate" ) @@ -26,7 +27,7 @@ func TestPoolManager_GetOrCreate(t *testing.T) { } // Test Delete - pm.Delete("func-1") + pm.DeletePool(context.Background(), nil, "func-1") // Should create a new one now since the old was deleted p4 := pm.GetOrCreate("func-1", 0, 1) diff --git a/test_data/zip/valid.zip b/test_data/zip/valid.zip index 9002bc9..023843c 100644 Binary files a/test_data/zip/valid.zip and b/test_data/zip/valid.zip differ