From 7a103148b2786e767b04881694840447bf1ae4ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andy=20M=C3=A9ry?= Date: Wed, 27 Nov 2024 16:53:51 +0100 Subject: [PATCH 1/8] feat(ci): add simple publish workflow + add userAgent --- .github/workflows/publish.yml | 26 ++++++++++++++++++++++++++ package.json | 2 +- shared/api/index.js | 3 +++ 3 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/publish.yml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..1355287 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,26 @@ +--- +name: "publish" + +on: + release: + types: [published] + +jobs: + release: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + + - name: "Set up Node" + uses: actions/setup-node@v4 + with: + node-version: 20.x + registry-url: "https://registry.npmjs.org" + + - name: "Install dependencies" + run: npm ci + + - name: "Publish package on NPM" + run: npm publish --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/package.json b/package.json index e439916..450aa5a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "serverless-scaleway-functions", - "version": "0.4.12", + "version": "0.4.13", "description": "Provider plugin for the Serverless Framework v1.x which adds support for Scaleway Functions.", "main": "index.js", "author": "scaleway.com", diff --git a/shared/api/index.js b/shared/api/index.js index 68219ba..cc13110 100644 --- a/shared/api/index.js +++ b/shared/api/index.js @@ -14,10 +14,13 @@ const runtimesApi = require("./runtimes"); // Registry const RegistryApi = require("./registry"); +const version = "0.4.13"; + function getApiManager(apiUrl, token) { return axios.create({ baseURL: apiUrl, headers: { + "User-Agent": `serverless-scaleway-functions/${version}`, "X-Auth-Token": token, }, httpsAgent: new https.Agent({ From 2fe94cfcaefbaaf07998f44f374347dbac9c0d16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andy=20M=C3=A9ry?= Date: Wed, 27 Nov 2024 16:57:06 +0100 Subject: [PATCH 2/8] fix: do the same for registry client --- shared/api/registry.js | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/shared/api/registry.js b/shared/api/registry.js index 9212d31..b0d7b24 100644 --- a/shared/api/registry.js +++ b/shared/api/registry.js @@ -1,20 +1,11 @@ "use strict"; -const axios = require("axios"); -const https = require("https"); +const { getApiManager } = require("./index"); const { manageError } = require("./utils"); class RegistryApi { constructor(registryApiUrl, token) { - this.apiManager = axios.create({ - baseURL: registryApiUrl, - headers: { - "X-Auth-Token": token, - }, - httpsAgent: new https.Agent({ - rejectUnauthorized: false, - }), - }); + this.apiManager = getApiManager(registryApiUrl, token); } listRegistryNamespace(projectId) { From 63ef10ae66c0a2ae80a61744ec2953461963695d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andy=20M=C3=A9ry?= Date: Wed, 27 Nov 2024 17:02:37 +0100 Subject: [PATCH 3/8] fix: updated changelog entry + lock --- CHANGELOG.md | 6 ++++++ package-lock.json | 27 +++++++++++++++------------ 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e509ef0..7ea2019 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.4.13 + +### Changed + +- HTTP calls to `api.scaleway.com` are now made with a custom user agent #245 + ## 0.4.12 ### Fixed diff --git a/package-lock.json b/package-lock.json index 025bf68..1d96c57 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "serverless-scaleway-functions", - "version": "0.4.12", + "version": "0.4.13", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "serverless-scaleway-functions", - "version": "0.4.12", + "version": "0.4.13", "license": "MIT", "dependencies": { "@serverless/utils": "^6.13.1", @@ -1793,9 +1793,10 @@ } }, "node_modules/axios": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", - "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "version": "1.7.8", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.8.tgz", + "integrity": "sha512-Uu0wb7KNqK2t5K+YQyVCLM76prD5sRFjKHbJYCP1J7JFGEQ6nN7HWn9+04LAeiJ3ji54lgS/gZCH1oxyrf1SPw==", + "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -2494,10 +2495,11 @@ } }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -5619,12 +5621,13 @@ "dev": true }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, + "license": "MIT", "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { From 0a3d3963f174a50c2029b674f8a41e1f5b057763 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andy=20M=C3=A9ry?= Date: Thu, 28 Nov 2024 11:46:07 +0100 Subject: [PATCH 4/8] feat(container): add healthCheck support --- README.md | 7 ++++ deploy/lib/createContainers.js | 22 ++++++++++++ .../container/my-container/requirements.txt | 2 +- examples/container/my-container/server.py | 17 ++++++---- examples/container/serverless.yml | 4 +++ shared/api/index.js | 19 +---------- shared/api/registry.js | 2 +- shared/api/utils.js | 34 ++++++++++++++++++- 8 files changed, 80 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index b745d50..5149bc5 100644 --- a/README.md +++ b/README.md @@ -237,6 +237,13 @@ custom: custom_domains: - my-container.some.domain.com + # Health check configuration + healthCheck: + type: http # Or tcp if you only want to check that the port is open + httpPath: /health + interval: 10s + failureThreshold: 3 + # List of events to trigger the container events: - schedule: diff --git a/deploy/lib/createContainers.js b/deploy/lib/createContainers.js index 02702b9..9888d87 100644 --- a/deploy/lib/createContainers.js +++ b/deploy/lib/createContainers.js @@ -5,6 +5,26 @@ const singleSource = require("../../shared/singleSource"); const secrets = require("../../shared/secrets"); const domainUtils = require("../../shared/domains"); +function adaptHealthCheckToAPI(healthCheck) { + if (!healthCheck) { + return null; + } + + // We need to find the type of the health check (tcp, http, ...) + // If httpPath is provided, we default to http, otherwise we default to tcp + let type = healthCheck.httpPath ? "http" : "tcp"; + if (healthCheck.type) { + type = healthCheck.type; + } + + return { + failure_threshold: healthCheck.failureThreshold, + interval: healthCheck.interval, + ...(type === "http" && { http: { path: healthCheck.httpPath || "/" } }), + ...(type === "tcp" && { tcp: {} }), + }; +} + module.exports = { createContainers() { return BbPromise.bind(this) @@ -104,6 +124,7 @@ module.exports = { port: container.port, http_option: container.httpOption, sandbox: container.sandbox, + health_check: adaptHealthCheckToAPI(container.healthCheck), }; // checking if there is custom_domains set on container creation. @@ -143,6 +164,7 @@ module.exports = { privacy: container.privacy, port: container.port, http_option: container.httpOption, + health_check: adaptHealthCheckToAPI(container.healthCheck), }; this.serverless.cli.log(`Updating container ${container.name}...`); diff --git a/examples/container/my-container/requirements.txt b/examples/container/my-container/requirements.txt index e3e9a71..f08dea7 100644 --- a/examples/container/my-container/requirements.txt +++ b/examples/container/my-container/requirements.txt @@ -1 +1 @@ -Flask +flask~=3.1.0 diff --git a/examples/container/my-container/server.py b/examples/container/my-container/server.py index e5fea35..b9f8bf8 100644 --- a/examples/container/my-container/server.py +++ b/examples/container/my-container/server.py @@ -1,6 +1,5 @@ -from flask import Flask +from flask import Flask, jsonify import os -import json DEFAULT_PORT = "8080" MESSAGE = "Hello, World from Scaleway Container !" @@ -9,12 +8,18 @@ @app.route("/") def root(): - return json.dumps({ + return jsonify({ "message": MESSAGE }) +@app.route("/health") +def health(): + # You could add more complex logic here, for example checking the health of a database... + return jsonify({ + "status": "UP" + }) + if __name__ == "__main__": # Scaleway's system will inject a PORT environment variable on which your application should start the server. - port_env = os.getenv("PORT", DEFAULT_PORT) - port = int(port_env) - app.run(debug=True, host="0.0.0.0", port=port) + port = os.getenv("PORT", DEFAULT_PORT) + app.run(host="0.0.0.0", port=int(port)) diff --git a/examples/container/serverless.yml b/examples/container/serverless.yml index 4964b24..2f7a89e 100644 --- a/examples/container/serverless.yml +++ b/examples/container/serverless.yml @@ -32,3 +32,7 @@ custom: # Local environment variables - used only in given function env: local: local + healthCheck: + httpPath: /health + interval: 10s + failureThreshold: 3 diff --git a/shared/api/index.js b/shared/api/index.js index cc13110..375ebd0 100644 --- a/shared/api/index.js +++ b/shared/api/index.js @@ -1,6 +1,4 @@ -const https = require("https"); -const axios = require("axios"); - +const { getApiManager } = require("./utils"); const accountApi = require("./account"); const domainApi = require("./domain"); const namespacesApi = require("./namespaces"); @@ -14,21 +12,6 @@ const runtimesApi = require("./runtimes"); // Registry const RegistryApi = require("./registry"); -const version = "0.4.13"; - -function getApiManager(apiUrl, token) { - return axios.create({ - baseURL: apiUrl, - headers: { - "User-Agent": `serverless-scaleway-functions/${version}`, - "X-Auth-Token": token, - }, - httpsAgent: new https.Agent({ - rejectUnauthorized: false, - }), - }); -} - class AccountApi { constructor(apiUrl, token) { this.apiManager = getApiManager(apiUrl, token); diff --git a/shared/api/registry.js b/shared/api/registry.js index b0d7b24..b0a54f4 100644 --- a/shared/api/registry.js +++ b/shared/api/registry.js @@ -1,6 +1,6 @@ "use strict"; -const { getApiManager } = require("./index"); +const { getApiManager } = require("./utils"); const { manageError } = require("./utils"); class RegistryApi { diff --git a/shared/api/utils.js b/shared/api/utils.js index 70116ec..15b95bb 100644 --- a/shared/api/utils.js +++ b/shared/api/utils.js @@ -1,3 +1,23 @@ +const axios = require("axios"); +const https = require("https"); + +const version = "0.4.13"; + +const invalidArgumentsType = "invalid_arguments"; + +function getApiManager(apiUrl, token) { + return axios.create({ + baseURL: apiUrl, + headers: { + "User-Agent": `serverless-scaleway-functions/${version}`, + "X-Auth-Token": token, + }, + httpsAgent: new https.Agent({ + rejectUnauthorized: false, + }), + }); +} + /** * Custom Error class, to print an error message, and pass the Response if applicable */ @@ -20,13 +40,25 @@ function manageError(err) { throw new Error(err); } if (err.response.data.message) { - throw new CustomError(err.response.data.message, err.response); + let message = err.response.data.message; + + // In case the error is an InvalidArgumentsError, provide some extra information + if (err.response.data.type === invalidArgumentsType) { + for (const details of err.response.data.details) { + const argumentName = details.argument_name; + const helpMessage = details.help_message; + message += `\n${argumentName}: ${helpMessage}`; + } + } + + throw new CustomError(message, err.response); } else if (err.response.data.error_message) { throw new CustomError(err.response.data.error_message, err.response); } } module.exports = { + getApiManager, manageError, CustomError, }; From 38b06e0ececf12fdf8dcc30c44208ec98193d3b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andy=20M=C3=A9ry?= Date: Thu, 28 Nov 2024 11:55:44 +0100 Subject: [PATCH 5/8] fix: format --- shared/api/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/api/utils.js b/shared/api/utils.js index 15b95bb..2413649 100644 --- a/shared/api/utils.js +++ b/shared/api/utils.js @@ -41,7 +41,7 @@ function manageError(err) { } if (err.response.data.message) { let message = err.response.data.message; - + // In case the error is an InvalidArgumentsError, provide some extra information if (err.response.data.type === invalidArgumentsType) { for (const details of err.response.data.details) { From 46f9d734313fb04ff811411f2908d5e1766dcbf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andy=20M=C3=A9ry?= Date: Thu, 28 Nov 2024 17:43:25 +0100 Subject: [PATCH 6/8] feat: add scaling option + bump version --- CHANGELOG.md | 15 ++++++++++++ README.md | 17 +++++++++++-- deploy/lib/createContainers.js | 40 +++++++++++++++++++++++++++++++ examples/container/serverless.yml | 4 +++- 4 files changed, 73 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ea2019..d9e04af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## 0.4.14 + +### Added + +- Added `healthCheck` to define a health check for containers +- Added `scalingOption` to allow scaling on concurrent requests, cpu usage or memory usage + +### Fixed + +- Updating an existing function or container `sandbox` option was not working + +### Changed + +- Following the introduction of `scalingOption`, the `maxConcurrency` parameter is now deprecated. It will continue to work but we invite you to use `scalingOption` of type `concurrentRequests` instead. + ## 0.4.13 ### Changed diff --git a/README.md b/README.md index 5149bc5..bfa05e3 100644 --- a/README.md +++ b/README.md @@ -200,8 +200,17 @@ custom: minScale: 0 maxScale: 10 - # Number of simultaneous requests to handle simultaneously - maxConcurrency: 20 + # Configuration used to decide when to scale the container up or down + scalingOption: + # Can be one of: concurrentRequests, cpuUsage, memoryUsage + type: concurrentRequests + # Value to trigger scaling up + # It's expressed in: + # - concurrentRequests: number of requests + # - cpuUsage: percentage of CPU usage + # - memoryUsage: percentage of memory usage + # Note that cpu and memory scaling are only available for minScale >= 1 containers + threshold: 50 # Memory limit (in MiB) # Limits: https://www.scaleway.com/en/docs/serverless/containers/reference-content/containers-limitations/ @@ -252,6 +261,10 @@ custom: input: key-a: "value-a" key-b: "value-b" + + # Deprecated: number of simultaneous requests to handle simultaneously + # Please use scalingOption of type concurrentRequests instead + maxConcurrency: 20 ``` ## Supported commands diff --git a/deploy/lib/createContainers.js b/deploy/lib/createContainers.js index 9888d87..af31df9 100644 --- a/deploy/lib/createContainers.js +++ b/deploy/lib/createContainers.js @@ -5,6 +5,9 @@ const singleSource = require("../../shared/singleSource"); const secrets = require("../../shared/secrets"); const domainUtils = require("../../shared/domains"); +const maxConcurrencyDeprecationWarning = `WARNING: maxConcurrency is deprecated and has been replaced by scalingOption of type: concurrentRequests. +Please update your serverless.yml file.`; + function adaptHealthCheckToAPI(healthCheck) { if (!healthCheck) { return null; @@ -25,6 +28,31 @@ function adaptHealthCheckToAPI(healthCheck) { }; } +const scalingOptionToAPIProperty = { + concurrentRequests: "concurrent_requests_threshold", + cpuUsage: "cpu_usage_threshold", + memoryUsage: "memory_usage_threshold", +}; + +function adaptScalingOptionToAPI(scalingOption) { + if (!scalingOption || !scalingOption.type) { + return null; + } + + const property = scalingOptionToAPIProperty[scalingOption.type]; + if (!property) { + throw new Error( + `scalingOption.type must be one of: ${Object.keys( + scalingOptionToAPIProperty + ).join(", ")}` + ); + } + + return { + [property]: scalingOption.threshold, + }; +} + module.exports = { createContainers() { return BbPromise.bind(this) @@ -125,6 +153,7 @@ module.exports = { http_option: container.httpOption, sandbox: container.sandbox, health_check: adaptHealthCheckToAPI(container.healthCheck), + scaling_option: adaptScalingOptionToAPI(container.scalingOption), }; // checking if there is custom_domains set on container creation. @@ -135,6 +164,11 @@ module.exports = { ); } + // note about maxConcurrency deprecation + if (container.maxConcurrency) { + this.serverless.cli.log(maxConcurrencyDeprecationWarning); + } + this.serverless.cli.log(`Creating container ${container.name}...`); return this.createContainer(params).then((response) => @@ -165,8 +199,14 @@ module.exports = { port: container.port, http_option: container.httpOption, health_check: adaptHealthCheckToAPI(container.healthCheck), + scaling_option: adaptScalingOptionToAPI(container.scalingOption), }; + // note about maxConcurrency deprecation + if (container.maxConcurrency) { + this.serverless.cli.log(maxConcurrencyDeprecationWarning); + } + this.serverless.cli.log(`Updating container ${container.name}...`); // assign domains diff --git a/examples/container/serverless.yml b/examples/container/serverless.yml index 2f7a89e..3e61148 100644 --- a/examples/container/serverless.yml +++ b/examples/container/serverless.yml @@ -26,7 +26,9 @@ custom: # memoryLimit: 256 # cpuLimit: 140 # maxScale: 2 - # maxConcurrency: 50 + # scalingOption: + # type: concurrentRequests + # threshold: 50 # timeout: "20s" # httpOption: redirected # Local environment variables - used only in given function From 19d58df27f548891d5ac421705ce83e738f08889 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andy=20M=C3=A9ry?= Date: Thu, 28 Nov 2024 17:50:55 +0100 Subject: [PATCH 7/8] docs: pleonasm --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bfa05e3..539052b 100644 --- a/README.md +++ b/README.md @@ -262,7 +262,7 @@ custom: key-a: "value-a" key-b: "value-b" - # Deprecated: number of simultaneous requests to handle simultaneously + # Deprecated: number of simultaneous requests to handle # Please use scalingOption of type concurrentRequests instead maxConcurrency: 20 ``` From c011640533e72183760b564a2bc788f4ada53377 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andy=20M=C3=A9ry?= Date: Fri, 29 Nov 2024 12:45:38 +0100 Subject: [PATCH 8/8] fix: comment maxConcurrency in README Co-authored-by: Emilie BOUIN <48752456+Bemilie@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 539052b..1a88ae5 100644 --- a/README.md +++ b/README.md @@ -264,7 +264,7 @@ custom: # Deprecated: number of simultaneous requests to handle # Please use scalingOption of type concurrentRequests instead - maxConcurrency: 20 + # maxConcurrency: 20 ``` ## Supported commands