├── examples ├── go │ ├── mypackage │ │ ├── go.sum │ │ ├── go.mod │ │ └── handler.go │ ├── go.mod │ ├── cmd │ │ └── main.go │ ├── package.json │ ├── go.sum │ ├── handler.go │ ├── serverless.yml │ ├── package-lock.json │ └── README.md ├── container-schedule │ ├── container │ │ ├── requirements.txt │ │ ├── Dockerfile │ │ └── server.py │ ├── package.json │ └── serverless.yml ├── container │ ├── my-container │ │ ├── requirements.txt │ │ ├── Dockerfile │ │ └── server.py │ ├── package.json │ └── serverless.yml ├── php │ ├── composer.json │ ├── package.json │ ├── serverless.yml │ └── handler.php ├── README.md ├── rust │ ├── Cargo.toml │ ├── README.md │ ├── package.json │ ├── src │ │ └── handler.rs │ └── serverless.yml ├── typescript │ ├── serverless.yml │ ├── package.json │ ├── handler.ts │ ├── tsconfig.json │ └── README.md ├── secrets │ ├── package.json │ ├── handler.py │ └── serverless.yml ├── multiple │ ├── package.json │ ├── handler.py │ ├── handler.js │ └── serverless.yml ├── python3 │ ├── package.json │ ├── serverless.yml │ └── handler.py ├── nodejs │ ├── package.json │ ├── handler.js │ └── serverless.yml ├── nodejs-schedule │ ├── package.json │ ├── serverless.yml │ └── handler.js └── nodejs-es-modules │ ├── package.json │ ├── handler.js │ └── serverless.yml ├── .npmignore ├── .gitignore ├── tests ├── teardown.js ├── setup-tests.js ├── shared │ ├── child-process.tests.js │ ├── validate.tests.js │ └── secrets.test.js ├── utils │ ├── fs │ │ └── index.js │ ├── clean-up.js │ └── misc │ │ └── index.js ├── domains │ └── domains.test.js ├── multi-region │ └── multi_region.test.js ├── runtimes │ └── runtimes.test.js ├── provider │ └── scalewayProvider.test.js ├── deploy │ └── buildAndPushContainers.test.js ├── triggers │ └── triggers.test.js └── containers │ ├── containers_private_registry.test.js │ └── containers.test.js ├── shared ├── runtimes.js ├── api │ ├── runtimes.js │ ├── logs.js │ ├── account.js │ ├── endpoint.js │ ├── registry.js │ ├── jwt.js │ ├── index.js │ ├── domain.js │ ├── utils.js │ ├── triggers.js │ ├── namespaces.js │ ├── containers.js │ └── functions.js ├── write-service-outputs.js ├── constants.js ├── setUpDeployment.js ├── child-process.js ├── singleSource.js ├── domains.js └── secrets.js ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.md │ └── bug_report.md ├── dependabot.yml ├── PULL_REQUEST_TEMPLATE.md ├── workflows │ ├── publish.yml │ └── test.yml ├── CODE_OF_CONDUCT.md └── CONTRIBUTING.md ├── docs ├── rust.md ├── php.md ├── troubleshooting.md ├── python.md ├── golang.md ├── secrets.md ├── custom-domains.md ├── containers.md ├── events.md ├── javascript.md └── development.md ├── eslint.config.mjs ├── logs ├── scalewayLogs.js └── lib │ └── getLogs.js ├── remove ├── lib │ └── removeNamespace.js └── scalewayRemove.js ├── index.js ├── LICENSE ├── jwt ├── scalewayJwt.js └── lib │ └── getJwt.js ├── info ├── scalewayInfo.js └── lib │ └── display.js ├── deploy ├── lib │ ├── deployContainers.js │ ├── deployFunctions.js │ ├── uploadCode.js │ ├── createNamespace.js │ ├── deployTriggers.js │ ├── buildAndPushContainers.js │ └── createContainers.js └── scalewayDeploy.js ├── package.json ├── invoke └── scalewayInvoke.js ├── provider └── scalewayProvider.js └── CHANGELOG.md /examples/go/mypackage/go.sum: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | examples 2 | .github 3 | tests -------------------------------------------------------------------------------- /examples/container-schedule/container/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask 2 | -------------------------------------------------------------------------------- /examples/container/my-container/requirements.txt: -------------------------------------------------------------------------------- 1 | flask~=3.1.0 2 | -------------------------------------------------------------------------------- /examples/go/mypackage/go.mod: -------------------------------------------------------------------------------- 1 | module mypackage 2 | 3 | go 1.18 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | node_modules 3 | .serverless 4 | **/rust*/target/ 5 | .eslintcache 6 | -------------------------------------------------------------------------------- /examples/php/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "ramsey/uuid": "^4.7" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /tests/teardown.js: -------------------------------------------------------------------------------- 1 | const { cleanup } = require("./utils/clean-up"); 2 | 3 | cleanup().catch(); 4 | -------------------------------------------------------------------------------- /tests/setup-tests.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { jest: requiredJest } = require("@jest/globals"); 4 | 5 | requiredJest.setTimeout(5000000); 6 | -------------------------------------------------------------------------------- /examples/go/go.mod: -------------------------------------------------------------------------------- 1 | module myhandler 2 | 3 | go 1.18 4 | 5 | require github.com/scaleway/serverless-functions-go v0.1.0 6 | 7 | require github.com/google/uuid v1.3.0 // indirect 8 | -------------------------------------------------------------------------------- /examples/container/my-container/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3-alpine 2 | WORKDIR /usr/src/app 3 | 4 | COPY requirements.txt . 5 | RUN pip install -qr requirements.txt 6 | COPY server.py . 7 | 8 | CMD ["python3", "./server.py"] -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | In this folder you can find simple project using different runtimes. 4 | 5 | For advanced examples visit our [Serverless Examples Repository](https://github.com/scaleway/serverless-examples) 6 | -------------------------------------------------------------------------------- /examples/container-schedule/container/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3-alpine 2 | WORKDIR /usr/src/app 3 | 4 | ENV PORT 8080 5 | EXPOSE 8080 6 | 7 | COPY requirements.txt . 8 | RUN pip install -qr requirements.txt 9 | COPY server.py . 10 | 11 | CMD ["python3", "./server.py"] -------------------------------------------------------------------------------- /examples/go/cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | scw "myhandler" 5 | 6 | "github.com/scaleway/serverless-functions-go/local" 7 | ) 8 | 9 | func main() { 10 | // Replace "Handle" with your function handler name if necessary 11 | local.ServeHandler(scw.Handle, local.WithPort(8080)) 12 | } 13 | -------------------------------------------------------------------------------- /shared/runtimes.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const RUNTIME_STATUS_AVAILABLE = "available"; 4 | const RUNTIME_STATUS_EOS = "end_of_support"; 5 | const RUNTIME_STATUS_EOL = "end_of_life"; 6 | 7 | module.exports = { 8 | RUNTIME_STATUS_AVAILABLE, 9 | RUNTIME_STATUS_EOS, 10 | RUNTIME_STATUS_EOL, 11 | }; 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Scaleway Community 4 | url: https://slack.scaleway.com 5 | about: GitHub issues in this repository are only intended for bug reports and feature requests. Other issues will be closed. Please ask questions on the Scaleway Community Slack (#opensource) 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | updates: 4 | - package-ecosystem: github-actions 5 | # Workflow files stored in the default location of `.github/workflows` 6 | directory: / 7 | schedule: 8 | interval: weekly 9 | - package-ecosystem: npm 10 | directory: / 11 | schedule: 12 | interval: weekly 13 | -------------------------------------------------------------------------------- /shared/api/runtimes.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { manageError } = require("./utils"); 4 | 5 | module.exports = { 6 | listRuntimes() { 7 | const functionsUrl = `runtimes`; 8 | return this.apiManager 9 | .get(functionsUrl) 10 | .then((response) => response.data.runtimes || []) 11 | .catch(manageError); 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Summary 2 | 3 | **_What's changed?_** 4 | 5 | **_Why do we need this?_** 6 | 7 | **_How have you tested it?_** 8 | 9 | ## Checklist 10 | 11 | - [ ] I have reviewed this myself 12 | - [ ] There is a unit test covering every change in this PR 13 | - [ ] I have updated the relevant documentation 14 | 15 | ## Details 16 | -------------------------------------------------------------------------------- /examples/rust/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "handler" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [lib] 9 | path = "src/handler.rs" 10 | 11 | [dependencies] 12 | axum = "0.7.5" 13 | http = "1.1.0" 14 | 15 | [build-dependencies] 16 | rustc_version = "0.4.0" 17 | -------------------------------------------------------------------------------- /examples/rust/README.md: -------------------------------------------------------------------------------- 1 | # Rust runtime 2 | 3 | ## Requirements 4 | 5 | Suggested code layout: 6 | 7 | ``` 8 | . 9 | ├── Cargo.toml 10 | ├── Cargo.lock 11 | └── src/handler.rs 12 | ``` 13 | 14 | ## Handler name 15 | 16 | The `handler name` is the name of your handler function (example: `Handle`). 17 | 18 | ## Handler definition 19 | 20 | Rust handler must be async to work. 21 | -------------------------------------------------------------------------------- /examples/typescript/serverless.yml: -------------------------------------------------------------------------------- 1 | service: typescript-hello-world 2 | configValidationMode: off 3 | provider: 4 | name: scaleway 5 | runtime: node20 6 | 7 | plugins: 8 | - serverless-scaleway-functions 9 | 10 | package: 11 | patterns: 12 | - "!node_modules/**" 13 | - "!.gitignore" 14 | - "!.git/**" 15 | 16 | functions: 17 | first: 18 | handler: handler.handle 19 | -------------------------------------------------------------------------------- /examples/go/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "golang-scaleway-starter", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "test": "echo \"Error: no test specified\" && exit 1" 6 | }, 7 | "keywords": [], 8 | "author": "", 9 | "license": "ISC", 10 | "dependencies": {}, 11 | "devDependencies": { 12 | "serverless-scaleway-functions": ">=0.4.14" 13 | }, 14 | "description": "" 15 | } 16 | -------------------------------------------------------------------------------- /examples/rust/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rust-scaleway-starter", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "test": "echo \"Error: no test specified\" && exit 1" 6 | }, 7 | "keywords": [], 8 | "author": "", 9 | "license": "ISC", 10 | "dependencies": {}, 11 | "devDependencies": { 12 | "serverless-scaleway-functions": ">=0.4.14" 13 | }, 14 | "description": "" 15 | } 16 | -------------------------------------------------------------------------------- /examples/rust/src/handler.rs: -------------------------------------------------------------------------------- 1 | use axum::{ 2 | body::Body, extract::Request, response::Response, 3 | }; 4 | use http::StatusCode; 5 | 6 | pub async fn my_handler(_req: Request) -> Response { 7 | Response::builder() 8 | .status(StatusCode::OK) 9 | .header("Content-Type", "text/plain") 10 | .body(Body::from("Ferris says hello!")) 11 | .unwrap() 12 | } 13 | -------------------------------------------------------------------------------- /examples/secrets/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "secrets", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "keywords": [], 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": {}, 12 | "devDependencies": { 13 | "serverless-scaleway-functions": ">=0.4.14" 14 | }, 15 | "description": "" 16 | } 17 | -------------------------------------------------------------------------------- /examples/php/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "php-scaleway-starter", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "keywords": [], 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": {}, 12 | "devDependencies": { 13 | "serverless-scaleway-functions": ">=0.4.14" 14 | }, 15 | "description": "" 16 | } 17 | -------------------------------------------------------------------------------- /examples/multiple/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "multiple-scaleway-starter", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "keywords": [], 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": {}, 12 | "devDependencies": { 13 | "serverless-scaleway-functions": ">=0.4.14" 14 | }, 15 | "description": "" 16 | } 17 | -------------------------------------------------------------------------------- /examples/python3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "python-scaleway-starter", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "keywords": [], 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": {}, 12 | "devDependencies": { 13 | "serverless-scaleway-functions": ">=0.4.14" 14 | }, 15 | "description": "" 16 | } 17 | -------------------------------------------------------------------------------- /docs/rust.md: -------------------------------------------------------------------------------- 1 | # Rust 2 | 3 | The recommended folder structure for Rust functions is: 4 | 5 | ```yml 6 | - src 7 | - handler.rs 8 | - serverless.yml 9 | ``` 10 | 11 | Your serverless.yml `functions` should look something like this: 12 | 13 | ```yml 14 | provider: 15 | runtime: rust179 16 | functions: 17 | main: 18 | handler: "handler" 19 | ``` 20 | 21 | You can find more Rust examples in the [examples folder](../examples). 22 | -------------------------------------------------------------------------------- /examples/container/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "container-scaleway-starter", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "keywords": [], 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": {}, 12 | "devDependencies": { 13 | "serverless-scaleway-functions": ">=0.4.14" 14 | }, 15 | "description": "" 16 | } 17 | -------------------------------------------------------------------------------- /examples/multiple/handler.py: -------------------------------------------------------------------------------- 1 | def handle(event, context): 2 | """handle a request to the function 3 | Args: 4 | event (dict): request params 5 | context (dict): function call metadata 6 | """ 7 | 8 | return { 9 | "body": "Hello From Python3 runtime on Serverless Framework and Scaleway Functions", 10 | "headers": { 11 | "Content-Type": ["text/plain"], 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/container-schedule/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "container-schedule-scaleway-starter", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "keywords": [], 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": {}, 12 | "devDependencies": { 13 | "serverless-scaleway-functions": ">=0.4.14" 14 | }, 15 | "description": "" 16 | } 17 | -------------------------------------------------------------------------------- /examples/nodejs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-scaleway-starter", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "keywords": [], 9 | "author": "", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "@scaleway/serverless-functions": "^1.0.2", 13 | "serverless-scaleway-functions": ">=0.4.14" 14 | }, 15 | "description": "" 16 | } 17 | -------------------------------------------------------------------------------- /shared/write-service-outputs.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { writeText, style } = require("@serverless/utils/log"); 4 | 5 | module.exports = (serviceOutputs) => { 6 | for (const [section, entries] of serviceOutputs) { 7 | if (typeof entries === "string") { 8 | writeText(`${style.aside(`${section}:`)} ${entries}`); 9 | } else { 10 | writeText(`${style.aside(`${section}:\n`)} ${entries.join("\n ")}`); 11 | } 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /examples/multiple/handler.js: -------------------------------------------------------------------------------- 1 | module.exports.handle = (event, context, callback) => { 2 | const result = { 3 | message: "Hello from Serverless Framework and Scaleway Functions :D", 4 | }; 5 | 6 | const response = { 7 | statusCode: 200, 8 | headers: { "Content-Type": ["application/json"] }, 9 | body: JSON.stringify(result), 10 | }; 11 | 12 | // either return cb(undefined, response) or return response 13 | return response; 14 | }; 15 | -------------------------------------------------------------------------------- /shared/api/logs.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { manageError } = require("./utils"); 4 | 5 | module.exports = { 6 | getLines(application) { 7 | let logsUrl = `functions/${application.id}/logs`; 8 | if (!application.runtime) { 9 | logsUrl = `containers/${application.id}/logs`; 10 | } 11 | return this.apiManager 12 | .get(logsUrl) 13 | .then((response) => response.data.logs || []) 14 | .catch(manageError); 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /examples/nodejs-schedule/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-schedule-scaleway-starter", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "keywords": [], 9 | "author": "", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "@scaleway/serverless-functions": "^1.0.2", 13 | "serverless-scaleway-functions": ">=0.4.14" 14 | }, 15 | "description": "" 16 | } 17 | -------------------------------------------------------------------------------- /examples/nodejs-es-modules/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-scaleway-starter", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "type": "module", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "@scaleway/serverless-functions": "^1.0.2", 14 | "serverless-scaleway-functions": ">=0.4.14" 15 | }, 16 | "description": "" 17 | } 18 | -------------------------------------------------------------------------------- /docs/php.md: -------------------------------------------------------------------------------- 1 | # PHP 2 | 3 | The Recommended folder structure for `php` functions is as follows: 4 | 5 | ```yml 6 | ├── handler.php 7 | ├── composer.json (not necessary if you do not need dependencies) 8 | └── serverless.yml 9 | ``` 10 | 11 | Your `serverless.yml` can then look something like this: 12 | 13 | ```yml 14 | provider: 15 | runtime: php82 16 | functions: 17 | main: 18 | handler: "handler" 19 | ``` 20 | 21 | You can find more PHP examples in the [examples folder](../examples). 22 | -------------------------------------------------------------------------------- /docs/troubleshooting.md: -------------------------------------------------------------------------------- 1 | # Troubleshooting 2 | 3 | ## Rate Limiting Issue 4 | 5 | If you are experiencing rate limiting issues (error 429) in your application, consider engaging with the support and/or the community. 6 | 7 | When seeking assistance, remember to provide relevant details, such as the specific rate limiting error messages, the affected components, and any relevant configuration information. This will enable us to better understand your situation and provide appropriate guidance or solutions. 8 | -------------------------------------------------------------------------------- /examples/nodejs-es-modules/handler.js: -------------------------------------------------------------------------------- 1 | export { handle }; 2 | 3 | function handle(event, context, cb) { 4 | return { 5 | body: process.version, 6 | headers: { "Content-Type": ["text/plain"] }, 7 | statusCode: 200, 8 | }; 9 | } 10 | 11 | /* This is used to test locally and will not be executed on Scaleway Functions */ 12 | if (process.env.NODE_ENV === "test") { 13 | import("@scaleway/serverless-functions").then((scw_fnc_node) => { 14 | scw_fnc_node.serveHandler(handle, 8080); 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /examples/nodejs-schedule/serverless.yml: -------------------------------------------------------------------------------- 1 | service: node-event-example 2 | configValidationMode: off 3 | provider: 4 | name: scaleway 5 | runtime: node20 6 | 7 | plugins: 8 | - serverless-scaleway-functions 9 | 10 | package: 11 | patterns: 12 | - "!.gitignore" 13 | - "!.git/**" 14 | 15 | functions: 16 | first: 17 | handler: handler.handle 18 | events: 19 | - schedule: 20 | rate: "1 * * * *" 21 | input: 22 | foo: "some-string" 23 | bar: 1234 24 | -------------------------------------------------------------------------------- /examples/typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript-with-node", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "@types/node": "^18.15.11", 14 | "@scaleway/serverless-functions": ">=1.0.2", 15 | "serverless-scaleway-functions": ">=0.4.14", 16 | "typescript": "^5.0.2" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/container-schedule/serverless.yml: -------------------------------------------------------------------------------- 1 | service: container-event-example 2 | configValidationMode: off 3 | 4 | provider: 5 | name: scaleway 6 | 7 | plugins: 8 | - serverless-scaleway-functions 9 | 10 | package: 11 | patterns: 12 | - "!node_modules/**" 13 | - "!.gitignore" 14 | - "!.git/**" 15 | 16 | custom: 17 | containers: 18 | first: 19 | directory: container 20 | events: 21 | - schedule: 22 | rate: "1 * * * *" 23 | input: 24 | field-a: "some value" 25 | field-b: 1234 26 | -------------------------------------------------------------------------------- /examples/php/serverless.yml: -------------------------------------------------------------------------------- 1 | service: scaleway-php 2 | configValidationMode: off 3 | singleSource: false 4 | provider: 5 | name: scaleway 6 | runtime: php82 7 | # Global Environment variables - used in every functions 8 | env: 9 | test: test 10 | 11 | plugins: 12 | - serverless-scaleway-functions 13 | 14 | package: 15 | patterns: 16 | - "!.gitignore" 17 | - "!.git/**" 18 | 19 | functions: 20 | first: 21 | handler: handler.handle 22 | # description: "" 23 | # Local environment variables - used only in given function 24 | env: 25 | local: local 26 | -------------------------------------------------------------------------------- /examples/secrets/handler.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | 5 | def handle(event, context): 6 | """handle a request to the function 7 | Args: 8 | event (dict): request params 9 | context (dict): function call metadata 10 | """ 11 | 12 | # print all environment variables beginning with "env" 13 | return { 14 | "env_vars": sorted( 15 | list( 16 | filter( 17 | lambda x: x.startswith("env"), 18 | dict(os.environ).keys() 19 | ) 20 | ) 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /examples/php/handler.php: -------------------------------------------------------------------------------- 1 | toString(), 20 | $uuid->getFields()->getVersion() 21 | ); 22 | 23 | return [ 24 | "body" => phpversion(), 25 | "statusCode" => 200, 26 | "headers" => $headers, 27 | ]; 28 | } 29 | -------------------------------------------------------------------------------- /shared/constants.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const FUNCTIONS_API_URL = "https://api.scaleway.com/functions/v1beta1/regions"; 4 | const CONTAINERS_API_URL = 5 | "https://api.scaleway.com/containers/v1beta1/regions"; 6 | const REGISTRY_API_URL = "https://api.scaleway.com/registry/v1/regions"; 7 | const ACCOUNT_API_URL = "https://api.scaleway.com/account/v3/projects"; 8 | const DEFAULT_REGION = "fr-par"; 9 | 10 | const PRIVACY_PRIVATE = "private"; 11 | 12 | module.exports = { 13 | FUNCTIONS_API_URL, 14 | CONTAINERS_API_URL, 15 | REGISTRY_API_URL, 16 | ACCOUNT_API_URL, 17 | PRIVACY_PRIVATE, 18 | DEFAULT_REGION, 19 | }; 20 | -------------------------------------------------------------------------------- /examples/nodejs-es-modules/serverless.yml: -------------------------------------------------------------------------------- 1 | service: scaleway-esmodule-nodeXX 2 | configValidationMode: off 3 | provider: 4 | name: scaleway 5 | runtime: node22 # Available node runtimes are listed in documentation 6 | # Global Environment variables - used in every functions 7 | env: 8 | test: test 9 | 10 | plugins: 11 | - serverless-scaleway-functions 12 | 13 | package: 14 | patterns: 15 | - "!.gitignore" 16 | - "!.git/**" 17 | 18 | functions: 19 | first: 20 | handler: handler.handle 21 | # description: "" 22 | # Local environment variables - used only in given function 23 | env: 24 | local: local 25 | -------------------------------------------------------------------------------- /examples/python3/serverless.yml: -------------------------------------------------------------------------------- 1 | service: scaleway-python3 2 | configValidationMode: off 3 | provider: 4 | name: scaleway 5 | runtime: python310 # Available python runtimes are listed in documentation 6 | # Global Environment variables - used in every functions 7 | env: 8 | test: test 9 | 10 | plugins: 11 | - serverless-scaleway-functions 12 | 13 | package: 14 | patterns: 15 | - "!node_modules/**" 16 | - "!.gitignore" 17 | - "!.git/**" 18 | 19 | functions: 20 | first: 21 | handler: handler.handle 22 | # description: "" 23 | # Local environment variables - used only in given function 24 | env: 25 | local: local 26 | -------------------------------------------------------------------------------- /shared/setUpDeployment.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | setUpDeployment() { 3 | const { service } = this.provider.serverless; 4 | const { provider } = service; 5 | this.namespaceName = service.service; 6 | this.namespaceVariables = provider.env || {}; 7 | this.namespaceSecretVariables = provider.secret || {}; 8 | this.runtime = provider.runtime; 9 | 10 | const defaultTokenExpirationDate = new Date(); 11 | defaultTokenExpirationDate.setFullYear( 12 | defaultTokenExpirationDate.getFullYear() + 1 13 | ); 14 | this.tokenExpirationDate = 15 | provider.tokenExpiration || defaultTokenExpirationDate.toISOString(); 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /examples/typescript/handler.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Callback, 3 | Context, 4 | Event, 5 | } from "@scaleway/serverless-functions/framework/dist/types/types"; 6 | 7 | export { handle }; 8 | 9 | function handle(event: Event, context: Context, cb: Callback) { 10 | return { 11 | body: "Hello world!", 12 | headers: { "Content-Type": ["application/json"] }, 13 | statusCode: 200, 14 | }; 15 | } 16 | 17 | /* This is used to test locally and will not be executed on Scaleway Functions */ 18 | if (process.env.NODE_ENV === "test") { 19 | import("@scaleway/serverless-functions").then((scw_fnc_node) => { 20 | scw_fnc_node.serveHandler(handle, 8080); 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "eslint/config"; 2 | import js from "@eslint/js"; 3 | 4 | export default defineConfig([ 5 | { 6 | languageOptions: { 7 | globals: { 8 | process: "readonly", 9 | __dirname: "readonly", 10 | module: "readonly", 11 | require: "readonly", 12 | console: "readonly", 13 | jest: "readonly", 14 | setTimeout: "readonly", 15 | }, 16 | }, 17 | }, 18 | { 19 | files: ["**/*.js"], 20 | plugins: { js }, 21 | extends: ["js/recommended"], 22 | ignores: ["examples/**/*.js"], 23 | }, 24 | { files: ["tests/**/*.test.js"], rules: { "no-unused-vars": "off" } }, 25 | ]); 26 | -------------------------------------------------------------------------------- /examples/typescript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 4 | "module": "commonjs" /* Specify what module code is generated. */, 5 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, 6 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 7 | "strict": true /* Enable all strict type-checking options. */, 8 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /docs/python.md: -------------------------------------------------------------------------------- 1 | # Python 2 | 3 | The `handler` for a Python function should be the path to the file, followed by the function to call. For example with a file structure like: 4 | 5 | ```yml 6 | - src 7 | - handlers 8 | - firstHandler.py => def my_first_handler 9 | - secondHandler.py => def my_second_handler 10 | - serverless.yml 11 | ``` 12 | 13 | Your `serverless.yml` would look like: 14 | 15 | ```yml 16 | provider: 17 | runtime: python310 18 | functions: 19 | first: 20 | handler: src/handlers/firstHandler.my_first_handler 21 | second: 22 | handler: src/handlers/secondHandler.my_second_handler 23 | ``` 24 | 25 | You can find more Python examples in the [examples folder](../examples). 26 | -------------------------------------------------------------------------------- /shared/api/account.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { manageError } = require("./utils"); 4 | 5 | module.exports = { 6 | listProjects(organizationId) { 7 | return this.apiManager 8 | .get( 9 | `?organization_id=${organizationId}&page_size=50&order_by=created_at_desc` 10 | ) 11 | .then((response) => response.data.projects) 12 | .catch(manageError); 13 | }, 14 | 15 | deleteProject(projectId) { 16 | return this.apiManager.delete(`${projectId}`).catch(manageError); 17 | }, 18 | 19 | createProject(params) { 20 | return this.apiManager 21 | .post("", params) 22 | .then((response) => response.data) 23 | .catch(manageError); 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /examples/go/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 3 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 4 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 5 | github.com/scaleway/serverless-functions-go v0.1.0 h1:U967AmTujugxzzvLIWUNoB+/5hLmsys0Xe7n4L38XPA= 6 | github.com/scaleway/serverless-functions-go v0.1.0/go.mod h1:SKb5XA5bONwJkecQElrKLYOVrg/5kmcaduB440HQbIg= 7 | github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= 8 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 9 | -------------------------------------------------------------------------------- /examples/rust/serverless.yml: -------------------------------------------------------------------------------- 1 | service: scaleway-rust 2 | configValidationMode: off 3 | provider: 4 | name: scaleway 5 | runtime: rust179 # Available rust runtimes are listed in documentation 6 | # Global Environment variables - used in every functions 7 | env: 8 | test: test 9 | 10 | plugins: 11 | - serverless-scaleway-functions 12 | 13 | package: 14 | patterns: 15 | - "!node_modules/**" 16 | - "!.gitignore" 17 | - "!.git/**" 18 | 19 | functions: 20 | first: 21 | # handler is just the name of the exported handler function 22 | handler: my_handler 23 | # description: "" 24 | # Local environment variables - used only in given function 25 | env: 26 | local: local 27 | -------------------------------------------------------------------------------- /docs/golang.md: -------------------------------------------------------------------------------- 1 | # Golang 2 | 3 | For Go functions, the `handler` parameter must be the path to your handler's **package**. For example, if you have the following structure: 4 | 5 | ```yml 6 | - src 7 | - testing 8 | - handler.go -> package main in src/testing subdirectory 9 | - second 10 | - handler.go -> package main in src/second subdirectory 11 | - serverless.yml 12 | - handler.go -> package main at the root of project 13 | ``` 14 | 15 | Your serverless.yml `functions` should look something like this: 16 | 17 | ```yml 18 | provider: 19 | # ... 20 | runtime: go122 21 | functions: 22 | main: 23 | handler: "." 24 | testing: 25 | handler: src/testing 26 | second: 27 | handler: src/second 28 | ``` 29 | -------------------------------------------------------------------------------- /examples/nodejs/handler.js: -------------------------------------------------------------------------------- 1 | module.exports.handle = (event, context, callback) => { 2 | const result = { 3 | message: "Hello from Serverless Framework and Scaleway Functions :D", 4 | }; 5 | const response = { 6 | statusCode: 200, 7 | headers: { "Content-Type": ["application/json"] }, 8 | body: JSON.stringify(result), 9 | }; 10 | 11 | // either return cb(undefined, response) or return response 12 | return response; 13 | }; 14 | 15 | /* This is used to test locally and will not be executed on Scaleway Functions */ 16 | if (process.env.NODE_ENV === "test") { 17 | import("@scaleway/serverless-functions").then((scw_fnc_node) => { 18 | scw_fnc_node.serveHandler(exports.handle, 8080); 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /examples/nodejs/serverless.yml: -------------------------------------------------------------------------------- 1 | service: scaleway-nodeXX 2 | configValidationMode: off 3 | singleSource: false 4 | provider: 5 | name: scaleway 6 | runtime: node22 # Available node runtimes are listed in documentation 7 | # Global Environment variables - used in every function 8 | env: 9 | test: test 10 | 11 | plugins: 12 | - serverless-scaleway-functions 13 | 14 | package: 15 | patterns: 16 | - "!.gitignore" 17 | - "!.git/**" 18 | 19 | functions: 20 | first: 21 | handler: handler.handle 22 | httpOption: redirected 23 | sandbox: v2 24 | memoryLimit: 1024 25 | # description: "" 26 | # Local environment variables - used only in given function 27 | env: 28 | local: local 29 | -------------------------------------------------------------------------------- /examples/go/handler.go: -------------------------------------------------------------------------------- 1 | package myfunc 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | ) 7 | 8 | // Handle - Handle event 9 | func Handle(w http.ResponseWriter, r *http.Request) { 10 | response := map[string]interface{}{ 11 | "message": "We're all good", 12 | "healthy": true, 13 | "number": 4, 14 | } 15 | 16 | responseBytes, err := json.Marshal(response) 17 | if err != nil { 18 | w.WriteHeader(http.StatusInternalServerError) 19 | return 20 | } 21 | 22 | // Set the header explicitly depending the returned data 23 | w.Header().Set("Content-Type", "application/json") 24 | 25 | // Customise status code. 26 | w.WriteHeader(http.StatusOK) 27 | 28 | // Add content to the response 29 | _, _ = w.Write(responseBytes) 30 | } 31 | -------------------------------------------------------------------------------- /tests/shared/child-process.tests.js: -------------------------------------------------------------------------------- 1 | const { execSync, execCaptureOutput } = require("../../shared/child-process"); 2 | const { describe, it, expect } = require("@jest/globals"); 3 | 4 | describe("Synchronous command execution test", () => { 5 | it("should execute a command synchronously", () => { 6 | execSync("ls"); 7 | }); 8 | 9 | it("should throw an error for an invalid command", () => { 10 | expect(() => { 11 | execSync("blah"); 12 | }).toThrow(); 13 | }); 14 | }); 15 | 16 | describe("Synchronous output capture of command test", () => { 17 | it("should capture the output of a command", () => { 18 | let output = execCaptureOutput("echo", ["foo bar"]); 19 | expect(output).toEqual("foo bar\n"); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /examples/container/my-container/server.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, jsonify 2 | import os 3 | 4 | DEFAULT_PORT = "8080" 5 | MESSAGE = "Hello, World from Scaleway Container !" 6 | 7 | app = Flask(__name__) 8 | 9 | @app.route("/") 10 | def root(): 11 | return jsonify({ 12 | "message": MESSAGE 13 | }) 14 | 15 | @app.route("/health") 16 | def health(): 17 | # You could add more complex logic here, for example checking the health of a database... 18 | return jsonify({ 19 | "status": "UP" 20 | }) 21 | 22 | if __name__ == "__main__": 23 | # Scaleway's system will inject a PORT environment variable on which your application should start the server. 24 | port = os.getenv("PORT", DEFAULT_PORT) 25 | app.run(host="0.0.0.0", port=int(port)) 26 | -------------------------------------------------------------------------------- /examples/python3/handler.py: -------------------------------------------------------------------------------- 1 | def handle(event, context): 2 | """handle a request to the function 3 | Args: 4 | event (dict): request params 5 | context (dict): function call metadata 6 | """ 7 | 8 | return { 9 | "body": "Hello From Python3 runtime on Serverless Framework and Scaleway Functions", 10 | "headers": { 11 | "Content-Type": ["text/plain"], 12 | } 13 | } 14 | 15 | # run 'pip install scaleway_functions_python' if necessary 16 | if __name__ == "__main__": 17 | # The import is conditional so that you do not need 18 | # to package the library when deploying on Scaleway Functions. 19 | from scaleway_functions_python import local 20 | local.serve_handler(handle, port=8080) -------------------------------------------------------------------------------- /examples/go/mypackage/handler.go: -------------------------------------------------------------------------------- 1 | package myfunc 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | ) 7 | 8 | // Handle - Handle event 9 | func Handle(w http.ResponseWriter, r *http.Request) { 10 | response := map[string]interface{}{ 11 | "message": "We're all good", 12 | "healthy": true, 13 | "number": 4, 14 | } 15 | 16 | responseBytes, err := json.Marshal(response) 17 | if err != nil { 18 | w.WriteHeader(http.StatusInternalServerError) 19 | return 20 | } 21 | 22 | // Set the header explicitly depending the returned data 23 | w.Header().Set("Content-Type", "application/json") 24 | 25 | // Customise status code. 26 | w.WriteHeader(http.StatusOK) 27 | 28 | // Add content to the response 29 | _, _ = w.Write(responseBytes) 30 | } 31 | -------------------------------------------------------------------------------- /examples/container-schedule/container/server.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from flask import request 3 | 4 | import os 5 | import json 6 | 7 | DEFAULT_PORT = "8080" 8 | MESSAGE = "Hello from the Python event example" 9 | 10 | app = Flask(__name__) 11 | 12 | 13 | @app.route("/", methods=["GET", "POST"]) 14 | def root(): 15 | app.logger.info(f"Event data: {request.json}") 16 | 17 | if request.json: 18 | field_a = request.json.get("field-a") 19 | field_b = request.json.get("field-b") 20 | app.logger.info(f"field-a = {field_a}") 21 | app.logger.info(f"field-b = {field_b}") 22 | 23 | return json.dumps({"message": MESSAGE}) 24 | 25 | 26 | if __name__ == "__main__": 27 | port_env = os.getenv("PORT", DEFAULT_PORT) 28 | port = int(port_env) 29 | 30 | app.run(debug=True, host="0.0.0.0", port=port) 31 | -------------------------------------------------------------------------------- /examples/multiple/serverless.yml: -------------------------------------------------------------------------------- 1 | service: scaleway-multiple 2 | configValidationMode: off 3 | provider: 4 | name: scaleway 5 | runtime: node22 6 | # Global Environment variables - used in every functions 7 | env: 8 | test: test 9 | 10 | plugins: 11 | - serverless-scaleway-functions 12 | 13 | package: 14 | patterns: 15 | - "!node_modules/**" 16 | - "!.gitignore" 17 | - "!.git/**" 18 | 19 | functions: 20 | nodefunc: 21 | handler: handler.handle 22 | # description: "" 23 | # Local environment variables - used only in given function 24 | env: 25 | local: local 26 | pythonfunc: 27 | runtime: python311 # Here we add a specific runtime for the function 28 | handler: handler.handle 29 | # description: "" 30 | # Local environment variables - used only in given function 31 | env: 32 | local: local 33 | -------------------------------------------------------------------------------- /logs/scalewayLogs.js: -------------------------------------------------------------------------------- 1 | const BbPromise = require("bluebird"); 2 | const setUpDeployment = require("../shared/setUpDeployment"); 3 | const getLogs = require("./lib/getLogs"); 4 | const scalewayApi = require("../shared/api/endpoint"); 5 | 6 | class ScalewayLogs { 7 | constructor(serverless, options) { 8 | this.serverless = serverless; 9 | this.options = options || {}; 10 | this.provider = this.serverless.getProvider("scaleway"); 11 | this.provider.initialize(this.serverless, this.options); 12 | 13 | const api = scalewayApi.getApi(this); 14 | 15 | Object.assign(this, setUpDeployment, getLogs, api); 16 | this.hooks = { 17 | "before:logs:logs": () => BbPromise.bind(this).then(this.setUpDeployment), 18 | "logs:logs": () => BbPromise.bind(this).then(this.getLogs), 19 | }; 20 | } 21 | } 22 | 23 | module.exports = ScalewayLogs; 24 | -------------------------------------------------------------------------------- /shared/child-process.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const child_process = require("child_process"); 4 | 5 | function execSync(command, options = null) { 6 | // Same as native but outputs std in case of error 7 | try { 8 | return child_process.execSync(command, options); 9 | } catch (error) { 10 | if (error.stdout) process.stdout.write(error.stdout); 11 | if (error.stderr) process.stderr.write(error.stderr); 12 | throw error; 13 | } 14 | } 15 | 16 | function execCaptureOutput(command, args) { 17 | let child = child_process.spawnSync(command, args, { encoding: "utf8" }); 18 | 19 | if (child.error) { 20 | if (child.stdout) process.stdout.write(child.stdout); 21 | if (child.stderr) process.stderr.write(child.stderr); 22 | throw child.error; 23 | } 24 | 25 | return child.stdout; 26 | } 27 | 28 | module.exports = { execSync, execCaptureOutput }; 29 | -------------------------------------------------------------------------------- /examples/nodejs-schedule/handler.js: -------------------------------------------------------------------------------- 1 | module.exports.handle = (event, context, callback) => { 2 | // The scheduled event data is held in a JSON string in the body field 3 | var eventBody = JSON.parse(event.body); 4 | 5 | // Log the event data 6 | console.log("foo = " + eventBody.foo); 7 | console.log("bar = " + eventBody.bar); 8 | 9 | // Return a success response 10 | const response = { 11 | statusCode: 200, 12 | headers: { "Content-Type": ["application/json"] }, 13 | body: JSON.stringify({ message: "Hello from scaleway functions" }), 14 | }; 15 | 16 | return response; 17 | }; 18 | 19 | /* This is used to test locally and will not be executed on Scaleway Functions */ 20 | if (process.env.NODE_ENV === "test") { 21 | import("@scaleway/serverless-functions").then((scw_fnc_node) => { 22 | scw_fnc_node.serveHandler(exports.handle, 8080); 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "publish" 3 | 4 | on: 5 | release: 6 | types: [published] 7 | 8 | permissions: 9 | id-token: write # Required for OIDC 10 | contents: read 11 | 12 | jobs: 13 | release: 14 | runs-on: ubuntu-24.04 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - name: "Set up Node" 20 | uses: actions/setup-node@v4 21 | with: 22 | node-version: 20.x 23 | registry-url: "https://registry.npmjs.org" 24 | 25 | - name: Update npm 26 | # We need to make sure we're using npm v11.5+ to use Trusted Publishers 27 | # Reference: https://docs.npmjs.com/trusted-publishers 28 | run: npm install -g npm@11 29 | 30 | - name: "Install dependencies" 31 | run: npm ci 32 | 33 | - name: "Publish package on NPM" 34 | run: npm publish --access public 35 | -------------------------------------------------------------------------------- /examples/go/serverless.yml: -------------------------------------------------------------------------------- 1 | service: scaleway-golang 2 | configValidationMode: off 3 | provider: 4 | name: scaleway 5 | runtime: go122 # Available go runtimes are listed in documentation 6 | # Global Environment variables - used in every functions 7 | env: 8 | test: test 9 | 10 | plugins: 11 | - serverless-scaleway-functions 12 | 13 | package: 14 | patterns: 15 | - "!node_modules/**" 16 | - "!.gitignore" 17 | - "!.git/**" 18 | 19 | functions: 20 | first: 21 | # handler is just the name of the exported handler function 22 | handler: Handle 23 | # description: "" 24 | # Local environment variables - used only in given function 25 | env: 26 | local: local 27 | 28 | mymodule: 29 | # if your handler is defined at the root of a file on the module `mypackage` 30 | # `mypackage` folder must be a valid Go module (with a go.mod) 31 | handler: "mypackage/Handle" 32 | -------------------------------------------------------------------------------- /shared/api/endpoint.js: -------------------------------------------------------------------------------- 1 | const { FunctionApi } = require("."); 2 | const { ContainerApi } = require("."); 3 | 4 | function getApi(object) { 5 | let api; 6 | if ( 7 | object.provider.serverless.service.custom && 8 | object.provider.serverless.service.custom.containers && 9 | Object.keys(object.provider.serverless.service.custom.containers).length !== 10 | 0 11 | ) { 12 | const credentials = object.provider.getContainerCredentials(); 13 | api = new ContainerApi(credentials.apiUrl, credentials.token); 14 | } 15 | 16 | if ( 17 | object.provider.serverless.service.functions && 18 | Object.keys(object.provider.serverless.service.functions).length !== 0 19 | ) { 20 | const credentials = object.provider.getFunctionCredentials(); 21 | api = new FunctionApi(credentials.apiUrl, credentials.token); 22 | } 23 | return api; 24 | } 25 | 26 | module.exports = { 27 | getApi, 28 | }; 29 | -------------------------------------------------------------------------------- /docs/secrets.md: -------------------------------------------------------------------------------- 1 | # Security and secret management 2 | 3 | We do not recommend hard-coding secrets in your `serverless.yml` file. Instead you can use environment variable substitution in your `serverless.yml`. 4 | 5 | These environment variables can come directly from your deployment environment, or be stored in a `.env` file. A sample configuration looks like: 6 | 7 | ```yml 8 | # Enable use of .env file 9 | useDotenv: true 10 | 11 | provider: 12 | name: scaleway 13 | runtime: node22 14 | 15 | functions: 16 | my-func: 17 | handler: handler.py 18 | 19 | # Template environment variables from .env file and/or environment variables 20 | secret: 21 | MY_SECRET: ${env:SOME_SECRET} 22 | MY_OTHER_SECRET: ${env:SOME_OTHER_SECRET} 23 | ``` 24 | 25 | A `.env` file can be placed alongside your `serverless.yml` file, which looks like: 26 | 27 | ```bash 28 | SOME_SECRET=XXX 29 | SOME_OTHER_SECRET=XXX 30 | ``` 31 | -------------------------------------------------------------------------------- /shared/api/registry.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { getApiManager } = require("./utils"); 4 | const { manageError } = require("./utils"); 5 | 6 | class RegistryApi { 7 | constructor(registryApiUrl, token) { 8 | this.apiManager = getApiManager(registryApiUrl, token); 9 | } 10 | 11 | listRegistryNamespace(projectId) { 12 | return this.apiManager 13 | .get(`namespaces?projectId=${projectId}`) 14 | .then((response) => response.data.namespaces) 15 | .catch(manageError); 16 | } 17 | 18 | deleteRegistryNamespace(namespaceId) { 19 | return this.apiManager 20 | .delete(`namespaces/${namespaceId}`) 21 | .then((response) => response.data) 22 | .catch(manageError); 23 | } 24 | 25 | createRegistryNamespace(params) { 26 | return this.apiManager 27 | .post("namespaces", params) 28 | .then((response) => response.data) 29 | .catch(manageError); 30 | } 31 | } 32 | 33 | module.exports = RegistryApi; 34 | -------------------------------------------------------------------------------- /remove/lib/removeNamespace.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const BbPromise = require("bluebird"); 4 | 5 | module.exports = { 6 | removeNamespace() { 7 | this.serverless.cli.log( 8 | "Removing namespace and associated functions/triggers..." 9 | ); 10 | return BbPromise.bind(this) 11 | .then(() => 12 | this.getNamespaceFromList( 13 | this.namespaceName, 14 | this.provider.getScwProject() 15 | ) 16 | ) 17 | .then(this.removeSingleNamespace); 18 | }, 19 | 20 | removeSingleNamespace(namespace) { 21 | if (!namespace) 22 | throw new Error( 23 | `Unable to remove namespace and functions: No namespace found with name ${this.namespaceName}` 24 | ); 25 | return this.deleteNamespace(namespace.id) 26 | .then(() => this.waitNamespaceIsDeleted(namespace.id)) 27 | .then(() => 28 | this.serverless.cli.log("Namespace has been deleted successfully") 29 | ); 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🚀 Feature request 3 | about: I have a suggestion (and might want to implement it myself 🙂)! 4 | labels: enhancement 5 | --- 6 | 7 | 8 | 9 | ### Community Note 10 | 11 | - Please vote on this issue by adding a 👍 [reaction](https://blog.github.com/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/) to the original issue to help the community and maintainers prioritize this request 12 | - Please do not leave "+1" or other comments that do not add relevant new information or questions, they generate extra noise for issue followers and do not help prioritize the request 13 | - If you are interested in working on this issue or have submitted a pull request, please leave a comment 14 | 15 | 16 | 17 | ### Proposal 18 | 19 | - **What is the proposed change?** 20 | 21 | - **Who does this proposal help, and why?** 22 | 23 | ### Example 24 | -------------------------------------------------------------------------------- /shared/api/jwt.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { manageError } = require("./utils"); 4 | 5 | module.exports = { 6 | issueJwtNamespace(namespaceId, expirationDate) { 7 | const jwtUrl = `issue-jwt?namespace_id=${namespaceId}&expiration_date=${expirationDate}`; 8 | return this.apiManager 9 | .get(jwtUrl) 10 | .then((response) => response.data || {}) 11 | .catch(manageError); 12 | }, 13 | 14 | issueJwtFunction(functionId, expirationDate) { 15 | const jwtUrl = `issue-jwt?function_id=${functionId}&expiration_date=${expirationDate}`; 16 | return this.apiManager 17 | .get(jwtUrl) 18 | .then((response) => response.data || {}) 19 | .catch(manageError); 20 | }, 21 | 22 | issueJwtContainer(containerId, expirationDate) { 23 | const jwtUrl = `issue-jwt?container_id=${containerId}&expiration_date=${expirationDate}`; 24 | return this.apiManager 25 | .get(jwtUrl) 26 | .then((response) => response.data || {}) 27 | .catch(manageError); 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /examples/container/serverless.yml: -------------------------------------------------------------------------------- 1 | service: scaleway-container 2 | configValidationMode: off 3 | provider: 4 | name: scaleway 5 | # Global Environment variables - used in every functions 6 | env: 7 | test: test 8 | 9 | plugins: 10 | - serverless-scaleway-functions 11 | 12 | package: 13 | patterns: 14 | - "!node_modules/**" 15 | - "!.gitignore" 16 | - "!.git/**" 17 | 18 | custom: 19 | containers: 20 | first: 21 | directory: my-container 22 | # registryImage: "" 23 | # port: 8080 24 | # description: "" 25 | # minScale: 1 26 | # memoryLimit: 256 27 | # cpuLimit: 140 28 | # maxScale: 2 29 | # scalingOption: 30 | # type: concurrentRequests 31 | # threshold: 50 32 | # timeout: "20s" 33 | # httpOption: redirected 34 | # Local environment variables - used only in given function 35 | env: 36 | local: local 37 | healthCheck: 38 | httpPath: /health 39 | interval: 10s 40 | failureThreshold: 3 41 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const ScalewayProvider = require("./provider/scalewayProvider"); 4 | const ScalewayDeploy = require("./deploy/scalewayDeploy"); 5 | const ScalewayRemove = require("./remove/scalewayRemove"); 6 | const ScalewayInvoke = require("./invoke/scalewayInvoke"); 7 | const ScalewayJwt = require("./jwt/scalewayJwt"); 8 | const ScalewayLogs = require("./logs/scalewayLogs"); 9 | const ScalewayInfo = require("./info/scalewayInfo"); 10 | 11 | class ScalewayIndex { 12 | constructor(serverless, options) { 13 | this.serverless = serverless; 14 | this.options = options; 15 | 16 | this.serverless.pluginManager.addPlugin(ScalewayProvider); 17 | this.serverless.pluginManager.addPlugin(ScalewayDeploy); 18 | this.serverless.pluginManager.addPlugin(ScalewayRemove); 19 | this.serverless.pluginManager.addPlugin(ScalewayInvoke); 20 | this.serverless.pluginManager.addPlugin(ScalewayJwt); 21 | this.serverless.pluginManager.addPlugin(ScalewayLogs); 22 | this.serverless.pluginManager.addPlugin(ScalewayInfo); 23 | } 24 | } 25 | 26 | module.exports = ScalewayIndex; 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🐛 Bug Report 3 | about: Unexpected or broken behavior of the framework 🤔 4 | labels: bug 5 | --- 6 | 7 | 8 | 9 | ### Community Note 10 | 11 | - Please vote on this issue by adding a 👍 [reaction](https://blog.github.com/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/) to the original issue to help the community and maintainers prioritize this request 12 | - Please do not leave "+1" or other comments that do not add relevant new information or questions, they generate extra noise for issue followers and do not help prioritize the request 13 | - If you are interested in working on this issue or have submitted a pull request, please leave a comment 14 | 15 | 16 | 17 | ### What did you do? 18 | 19 | ### What did you expect to see? 20 | 21 | ### What did you see instead? 22 | 23 | ### What version of Node are you using (`node --version`)? 24 | 25 | ```sh 26 | $ node --version 27 | ``` 28 | 29 | ### Does this issue reproduce with the latest release? 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Scaleway 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /docs/custom-domains.md: -------------------------------------------------------------------------------- 1 | # Custom domains 2 | 3 | In addition to the default domain allocated to each function and container, you can also set one or more custom domains. 4 | 5 | You can find more information in the Scaleway docs on how to [add custom domains to functions](https://www.scaleway.com/en/docs/compute/functions/how-to/add-a-custom-domain-name-to-a-function/) or 6 | [add custom domains on containers](https://www.scaleway.com/en/docs/compute/containers/how-to/add-a-custom-domain-to-a-container/). 7 | 8 | You can configure custom domains via your `serverless.yml` too, e.g.: 9 | 10 | ```yaml 11 | functions: 12 | first: 13 | handler: handler.handle 14 | custom_domains: 15 | - my-domain.somehost.com 16 | - my-other-domain.somehost.com 17 | ``` 18 | 19 | Note that you must have a `CNAME` record set up in each domain's DNS configuration, which points to the endpoint of your function or container 20 | 21 | **NOTE**: if you create a domain with other tools (e.g. the Scaleway console, the CLI or APIs) you must also add the created domain to your `serverless.yml`. If not, it will be deleted by your Serverless Framework deployment. 22 | -------------------------------------------------------------------------------- /shared/singleSource.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | getElementsToDelete(singleSourceParam, existingServicesOnApi, servicesNames) { 5 | const serviceNamesRet = servicesNames; 6 | const elementsIdsToRemove = []; 7 | 8 | if ( 9 | singleSourceParam !== undefined && 10 | singleSourceParam !== null && 11 | singleSourceParam === true 12 | ) { 13 | // If a container is available in the API but not in the serverlss.yml file, remove it 14 | for (let i = 0; i < existingServicesOnApi.length; i++) { 15 | const apiService = existingServicesOnApi[i]; 16 | 17 | for (let ii = 0; ii < serviceNamesRet.length; ii++) { 18 | const serviceName = serviceNamesRet[ii]; 19 | 20 | if (apiService === serviceName) { 21 | serviceNamesRet.slice(ii, 1); 22 | break; 23 | } 24 | } 25 | 26 | if (!serviceNamesRet.includes(apiService.name)) { 27 | elementsIdsToRemove.push(apiService.id); 28 | } 29 | } 30 | } 31 | 32 | return { 33 | serviceNamesRet, 34 | elementsIdsToRemove, 35 | }; 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /examples/go/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "golang-scaleway-starter", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "golang-scaleway-starter", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "serverless-scaleway-functions": "file:../.." 13 | } 14 | }, 15 | "../..": { 16 | "version": "0.4.18", 17 | "dev": true, 18 | "license": "MIT", 19 | "dependencies": { 20 | "@serverless/utils": "^6.13.1", 21 | "argon2": "^0.30.3", 22 | "axios": "^1.4.0", 23 | "bluebird": "^3.7.2", 24 | "dockerode": "^3.3.5", 25 | "js-yaml": "^4.1.0" 26 | }, 27 | "devDependencies": { 28 | "@eslint/js": "^9.27.0", 29 | "@jest/globals": "^29.6.1", 30 | "eslint": "^9.12.0", 31 | "fs-extra": "^11.1.1", 32 | "jest": "^29.6.1", 33 | "prettier": "^2.8.8", 34 | "rewire": "^6.0.0" 35 | } 36 | }, 37 | "node_modules/serverless-scaleway-functions": { 38 | "resolved": "../..", 39 | "link": true 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /remove/scalewayRemove.js: -------------------------------------------------------------------------------- 1 | const BbPromise = require("bluebird"); 2 | const setUpDeployment = require("../shared/setUpDeployment"); 3 | const removeNamespace = require("./lib/removeNamespace"); 4 | const validate = require("../shared/validate"); 5 | const scalewayApi = require("../shared/api/endpoint"); 6 | 7 | class ScalewayDeploy { 8 | constructor(serverless, options) { 9 | this.serverless = serverless; 10 | this.options = options || {}; 11 | this.provider = this.serverless.getProvider("scaleway"); 12 | this.provider.initialize(this.serverless, this.options); 13 | 14 | const api = scalewayApi.getApi(this); 15 | 16 | Object.assign(this, setUpDeployment, removeNamespace, validate, api); 17 | 18 | this.hooks = { 19 | // Validate serverless.yml, set up default values, configure deployment... 20 | "before:remove:remove": () => 21 | BbPromise.bind(this).then(this.setUpDeployment).then(this.validate), 22 | // Every tasks related to space deletion: 23 | // - Delete given space if it exists 24 | "remove:remove": () => BbPromise.bind(this).then(this.removeNamespace), 25 | }; 26 | } 27 | } 28 | 29 | module.exports = ScalewayDeploy; 30 | -------------------------------------------------------------------------------- /examples/secrets/serverless.yml: -------------------------------------------------------------------------------- 1 | service: scaleway-secrets 2 | configValidationMode: off 3 | provider: 4 | name: scaleway 5 | runtime: python310 # Available python runtimes are listed in documentation 6 | # Global Environment variables - used in every functions 7 | env: 8 | env_notSecretA: notSecret 9 | # Global Secret Environment variables - used in every functions 10 | secret: 11 | env_secretA: valueA 12 | env_secretB: "value with special characters ^:;" 13 | env_secretC: ${ENV_SECRETC} # reference to a local env var ENV_SECRETC, must be set 14 | 15 | plugins: 16 | - serverless-scaleway-functions 17 | 18 | package: 19 | patterns: 20 | - "!node_modules/**" 21 | - "!.gitignore" 22 | - "!.git/**" 23 | 24 | functions: 25 | first: 26 | handler: handler.handle 27 | # description: "" 28 | # Local environment variables - used only in given function 29 | env: 30 | env_notSecret1: notSecret1 31 | # Local secret environment variables - used only in given function 32 | secret: 33 | env_secret1: value1 34 | env_secret2: "other value with special characters ^:;" 35 | env_secret3: ${ENV_SECRET3} # reference to a local env var ENV_SECRET3, must be set 36 | -------------------------------------------------------------------------------- /jwt/scalewayJwt.js: -------------------------------------------------------------------------------- 1 | const BbPromise = require("bluebird"); 2 | const setUpDeployment = require("../shared/setUpDeployment"); 3 | const getJwt = require("./lib/getJwt"); 4 | const scalewayApi = require("../shared/api/endpoint"); 5 | 6 | class ScalewayJwt { 7 | constructor(serverless, options) { 8 | this.serverless = serverless; 9 | this.options = options || {}; 10 | this.provider = this.serverless.getProvider("scaleway"); 11 | this.provider.initialize(this.serverless, this.options); 12 | 13 | const api = scalewayApi.getApi(this); 14 | 15 | Object.assign(this, setUpDeployment, getJwt, api); 16 | 17 | this.commands = { 18 | jwt: { 19 | usage: "Get JWT Token", 20 | lifecycleEvents: ["jwt"], 21 | commands: { 22 | start: { 23 | usage: 24 | "Get JWT tokens for your namespace and your private functions/containers.", 25 | lifecycleEvents: ["jwt"], 26 | }, 27 | }, 28 | }, 29 | }; 30 | 31 | this.hooks = { 32 | // Validate serverless.yml, set up default values, configure deployment... 33 | "before:jwt:jwt": () => BbPromise.bind(this).then(this.setUpDeployment), 34 | "jwt:jwt": () => BbPromise.bind(this).then(this.getJwt), 35 | }; 36 | } 37 | } 38 | 39 | module.exports = ScalewayJwt; 40 | -------------------------------------------------------------------------------- /tests/utils/fs/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const os = require("os"); 4 | const path = require("path"); 5 | const fs = require("fs"); 6 | const crypto = require("crypto"); 7 | const YAML = require("js-yaml"); 8 | 9 | const tmpDirCommonPath = path.join( 10 | os.tmpdir(), 11 | "tmpdirs-serverless", 12 | crypto.randomBytes(2).toString("hex") 13 | ); 14 | 15 | function getTmpDirPath() { 16 | return path.join(tmpDirCommonPath, crypto.randomBytes(8).toString("hex")); 17 | } 18 | 19 | function createTmpDir() { 20 | const tmpDir = getTmpDirPath(); 21 | fs.mkdirSync(tmpDir, { recursive: true }); 22 | return tmpDir; 23 | } 24 | 25 | function replaceTextInFile(filePath, subString, newSubString) { 26 | const fileContent = fs.readFileSync(filePath).toString(); 27 | fs.writeFileSync(filePath, fileContent.replace(subString, newSubString)); 28 | } 29 | 30 | function readYamlFile(filePath) { 31 | const content = fs.readFileSync(filePath, "utf8"); 32 | return YAML.load(content); 33 | } 34 | 35 | function writeYamlFile(filePath, content) { 36 | const yaml = YAML.dump(content); 37 | fs.writeFileSync(filePath, yaml); 38 | return yaml; 39 | } 40 | 41 | module.exports = { 42 | tmpDirCommonPath, 43 | getTmpDirPath, 44 | createTmpDir, 45 | 46 | replaceTextInFile, 47 | readYamlFile, 48 | writeYamlFile, 49 | }; 50 | -------------------------------------------------------------------------------- /info/scalewayInfo.js: -------------------------------------------------------------------------------- 1 | const BbPromise = require("bluebird"); 2 | const display = require("./lib/display"); 3 | const writeServiceOutputs = require("../shared/write-service-outputs"); 4 | const scalewayApi = require("../shared/api/endpoint"); 5 | 6 | class ScalewayInfo { 7 | constructor(serverless, options) { 8 | this.serverless = serverless; 9 | this.options = options || {}; 10 | this.provider = this.serverless.getProvider("scaleway"); 11 | this.provider.initialize(this.serverless, this.options); 12 | 13 | const api = scalewayApi.getApi(this); 14 | 15 | Object.assign(this, display, api); 16 | 17 | this.commands = { 18 | scaleway: { 19 | type: "entrypoint", 20 | commands: { 21 | info: { 22 | lifecycleEvents: ["displayInfo"], 23 | }, 24 | }, 25 | }, 26 | }; 27 | 28 | this.hooks = { 29 | "info:info": () => this.serverless.pluginManager.spawn("scaleway:info"), 30 | "scaleway:info:displayInfo": async () => 31 | BbPromise.bind(this).then(this.displayInfo), 32 | finalize: () => { 33 | if (this.serverless.processedInput.commands.join(" ") !== "info") 34 | return; 35 | writeServiceOutputs(this.serverless.serviceOutputs); 36 | }, 37 | }; 38 | } 39 | } 40 | 41 | module.exports = ScalewayInfo; 42 | -------------------------------------------------------------------------------- /docs/containers.md: -------------------------------------------------------------------------------- 1 | # Managing containers 2 | 3 | To manage your containers, you can define them in the `custom.containers` field in your `serverless.yml` configuration file. 4 | 5 | Each container must specify the relative path to its directory, which contains the Dockerfile, and all files related to the application: 6 | 7 | ```yml 8 | custom: 9 | containers: 10 | mycontainer: 11 | directory: my-container-directory 12 | env: 13 | MY_VARIABLE: "my-value" 14 | ``` 15 | 16 | Below is an example of a project structure corresponding to the example above, crucially the `my-container-directory` contains all the files necessary for the container build. 17 | 18 | ``` 19 | . 20 | ├── my-container-directory 21 | │   ├── Dockerfile 22 | │   ├── requirements.txt 23 | │ ├── server.py 24 | │   └── (...) 25 | ├── node_modules 26 | │   ├── serverless-scaleway-functions 27 | │ └── (...) 28 | ├── package-lock.json 29 | ├── package.json 30 | └── serverless.yml 31 | ``` 32 | 33 | Serverless Containers automatically have a `PORT` environment variable set, which indicates which port the container's webserver should be listening on. By default `PORT` is 8080. You can change this via the `port` variable in your container definition. 34 | 35 | See the [container example](https://github.com/scaleway/serverless-scaleway-functions/tree/master/examples/container) for more information. 36 | -------------------------------------------------------------------------------- /info/lib/display.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const yaml = require("js-yaml"); 4 | 5 | module.exports = { 6 | displayInfo() { 7 | const configInput = this.serverless.configurationInput; 8 | 9 | this.getNamespaceFromList( 10 | configInput.service, 11 | this.provider.getScwProject() 12 | ).then((namespace) => { 13 | if ( 14 | namespace === undefined || 15 | namespace === null || 16 | namespace.id === undefined || 17 | namespace.id === null 18 | ) { 19 | return; 20 | } 21 | 22 | if ( 23 | configInput.custom && 24 | configInput.custom.containers && 25 | Object.keys(configInput.custom.containers).length !== 0 26 | ) { 27 | this.listContainers(namespace.id).then((containers) => { 28 | let output = {}; 29 | containers.forEach((container) => { 30 | output[container["name"]] = container; 31 | }); 32 | console.log(yaml.dump({ "Stack Outputs": { containers: output } })); 33 | }); 34 | } else { 35 | this.listFunctions(namespace.id).then((functions) => { 36 | let output = {}; 37 | functions.forEach((func) => { 38 | output[func["name"]] = func; 39 | }); 40 | console.log(yaml.dump({ "Stack Outputs": { functions: output } })); 41 | }); 42 | } 43 | }); 44 | }, 45 | }; 46 | -------------------------------------------------------------------------------- /deploy/lib/deployContainers.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const BbPromise = require("bluebird"); 4 | 5 | module.exports = { 6 | deployContainers() { 7 | this.serverless.cli.log("Deploying Containers..."); 8 | return BbPromise.bind(this) 9 | .then(this.deployEachContainer) 10 | .then(() => 11 | this.serverless.cli.log( 12 | "Waiting for container deployments, this may take multiple minutes..." 13 | ) 14 | ) 15 | .then(this.printContainerEndpointsAfterDeployment); 16 | }, 17 | 18 | deployEachContainer() { 19 | const promises = this.containers.map((container) => 20 | this.deployContainer(container.id) 21 | ); 22 | return Promise.all(promises); 23 | }, 24 | 25 | printContainerEndpointsAfterDeployment() { 26 | return this.waitContainersAreDeployed(this.namespace.id).then( 27 | (containers) => { 28 | containers.forEach((container) => { 29 | this.serverless.cli.log( 30 | `Container ${container.name} has been deployed to: https://${container.domain_name}` 31 | ); 32 | 33 | this.serverless.cli.log("Waiting for domains deployment..."); 34 | 35 | this.waitDomainsAreDeployedContainer(container.id).then((domains) => { 36 | domains.forEach((domain) => { 37 | this.serverless.cli.log(`Domain ready : ${domain.hostname}`); 38 | }); 39 | }); 40 | }); 41 | } 42 | ); 43 | }, 44 | }; 45 | -------------------------------------------------------------------------------- /docs/events.md: -------------------------------------------------------------------------------- 1 | # Events 2 | 3 | With events, you can link your functions with CRON Schedule (time-based) or NATS (message-based) triggers. 4 | 5 | To do this you can add an `events` key in your function or container as follows: 6 | 7 | ```yml 8 | # Function 9 | functions: 10 | handler: myHandler.handle 11 | events: 12 | - schedule: 13 | # CRON Job Schedule (UNIX Format) 14 | rate: "1 * * * *" 15 | 16 | # Input variable are passed in your function's event during execution 17 | input: 18 | key: value 19 | key2: value2 20 | - nats: 21 | name: my-nats-event 22 | scw_nats_config: 23 | subject: ">" 24 | mnq_nats_account_id: "nats account id" 25 | mnq_project_id: "project id" 26 | mnq_region: "fr-par" 27 | - sqs: 28 | name: my-sqs-trigger 29 | queue: "name" 30 | projectId: "project-id" # Optional 31 | region: "fr-par" # Optional 32 | 33 | # Container 34 | custom: 35 | containers: 36 | mycontainer: 37 | directory: my-directory 38 | 39 | # Events key 40 | events: 41 | - schedule: 42 | rate: "1 * * * *" 43 | input: 44 | key: value 45 | key2: value2 46 | ``` 47 | 48 | For more information, see the following examples: 49 | 50 | - [NodeJS with schedule trigger](../examples/nodejs-schedule) 51 | - [Container with Schedule Trigger](../examples/container-schedule) 52 | -------------------------------------------------------------------------------- /shared/domains.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | getDomainsToCreate(customDomains, existingDomains) { 5 | const domainsToCreate = []; 6 | 7 | if ( 8 | customDomains !== undefined && 9 | customDomains !== null && 10 | customDomains.length > 0 11 | ) { 12 | customDomains.forEach((customDomain) => { 13 | const domainFounds = existingDomains.filter( 14 | (existingDomain) => existingDomain.hostname === customDomain 15 | ); 16 | 17 | if (domainFounds.length === 0) { 18 | domainsToCreate.push(customDomain); 19 | } 20 | }); 21 | } 22 | 23 | return domainsToCreate; 24 | }, 25 | 26 | getDomainsToDelete(customDomains, existingDomains) { 27 | const domainsIdToDelete = []; 28 | existingDomains.forEach((existingDomain) => { 29 | if ( 30 | (customDomains === undefined || customDomains === null) && 31 | existingDomain.id !== undefined 32 | ) { 33 | domainsIdToDelete.push(existingDomain.id); 34 | } else if (!customDomains.includes(existingDomain.hostname)) { 35 | domainsIdToDelete.push(existingDomain.id); 36 | } 37 | }); 38 | 39 | return domainsIdToDelete; 40 | }, 41 | 42 | formatDomainsStructure(domains) { 43 | const formattedDomains = []; 44 | 45 | domains.forEach((domain) => { 46 | formattedDomains.push({ hostname: domain.hostname, id: domain.id }); 47 | }); 48 | 49 | return formattedDomains; 50 | }, 51 | }; 52 | -------------------------------------------------------------------------------- /logs/lib/getLogs.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const BbPromise = require("bluebird"); 4 | 5 | module.exports = { 6 | getLogs() { 7 | return BbPromise.bind(this) 8 | .then(() => 9 | this.getNamespaceFromList( 10 | this.namespaceName, 11 | this.provider.getScwProject() 12 | ) 13 | ) 14 | .then(this.listApplications) 15 | .all() 16 | .then(this.getApplicationId) 17 | .then(this.getLines) 18 | .then(this.printLines); 19 | }, 20 | 21 | listApplications(namespace) { 22 | if (typeof this.listFunctions === "function") { 23 | return this.listFunctions(namespace.id); 24 | } 25 | return this.listContainers(namespace.id); 26 | }, 27 | 28 | getApplicationId(apps) { 29 | for (let i = 0; i < apps.length; i += 1) { 30 | if (apps[i].name === this.options.function) { 31 | return apps[i]; 32 | } 33 | } 34 | throw new Error(`application "${this.options.function}" not found`); 35 | }, 36 | 37 | printLines(logs) { 38 | this.serverless.cli.log( 39 | '----\n⚠️ WARNING: "serverless logs" command is deprecated and will be removed on March 12, 2024. ' + 40 | "Please use Cockpit as soon as possible to continue browsing your logs. " + 41 | "Refer to our documentation here: https://www.scaleway.com/en/developers/api/serverless-containers/#logs.\n----" 42 | ); 43 | for (let i = logs.length - 1; i >= 0; i -= 1) { 44 | this.serverless.cli.log(logs[i].message); 45 | } 46 | }, 47 | }; 48 | -------------------------------------------------------------------------------- /shared/api/index.js: -------------------------------------------------------------------------------- 1 | const { getApiManager } = require("./utils"); 2 | const accountApi = require("./account"); 3 | const domainApi = require("./domain"); 4 | const namespacesApi = require("./namespaces"); 5 | const functionsApi = require("./functions"); 6 | const containersApi = require("./containers"); 7 | const triggersApi = require("./triggers"); 8 | const jwtApi = require("./jwt"); 9 | const logsApi = require("./logs"); 10 | const runtimesApi = require("./runtimes"); 11 | 12 | // Registry 13 | const RegistryApi = require("./registry"); 14 | 15 | class AccountApi { 16 | constructor(apiUrl, token) { 17 | this.apiManager = getApiManager(apiUrl, token); 18 | Object.assign(this, accountApi); 19 | } 20 | } 21 | 22 | class FunctionApi { 23 | constructor(apiUrl, token) { 24 | this.apiManager = getApiManager(apiUrl, token); 25 | Object.assign( 26 | this, 27 | accountApi, 28 | domainApi, 29 | namespacesApi, 30 | functionsApi, 31 | triggersApi, 32 | jwtApi, 33 | logsApi, 34 | runtimesApi 35 | ); 36 | } 37 | } 38 | 39 | class ContainerApi { 40 | constructor(apiUrl, token) { 41 | this.apiManager = getApiManager(apiUrl, token); 42 | Object.assign( 43 | this, 44 | accountApi, 45 | domainApi, 46 | namespacesApi, 47 | containersApi, 48 | triggersApi, 49 | jwtApi, 50 | logsApi, 51 | runtimesApi 52 | ); 53 | } 54 | } 55 | 56 | module.exports = { 57 | getApiManager, 58 | AccountApi, 59 | FunctionApi, 60 | ContainerApi, 61 | RegistryApi, 62 | }; 63 | -------------------------------------------------------------------------------- /shared/api/domain.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { manageError } = require("./utils"); 4 | 5 | module.exports = { 6 | /** 7 | * createDomain is used to call for domain creation, warning : this 8 | * function does not wait for the domain 9 | * to be ready. 10 | * @param {function_id, hostname} params is an object that contains 11 | * the "function_id" and "hostname". 12 | * @returns Promise with create request result. 13 | */ 14 | createDomain(params) { 15 | return this.apiManager 16 | .post("domains", params) 17 | .then((response) => response.data) 18 | .catch(manageError); 19 | }, 20 | 21 | /** 22 | * deleteDomains is used to destroy an existing domain by it's ID. 23 | * @param {Number} domainID ID of the selected domain. 24 | * @returns 25 | */ 26 | deleteDomain(domainID) { 27 | const updateUrl = `domains/${domainID}`; 28 | 29 | return this.apiManager 30 | .delete(updateUrl) 31 | .then((response) => response.data) 32 | .catch(manageError); 33 | }, 34 | 35 | createDomainAndLog(createDomainParams) { 36 | this.createDomain(createDomainParams) 37 | .then((res) => { 38 | this.serverless.cli.log(`Creating domain ${res.hostname}`); 39 | }) 40 | .then( 41 | () => {}, 42 | (reason) => { 43 | this.serverless.cli.log( 44 | `Error on domain : ${createDomainParams.hostname}, reason : ${reason.message}` 45 | ); 46 | 47 | if (reason.message.includes("could not validate")) { 48 | this.serverless.cli.log( 49 | "Ensure CNAME configuration is ok, it can take some time for a record to propagate" 50 | ); 51 | } 52 | } 53 | ); 54 | }, 55 | }; 56 | -------------------------------------------------------------------------------- /examples/go/README.md: -------------------------------------------------------------------------------- 1 | # Go Runtime (>= 1.17) 2 | 3 | ## Requirements 4 | 5 | - your code must be a valid Go module: a `go.mod` file is expected in the root directory 6 | - your handler function should be in a file at the root of your module 7 | - your handler must be exported, example: `Handle` is correct, `handle` is not because it is not exported 8 | - your handler must have the following signature: `func Handle(w http.ResponseWriter, r *http.Request)` 9 | - `main` package is reserved: you must not have any package named `main` in your module 10 | 11 | Suggested code layout: 12 | 13 | ``` 14 | . 15 | ├── go.mod # your go.mod defines your module 16 | ├── go.sum # not always necessary 17 | ├── myfunc.go # your handler method (exported) must be defined here 18 | └── subpackage # you can have subpackages 19 | └── hello.go # with files inside 20 | ``` 21 | 22 | ## Handler name 23 | 24 | The `handler name` is the name of your handler function (example: `Handle`). 25 | 26 | If your code is in a subfolder, like this: 27 | 28 | ``` 29 | . 30 | └── subfolder 31 | ├── go.mod 32 | ├── go.sum 33 | └── myfunc.go # Handle function in that file 34 | ``` 35 | 36 | The `handler name` must be composed of the folder name and the handler function name, separated by `/`. For the example above, `subfolder/Handle` is the right `handler name`. 37 | 38 | ## Run 39 | 40 | If your code depends on private dependencies, you will need to run `go mod vendor` before deploying your function. 41 | 42 | See [Official Go Vendoring reference](https://go.dev/ref/mod#go-mod-vendor). 43 | 44 | ## Local testing 45 | 46 | This examples use the [Go Framework](https://github.com/scaleway/serverless-functions-go) for local testing. 47 | To call you handler locally run `go run cmd/main.go`. 48 | -------------------------------------------------------------------------------- /deploy/lib/deployFunctions.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const BbPromise = require("bluebird"); 4 | const DEPLOY_FUNCTIONS_CONCURRENCY = 5; // max number of functions deployed at a time 5 | 6 | module.exports = { 7 | deployFunctions() { 8 | this.serverless.cli.log("Deploying Functions..."); 9 | return BbPromise.bind(this).then(this.deployEachFunction); 10 | }, 11 | 12 | deployEachFunction() { 13 | return BbPromise.map( 14 | this.functions, 15 | (func) => { 16 | return this.deployFunction(func.id, {}) 17 | .then((func) => { 18 | this.serverless.cli.log(`Deploying ${func.name}...`); 19 | return func; 20 | }) 21 | .then((func) => this.waitForFunctionStatus(func.id, "ready")) 22 | .then((func) => this.printFunctionInformationAfterDeployment(func)) 23 | .then((func) => this.waitForDomainsDeployment(func)); 24 | }, 25 | { concurrency: DEPLOY_FUNCTIONS_CONCURRENCY } 26 | ); 27 | }, 28 | 29 | printFunctionInformationAfterDeployment(func) { 30 | this.serverless.cli.log( 31 | `Function ${func.name} has been deployed to: https://${func.domain_name}` 32 | ); 33 | 34 | if (func.runtime_message !== undefined && func.runtime_message !== "") { 35 | this.serverless.cli.log(`Runtime information : ${func.runtime_message}`); 36 | } 37 | 38 | return func; 39 | }, 40 | 41 | waitForDomainsDeployment(func) { 42 | this.serverless.cli.log(`Waiting for ${func.name} domains deployment...`); 43 | 44 | this.waitDomainsAreDeployedFunction(func.id).then((domains) => { 45 | domains.forEach((domain) => { 46 | this.serverless.cli.log( 47 | `Domain ready (${func.name}): ${domain.hostname}` 48 | ); 49 | }); 50 | this.serverless.cli.log(`Domains for ${func.name} have been deployed!`); 51 | }); 52 | }, 53 | }; 54 | -------------------------------------------------------------------------------- /shared/api/utils.js: -------------------------------------------------------------------------------- 1 | const axios = require("axios"); 2 | const https = require("https"); 3 | 4 | const version = "0.4.18"; 5 | 6 | const invalidArgumentsType = "invalid_arguments"; 7 | 8 | function getApiManager(apiUrl, token) { 9 | return axios.create({ 10 | baseURL: apiUrl, 11 | headers: { 12 | "User-Agent": `serverless-scaleway-functions/${version}`, 13 | "X-Auth-Token": token, 14 | }, 15 | httpsAgent: new https.Agent({ 16 | rejectUnauthorized: false, 17 | }), 18 | }); 19 | } 20 | 21 | /** 22 | * Custom Error class, to print an error message, and pass the Response if applicable 23 | */ 24 | class CustomError extends Error { 25 | constructor(message, response) { 26 | super(message); 27 | this.response = response; 28 | } 29 | } 30 | 31 | /** 32 | * Display the right error message, check if error has a response and data attribute 33 | * to properly display either the global error, or the component-level error (function/container) 34 | * @param {Error} err - Error thrown 35 | */ 36 | function manageError(err) { 37 | err.response = err.response || {}; 38 | if (!err.response || !err.response.data) { 39 | throw new Error(err); 40 | } 41 | if (err.response.data.message) { 42 | let message = err.response.data.message; 43 | 44 | // In case the error is an InvalidArgumentsError, provide some extra information 45 | if (err.response.data.type === invalidArgumentsType) { 46 | for (const details of err.response.data.details) { 47 | const argumentName = details.argument_name; 48 | const helpMessage = details.help_message; 49 | message += `\n${argumentName}: ${helpMessage}`; 50 | } 51 | } 52 | 53 | throw new CustomError(message, err.response); 54 | } else if (err.response.data.error_message) { 55 | throw new CustomError(err.response.data.error_message, err.response); 56 | } 57 | } 58 | 59 | module.exports = { 60 | getApiManager, 61 | manageError, 62 | CustomError, 63 | }; 64 | -------------------------------------------------------------------------------- /tests/domains/domains.test.js: -------------------------------------------------------------------------------- 1 | const { describe, it, expect } = require("@jest/globals"); 2 | const domainUtils = require("../../shared/domains"); 3 | 4 | describe("Domain utils tests ", () => { 5 | // represents the data from serverless.yml file 6 | const hostnamesInput = ["host1", "host2"]; 7 | 8 | // represents the struct of domains from the API 9 | const structInput = [ 10 | { id: "id1", hostname: "host1" }, 11 | { id: "id2", hostname: "host2" }, 12 | ]; 13 | 14 | it("should format domains", () => { 15 | const res = domainUtils.formatDomainsStructure(structInput); 16 | expect(res.length).toBe(2); 17 | 18 | expect(res[0].id).toBe("id1"); 19 | expect(res[0].hostname).toBe("host1"); 20 | 21 | expect(res[1].id).toBe("id2"); 22 | expect(res[1].hostname).toBe("host2"); 23 | }); 24 | 25 | it("should filters domains that need to be created", () => { 26 | // existing domains and domains to create are the same so should not return elements 27 | const domainsToCreateEmpty = domainUtils.getDomainsToCreate( 28 | hostnamesInput, 29 | structInput 30 | ); 31 | 32 | expect(domainsToCreateEmpty.length).toBe(0); 33 | 34 | // adding host3 35 | const domainsToCreateOne = domainUtils.getDomainsToCreate( 36 | ["host1", "host2", "host3"], 37 | structInput 38 | ); 39 | 40 | expect(domainsToCreateOne.length).toBe(1); 41 | expect(domainsToCreateOne[0]).toBe("host3"); 42 | }); 43 | 44 | it("should filters domains that need to be deleted", () => { 45 | // existing domains and domains to delete are the same so should not delete anything 46 | const domainsToDeleteEmpty = domainUtils.getDomainsToDelete( 47 | hostnamesInput, 48 | structInput 49 | ); 50 | 51 | expect(domainsToDeleteEmpty.length).toBe(0); 52 | 53 | // removing host 2 54 | const domainsToDeleteOne = domainUtils.getDomainsToDelete( 55 | ["host1"], 56 | structInput 57 | ); 58 | 59 | expect(domainsToDeleteOne.length).toBe(1); 60 | expect(domainsToDeleteOne[0]).toBe("id2"); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /deploy/lib/uploadCode.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const fs = require("fs"); 4 | const path = require("path"); 5 | const axios = require("axios"); 6 | const BbPromise = require("bluebird"); 7 | 8 | module.exports = { 9 | uploadCode() { 10 | return BbPromise.bind(this) 11 | .then(this.getPresignedUrlForFunctions) 12 | .then(this.uploadFunctionsCode); 13 | }, 14 | 15 | getPresignedUrlForFunctions() { 16 | const promises = this.functions.map((func) => { 17 | const archivePath = path.resolve( 18 | this.serverless.config.servicePath, 19 | ".serverless", 20 | `${this.namespaceName}.zip` 21 | ); 22 | const stats = fs.statSync(archivePath); 23 | const archiveSize = stats.size; 24 | 25 | // get presigned url 26 | return this.getPresignedUrl(func.id, archiveSize).then((response) => 27 | Object.assign(func, { 28 | uploadUrl: response.url, 29 | uploadHeader: { 30 | content_length: archiveSize, 31 | "Content-Type": "application/octet-stream", 32 | }, 33 | }) 34 | ); 35 | }); 36 | 37 | return Promise.all(promises).catch(() => { 38 | throw new Error( 39 | "An error occured while getting a presigned URL to upload functions's archived code." 40 | ); 41 | }); 42 | }, 43 | 44 | uploadFunctionsCode(functions) { 45 | this.serverless.cli.log("Uploading source code..."); 46 | // Upload functions to s3 47 | const promises = functions.map((func) => { 48 | const archivePath = path.resolve( 49 | this.serverless.config.servicePath, 50 | ".serverless", 51 | `${this.namespaceName}.zip` 52 | ); 53 | return new Promise((resolve, reject) => { 54 | fs.readFile(archivePath, (err, data) => { 55 | if (err) reject(err); 56 | resolve(data); 57 | }); 58 | }).then((data) => 59 | axios({ 60 | data, 61 | method: "put", 62 | url: func.uploadUrl, 63 | headers: func.uploadHeader, 64 | maxContentLength: Infinity, 65 | maxBodyLength: Infinity, 66 | }) 67 | ); 68 | }); 69 | 70 | return Promise.all(promises); 71 | }, 72 | }; 73 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: npmtest 3 | 4 | on: 5 | push: 6 | branches: ["master"] 7 | pull_request: 8 | branches: ["master"] 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | lint: 15 | runs-on: ubuntu-24.04 16 | steps: 17 | - uses: actions/checkout@v4 18 | - run: npx prettier@2.8.8 --check . 19 | 20 | test: 21 | needs: lint 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | tests: 26 | [ 27 | "containers", 28 | "containers-private", 29 | "deploy", 30 | "domains", 31 | "functions", 32 | "multi-region", 33 | "provider", 34 | "runtimes", 35 | "shared", 36 | "triggers", 37 | ] 38 | node-version: ["18.x", "20.x"] 39 | 40 | runs-on: ubuntu-24.04 41 | steps: 42 | - uses: actions/checkout@v4 43 | 44 | - name: Set up node ${{ matrix.node-version }} 45 | id: setup-node 46 | uses: actions/setup-node@v4 47 | with: 48 | node-version: ${{ matrix.node-version }} 49 | cache: "npm" 50 | 51 | - name: Install Serverless Framework 52 | run: npm install -g osls@3.51.0 53 | 54 | - name: Install dependencies 55 | run: npm ci 56 | 57 | - name: Run tests 58 | run: npm run test:${{ matrix.tests }} 59 | env: 60 | SCW_SECRET_KEY: ${{ secrets.SCW_SECRET_KEY }} 61 | SCW_REGION: ${{ vars.SCW_REGION }} 62 | SCW_ORGANIZATION_ID: ${{ secrets.SCW_ORGANIZATION_ID }} 63 | 64 | clean-up: 65 | if: ${{ always() }} 66 | needs: test 67 | runs-on: ubuntu-24.04 68 | steps: 69 | - uses: actions/checkout@v4 70 | 71 | - name: Set up node 20.x 72 | id: setup-node 73 | uses: actions/setup-node@v4 74 | with: 75 | node-version: 20.x 76 | cache: "npm" 77 | 78 | - name: Install Serverless Framework 79 | run: npm install -g osls@3.51.0 80 | 81 | - name: Install dependencies 82 | run: npm ci 83 | 84 | - name: Run cleanup 85 | run: npm run clean-up 86 | env: 87 | SCW_SECRET_KEY: ${{ secrets.SCW_SECRET_KEY }} 88 | SCW_ORGANIZATION_ID: ${{ secrets.SCW_ORGANIZATION_ID }} 89 | -------------------------------------------------------------------------------- /docs/javascript.md: -------------------------------------------------------------------------------- 1 | # Node functions 2 | 3 | The `handler` for Node functions must be the path to your handler file plus the function to invoke. For example, with the following directory structure: 4 | 5 | ```yml 6 | - src 7 | - handlers 8 | - firstHandler.js => module.exports.myFirstHandler = ... 9 | - secondHandler.js => module.exports.mySecondHandler = ... 10 | - serverless.yml 11 | ``` 12 | 13 | Your `serverless.yml` would look like: 14 | 15 | ```yml 16 | provider: 17 | runtime: node22 18 | functions: 19 | first: 20 | handler: src/handlers/firstHandler.myFirstHandler 21 | second: 22 | handler: src/handlers/secondHandler.mySecondHandler 23 | ``` 24 | 25 | **NOTE** if you wish to use Typescript, you can do so by transpiling your code locally before deploying it. An example is available [here](../examples/typescript). 26 | 27 | ## ES modules 28 | 29 | Node has two module systems: 30 | 31 | - `CommonJS` - modules (default) 32 | - `ECMAScript`/`ES` modules - gives a more modern way to reuse your code ([docs](https://nodejs.org/api/esm.html)) 33 | 34 | According to the official documentation, to use ES modules you can specify the module type in `package.json`, as in the following example: 35 | 36 | ```json 37 | ... 38 | "type": "module", 39 | ... 40 | ``` 41 | 42 | This then enables you to write your code for ES modules: 43 | 44 | ```javascript 45 | export { handle }; 46 | 47 | function handle(event, context, cb) { 48 | return { 49 | body: process.version, 50 | headers: { "Content-Type": ["text/plain"] }, 51 | statusCode: 200, 52 | }; 53 | } 54 | ``` 55 | 56 | The use of ES modules is encouraged since they are more efficient and make setup and debugging much easier. 57 | 58 | Note that using `"type": "module"` or `"type": "commonjs"` in your `package.json` file will enable or disable some features in Node runtime, such as: 59 | 60 | - `commonjs` is used as the default value 61 | - `commonjs` allows you to use `require/module.exports` (synchronous code loading - it basically copies all file contents) 62 | - `module` allows you to use `import/export` ES6 instructions (asynchronous loading - more optimized as it imports only the pieces of code you need) 63 | 64 | > **Tip**: 65 | > For a comprehensive list of differences, please refer to the [Node.js official documentation](https://nodejs.org/api/esm.html). 66 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-scaleway-functions", 3 | "version": "0.4.18", 4 | "description": "Provider plugin for the Serverless Framework v3.x which adds support for Scaleway Functions.", 5 | "main": "index.js", 6 | "author": "scaleway.com", 7 | "license": "MIT", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/scaleway/serverless-scaleway-functions.git" 11 | }, 12 | "scripts": { 13 | "clean-up": "node tests/teardown.js", 14 | "test:containers": "jest tests/containers/containers.test.js", 15 | "test:containers-private": "jest tests/containers/containers_private_registry.test.js", 16 | "test:deploy": "jest tests/deploy", 17 | "test:domains": "jest tests/domain", 18 | "test:functions": "jest tests/functions", 19 | "test:multi-region": "jest tests/multi-region", 20 | "test:provider": "jest tests/provider", 21 | "test:runtimes": "jest tests/runtimes", 22 | "test:shared": "jest tests/shared", 23 | "test:triggers": "jest tests/triggers", 24 | "coverage": "jest --coverage", 25 | "lint": "eslint . --cache", 26 | "check-format": "prettier --check .", 27 | "format": "prettier --write ." 28 | }, 29 | "homepage": "https://github.com/scaleway/serverless-scaleway-functions", 30 | "keywords": [ 31 | "serverless", 32 | "serverless framework", 33 | "serverless applications", 34 | "serverless modules", 35 | "scaleway functions", 36 | "scaleway", 37 | "iot", 38 | "internet of things", 39 | "osls" 40 | ], 41 | "jest": { 42 | "testEnvironment": "node", 43 | "testRegex": "(/tests/.*|(\\.|/)(test|spec))\\.js$", 44 | "useStderr": true, 45 | "setupFiles": [ 46 | "/tests/setup-tests.js" 47 | ], 48 | "testPathIgnorePatterns": [ 49 | "tests/utils", 50 | "tests/setup-tests.js", 51 | "tests/teardown.js" 52 | ], 53 | "verbose": true 54 | }, 55 | "dependencies": { 56 | "@serverless/utils": "^6.13.1", 57 | "argon2": "^0.30.3", 58 | "axios": "^1.4.0", 59 | "bluebird": "^3.7.2", 60 | "dockerode": "^4.0.6", 61 | "js-yaml": "^4.1.0" 62 | }, 63 | "devDependencies": { 64 | "@eslint/js": "^9.27.0", 65 | "@jest/globals": "^29.6.1", 66 | "eslint": "^9.12.0", 67 | "fs-extra": "^11.1.1", 68 | "jest": "^29.6.1", 69 | "prettier": "^2.8.8", 70 | "rewire": "^6.0.0" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /shared/api/triggers.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { manageError } = require("./utils"); 4 | 5 | module.exports = { 6 | async listTriggersForApplication(applicationId, isFunction) { 7 | let cronTriggersUrl = `crons?function_id=${applicationId}`; 8 | if (!isFunction) { 9 | cronTriggersUrl = `crons?container_id=${applicationId}`; 10 | } 11 | 12 | const cronTriggers = await this.apiManager 13 | .get(cronTriggersUrl) 14 | .then((response) => response.data.crons) 15 | .catch(manageError); 16 | 17 | let messageTriggersUrl = `triggers?function_id=${applicationId}`; 18 | if (!isFunction) { 19 | messageTriggersUrl = `triggers?container_id=${applicationId}`; 20 | } 21 | 22 | const messageTriggers = await this.apiManager 23 | .get(messageTriggersUrl) 24 | .then((response) => response.data.triggers) 25 | .catch(manageError); 26 | 27 | return [...cronTriggers, ...messageTriggers]; 28 | }, 29 | 30 | createCronTrigger(applicationId, isFunction, params) { 31 | let payload = { 32 | ...params, 33 | function_id: applicationId, 34 | }; 35 | 36 | if (!isFunction) { 37 | payload = { 38 | ...params, 39 | container_id: applicationId, 40 | }; 41 | } 42 | return this.apiManager 43 | .post("crons", payload) 44 | .then((response) => response.data) 45 | .catch(manageError); 46 | }, 47 | 48 | createMessageTrigger(applicationId, isFunction, params) { 49 | let payload = { 50 | ...params, 51 | function_id: applicationId, 52 | }; 53 | 54 | if (!isFunction) { 55 | payload = { 56 | ...params, 57 | container_id: applicationId, 58 | }; 59 | } 60 | return this.apiManager 61 | .post("triggers", payload) 62 | .then((response) => response.data) 63 | .catch(manageError); 64 | }, 65 | 66 | updateCronTrigger(triggerId, params) { 67 | const updateUrl = `crons/${triggerId}`; 68 | return this.apiManager 69 | .patch(updateUrl, params) 70 | .then((response) => response.data) 71 | .catch(manageError); 72 | }, 73 | 74 | deleteCronTrigger(triggerId) { 75 | return this.apiManager 76 | .delete(`crons/${triggerId}`) 77 | .then((response) => response.data) 78 | .catch(manageError); 79 | }, 80 | 81 | deleteMessageTrigger(triggerId) { 82 | return this.apiManager 83 | .delete(`triggers/${triggerId}`) 84 | .then((response) => response.data) 85 | .catch(manageError); 86 | }, 87 | }; 88 | -------------------------------------------------------------------------------- /shared/secrets.js: -------------------------------------------------------------------------------- 1 | const argon2 = require("argon2"); 2 | 3 | module.exports = { 4 | // converts an object from serverless framework ({"a": "b", "c": "d"}) 5 | // to an array of secrets expected by the API : 6 | // [{"key": "a", "value": "b"}, {"key": "c", "value": "d"}] 7 | convertObjectToModelSecretsArray(obj) { 8 | if (obj === null || obj === undefined) { 9 | return []; 10 | } 11 | return Object.keys(obj).map((k) => ({ 12 | key: k, 13 | value: obj[k], 14 | })); 15 | }, 16 | 17 | // resolves a value from a secret 18 | // if this is a raw value, return the value 19 | // if this is a reference to a local environment variable, return the value of that env var 20 | resolveSecretValue(key, value, logger) { 21 | const envVarRe = /^\${([^}]*)}$/; 22 | const found = value.match(envVarRe); 23 | 24 | if (!found) { 25 | return value; 26 | } 27 | 28 | if (found[1] in process.env) { 29 | return process.env[found[1]]; 30 | } 31 | 32 | logger.log( 33 | `WARNING: Env var ${found[1]} used in secret ${key} does not exist: this secret will not be created` 34 | ); 35 | return null; 36 | }, 37 | 38 | // returns the secret env vars to send to the API 39 | // it is computed by making the difference between existing secrets and secrets sent via the framework 40 | // see unit tests for all use cases 41 | async mergeSecretEnvVars(existingSecretEnvVars, newSecretEnvVars, logger) { 42 | const existingSecretEnvVarsByKey = new Map( 43 | existingSecretEnvVars.map((i) => [i.key, i.hashed_value]) 44 | ); 45 | const newSecretEnvVarsByKey = new Map( 46 | newSecretEnvVars.map((i) => [ 47 | i.key, 48 | this.resolveSecretValue(i.key, i.value, logger), 49 | ]) 50 | ); 51 | 52 | const result = []; 53 | 54 | for (const [key, hashedValue] of existingSecretEnvVarsByKey) { 55 | if ( 56 | newSecretEnvVarsByKey.get(key) === undefined || 57 | newSecretEnvVarsByKey.get(key) === null 58 | ) { 59 | // secret is removed 60 | result.push({ key, value: null }); 61 | } else { 62 | // exists in both 63 | const hashMatches = await argon2.verify( 64 | hashedValue, 65 | newSecretEnvVarsByKey.get(key) 66 | ); 67 | 68 | if (!hashMatches) { 69 | // secret has changed 70 | result.push({ key, value: newSecretEnvVarsByKey.get(key) }); 71 | } 72 | 73 | newSecretEnvVarsByKey.delete(key); 74 | } 75 | } 76 | 77 | // new secrets 78 | newSecretEnvVarsByKey.forEach((value, key) => { 79 | result.push({ key, value }); 80 | }); 81 | 82 | return result; 83 | }, 84 | }; 85 | -------------------------------------------------------------------------------- /deploy/lib/createNamespace.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const BbPromise = require("bluebird"); 4 | const secrets = require("../../shared/secrets"); 5 | 6 | module.exports = { 7 | createServerlessNamespace() { 8 | return BbPromise.bind(this) 9 | .then(() => 10 | this.getNamespaceFromList( 11 | this.namespaceName, 12 | this.provider.getScwProject() 13 | ) 14 | ) 15 | .then(this.createIfNotExists); 16 | }, 17 | 18 | updateServerlessNamespace() { 19 | return BbPromise.bind(this).then(() => this.updateNamespaceConfiguration()); 20 | }, 21 | 22 | saveNamespaceToProvider(namespace) { 23 | this.namespace = namespace; 24 | }, 25 | 26 | createIfNotExists(foundNamespace) { 27 | // If Space already exists -> Do not create 28 | if (foundNamespace && foundNamespace.status === "error") { 29 | this.saveNamespaceToProvider(foundNamespace); 30 | throw new Error(foundNamespace.error_message); 31 | } 32 | 33 | if (foundNamespace && foundNamespace.status === "ready") { 34 | this.saveNamespaceToProvider(foundNamespace); 35 | return BbPromise.resolve(); 36 | } 37 | 38 | if (foundNamespace && foundNamespace.status !== "ready") { 39 | this.serverless.cli.log("Waiting for Namespace to become ready..."); 40 | return this.waitNamespaceIsReadyAndSave(); 41 | } 42 | 43 | this.serverless.cli.log("Creating namespace..."); 44 | const params = { 45 | name: this.namespaceName, 46 | project_id: this.provider.getScwProject(), 47 | environment_variables: this.namespaceVariables, 48 | secret_environment_variables: secrets.convertObjectToModelSecretsArray( 49 | this.namespaceSecretVariables 50 | ), 51 | }; 52 | 53 | return this.createNamespace(params) 54 | .then((response) => this.saveNamespaceToProvider(response)) 55 | .then(() => this.waitNamespaceIsReadyAndSave()); 56 | }, 57 | 58 | async updateNamespaceConfiguration() { 59 | if (this.namespaceVariables || this.namespaceSecretVariables) { 60 | const params = {}; 61 | if (this.namespaceVariables) { 62 | params.environment_variables = this.namespaceVariables; 63 | } 64 | if (this.namespaceSecretVariables) { 65 | params.secret_environment_variables = await secrets.mergeSecretEnvVars( 66 | this.namespace.secret_environment_variables, 67 | secrets.convertObjectToModelSecretsArray( 68 | this.namespaceSecretVariables 69 | ), 70 | this.serverless.cli 71 | ); 72 | } 73 | return this.updateNamespace(this.namespace.id, params); 74 | } 75 | return undefined; 76 | }, 77 | 78 | waitNamespaceIsReadyAndSave() { 79 | return this.waitNamespaceIsReady(this.namespace.id).then((namespace) => 80 | this.saveNamespaceToProvider(namespace) 81 | ); 82 | }, 83 | }; 84 | -------------------------------------------------------------------------------- /docs/development.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | To run Serverless Framework with a local checkout of this plugin, you can modify the `serverless.yml` for one or more functions as follows: 4 | 5 | ```yaml 6 | ... 7 | 8 | # Change this 9 | plugins: 10 | - serverless-scaleway-functions 11 | 12 | # To this 13 | plugins: 14 | - 15 | ``` 16 | 17 | Then you can run commands as normal. 18 | 19 | ## Integration tests 20 | 21 | This repository contains multiple test suites, each with its own purpose: 22 | 23 | - [Functions](): tests that functions lifecycle (`serverless deploy` and `serverless remove`) works properly. 24 | - [Containers](): tests that container lifecycle (`serverless deploy` and `serverless remove`) works properly. 25 | - [Runtimes](): tests that our runtimes work properly by using the [examples]() we provide to use our platform. 26 | 27 | ### Requirements 28 | 29 | To run your tests locally, you have to make sure of the following: 30 | 31 | - You have docker installed (and usable from your Command Line) 32 | - You have [Serverless CLI](https://github.com/serverless/serverless) installed (and usable from your Command Line) 33 | - You have access to Scaleway Function's Product and Scaleway Container Registry (and still have quotas available). 34 | - You have a Scaleway Account 35 | 36 | ### How to run tests 37 | 38 | #### Configuration 39 | 40 | In order to run tests locally, you have to configure your test suite (for `authentication`). 41 | 42 | To do so, I recommend following the [guide on how to retrieve a token and your project ID](https://github.com/scaleway/serverless-scaleway-functions/blob/master/docs/README.md). 43 | 44 | Then, add it to your environment variables: 45 | 46 | ```bash 47 | export SCW_TOKEN= 48 | export SCW_PROJECT= 49 | ``` 50 | 51 | Optionally, you may change the URL of our `functions` API endpoint (if you need to test different environments for example): 52 | 53 | ```bash 54 | export SCW_URL= 55 | ``` 56 | 57 | #### Run Tests 58 | 59 | We provided multiple test suites, as described above, with the following `npm` scripts: 60 | 61 | - `npm run test`: Run all test suites 62 | - `npm run test:functions`: Run functions's test suite 63 | - `npm run test:containers`: Run containers's test suite 64 | - `npm run test:runtimes`: Run runtimes's test suite 65 | - `npm run test -- -t "Some test regex*"`: Runs all tests matching the regex 66 | 67 | These tests use [Jest](https://jestjs.io/docs/) under the hood. 68 | 69 | **Also, make sure that you did not install this repository inside a `node_modules` folder, otherwhise your npm commands won't work (`no tests found`)**. 70 | 71 | As these test suites imply real-time build/packaging of your functions/containers code and deployment to our platform, they take a bit of time (~3 minutes for functions/containers, and ~6 minutes for runtimes). 72 | -------------------------------------------------------------------------------- /examples/typescript/README.md: -------------------------------------------------------------------------------- 1 | # Use typescript with Node runtime 2 | 3 | ## Requirements 4 | 5 | This example assumes you are familiar with how serverless functions work. If needed, you can check [Scaleway's official documentation](https://www.scaleway.com/en/docs/serverless/functions/quickstart/) 6 | 7 | This example uses the Scaleway Serverless Framework Plugin. Please set up your environment with the requirements stated in the [Scaleway Serverless Framework Plugin](https://github.com/scaleway/serverless-scaleway-functions) before trying out the example. 8 | 9 | Finally, you will need Node.js installed in your computer to run this example. 10 | 11 | ## Context 12 | 13 | By default, Node runtime treats files with the .js suffix. If you wish to use Typescript language with Node runtime, you can do so by following this example. 14 | 15 | ## Description 16 | 17 | This example aims to show how to use Typescript language with Node runtime (node 18 runtime in this example). Used packages are specified in `package.json`. 18 | 19 | The function in this example returns a simple "Hello world!" with a status code 200. 20 | 21 | ## Setup 22 | 23 | ### Install npm modules 24 | 25 | Once your environment is set up (see [Requirements](#requirements)), you can install `npm` dependencies from `package.json` file using: 26 | 27 | ```sh 28 | npm install 29 | ``` 30 | 31 | ### Install a Typescript compiler 32 | 33 | Then, it is necessary to install the [Typescript compiler package](https://www.npmjs.com/package/typescript) globally. 34 | 35 | ```sh 36 | npm install -g typescript 37 | ``` 38 | 39 | You can run `tsc --version` to ensure the compiler is correctly installed. 40 | 41 | ### Create a Typescript configuration file 42 | 43 | When this is done, you can initialize the Typescript project with Node.js. For that, you can run: 44 | 45 | ```sh 46 | tsc --init 47 | ``` 48 | 49 | This will create a `tsconfig.json` file in the project root directory. 50 | 51 | ### Transpile your code 52 | 53 | Before deploying your function, you need to transpile your Typescript code into brower readable JavaScript. 54 | 55 | ```sh 56 | tsc 57 | ``` 58 | 59 | ### Test locally 60 | 61 | The last step before deploying your function is to test it locally. For that, you can run: 62 | 63 | ```sh 64 | NODE_ENV=test node handler.js 65 | ``` 66 | 67 | This will launch a local server, allowing you to test the function. In another terminal, you can now run: 68 | 69 | ```sh 70 | curl -X GET http://localhost:8080 71 | ``` 72 | 73 | The expected output is "Hello world!". 74 | 75 | ## Deploy and run 76 | 77 | Finally, if the test succeeded, you can deploy your function with: 78 | 79 | ```sh 80 | serverless deploy 81 | ``` 82 | 83 | Then, from the given URL, you can check the result in a browser or by running the following command: 84 | 85 | ```sh 86 | # Get request 87 | curl -i -X GET 88 | ``` 89 | 90 | The output should be "Hello world!". 91 | -------------------------------------------------------------------------------- /shared/api/namespaces.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { manageError } = require("./utils"); 4 | 5 | module.exports = { 6 | listNamespaces(projectId) { 7 | const projectIdReq = 8 | projectId === undefined ? "" : `&project_id=${projectId}`; 9 | return this.apiManager 10 | .get(`namespaces?page_size=100${projectIdReq}`) 11 | .then((response) => response.data.namespaces || []) 12 | .catch(manageError); 13 | }, 14 | 15 | getNamespaceFromList(namespaceName, projectId) { 16 | const projectIdReq = 17 | projectId === undefined ? "" : `&project_id=${projectId}`; 18 | // query Scaleway API to check if space exists 19 | return this.apiManager 20 | .get(`namespaces?name=${namespaceName}${projectIdReq}`) 21 | .then((response) => { 22 | const { namespaces } = response.data; 23 | return namespaces[0]; 24 | }) 25 | .catch(manageError); 26 | }, 27 | 28 | getNamespace(namespaceId) { 29 | return this.apiManager 30 | .get(`namespaces/${namespaceId}`) 31 | .then((response) => response.data) 32 | .catch(manageError); 33 | }, 34 | 35 | waitNamespaceIsReady(namespaceId) { 36 | return this.getNamespace(namespaceId).then((namespace) => { 37 | if (namespace.status === "error") { 38 | throw new Error(namespace.error_message); 39 | } 40 | if (namespace.status !== "ready") { 41 | return new Promise((resolve) => { 42 | setTimeout( 43 | () => resolve(this.waitNamespaceIsReady(namespaceId)), 44 | 1000 45 | ); 46 | }); 47 | } 48 | return namespace; 49 | }); 50 | }, 51 | 52 | createNamespace(params) { 53 | return this.apiManager 54 | .post("namespaces", params) 55 | .then((response) => response.data) 56 | .catch(manageError); 57 | }, 58 | 59 | updateNamespace(namespaceId, params) { 60 | return this.apiManager 61 | .patch(`namespaces/${namespaceId}`, params) 62 | .catch(manageError); 63 | }, 64 | 65 | deleteNamespace(namespaceId) { 66 | return this.apiManager 67 | .delete(`namespaces/${namespaceId}`) 68 | .then((response) => response.data) 69 | .catch(manageError); 70 | }, 71 | 72 | waitNamespaceIsDeleted(namespaceId) { 73 | return this.getNamespace(namespaceId) 74 | .then((response) => { 75 | if (response && response.status === "deleting") { 76 | return new Promise((resolve) => { 77 | setTimeout( 78 | () => resolve(this.waitNamespaceIsDeleted(namespaceId)), 79 | 1000 80 | ); 81 | }); 82 | } 83 | return true; 84 | }) 85 | .catch((err) => { 86 | if (err.response && err.response.status === 404) { 87 | return true; 88 | } 89 | throw new Error("An error occured during namespace deletion"); 90 | }); 91 | }, 92 | }; 93 | -------------------------------------------------------------------------------- /invoke/scalewayInvoke.js: -------------------------------------------------------------------------------- 1 | const BbPromise = require("bluebird"); 2 | const axios = require("axios"); 3 | const { EOL } = require("os"); 4 | 5 | const scalewayApi = require("../shared/api/endpoint"); 6 | const setUpDeployment = require("../shared/setUpDeployment"); 7 | const validate = require("../shared/validate"); 8 | 9 | class ScalewayInvoke { 10 | constructor(serverless, options) { 11 | this.serverless = serverless; 12 | this.options = options || {}; 13 | this.provider = this.serverless.getProvider("scaleway"); 14 | this.provider.initialize(this.serverless, this.options); 15 | 16 | const api = scalewayApi.getApi(this); 17 | 18 | Object.assign(this, validate, setUpDeployment, api); 19 | 20 | this.isContainer = false; 21 | this.isFunction = false; 22 | 23 | function validateFunctionOrContainer() { 24 | // Check the user has specified a name, and that it's defined as either a function or container 25 | if (!this.options.function) { 26 | const msg = "Function or container not specified"; 27 | this.serverless.cli.log(msg); 28 | throw new Error(msg); 29 | } 30 | 31 | this.isContainer = this.isDefinedContainer(this.options.function); 32 | this.isFunction = this.isDefinedFunction(this.options.function); 33 | 34 | if (!this.isContainer && !this.isFunction) { 35 | const msg = `Function or container ${this.options.function} not defined in servleress.yml`; 36 | this.serverless.cli.log(msg); 37 | throw new Error(msg); 38 | } 39 | } 40 | 41 | function lookUpFunctionOrContainer(ns) { 42 | // List containers/functions in the namespace 43 | if (this.isContainer) { 44 | return this.listContainers(ns.id); 45 | } else { 46 | return this.listFunctions(ns.id); 47 | } 48 | } 49 | 50 | function doInvoke(found) { 51 | // Filter on name 52 | let func = found.find((f) => f.name === this.options.function); 53 | const url = "https://" + func.domain_name; 54 | 55 | // Invoke 56 | axios 57 | .get(url) 58 | .then((res) => { 59 | // Make sure we write to stdout here to ensure we can capture output 60 | process.stdout.write(JSON.stringify(res.data)); 61 | }) 62 | .catch((error) => { 63 | process.stderr.write(error.toString() + EOL); 64 | }); 65 | } 66 | 67 | this.hooks = { 68 | "before:invoke:invoke": () => 69 | BbPromise.bind(this).then(this.setUpDeployment).then(this.validate), 70 | "invoke:invoke": () => 71 | BbPromise.bind(this) 72 | .then(validateFunctionOrContainer) 73 | .then(() => 74 | this.getNamespaceFromList( 75 | this.namespaceName, 76 | this.provider.getScwProject() 77 | ) 78 | ) 79 | .then(lookUpFunctionOrContainer) 80 | .then(doInvoke), 81 | }; 82 | } 83 | } 84 | 85 | module.exports = ScalewayInvoke; 86 | -------------------------------------------------------------------------------- /tests/utils/clean-up.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { 4 | AccountApi, 5 | FunctionApi, 6 | ContainerApi, 7 | RegistryApi, 8 | } = require("../../shared/api"); 9 | const { 10 | ACCOUNT_API_URL, 11 | FUNCTIONS_API_URL, 12 | CONTAINERS_API_URL, 13 | REGISTRY_API_URL, 14 | } = require("../../shared/constants"); 15 | 16 | const accountApi = new AccountApi(ACCOUNT_API_URL, process.env.SCW_SECRET_KEY); 17 | const regions = ["fr-par", "nl-ams", "pl-waw"]; 18 | 19 | const cleanup = async () => { 20 | const accountApi = new AccountApi( 21 | ACCOUNT_API_URL, 22 | process.env.SCW_SECRET_KEY 23 | ); 24 | const projects = await accountApi.listProjects( 25 | process.env.SCW_ORGANIZATION_ID 26 | ); 27 | for (const project of projects) { 28 | if (project.name.includes("test-slsframework-")) { 29 | process.env.SCW_DEFAULT_PROJECT_ID = project.id; 30 | await removeProjectById(project.id).catch(); 31 | } 32 | } 33 | }; 34 | 35 | const removeProjectById = async (projectId) => { 36 | process.env.SCW_DEFAULT_PROJECT_ID = projectId; 37 | await removeAllTestNamespaces(projectId) 38 | .then(() => accountApi.deleteProject(projectId)) 39 | .catch(() => console.log(`failed to delete project ${projectId}`)); 40 | }; 41 | 42 | module.exports = { removeProjectById, cleanup }; 43 | 44 | const removeAllTestNamespaces = async (projectId) => { 45 | for (const region of regions) { 46 | await removeFunctions(region, projectId).catch(); 47 | await removeContainers(region, projectId).catch(); 48 | await removeRegistryNamespaces(region, projectId).catch(); 49 | } 50 | }; 51 | 52 | const removeFunctions = async (region, projectId) => { 53 | const functionApi = new FunctionApi( 54 | FUNCTIONS_API_URL + `/${region}`, 55 | process.env.SCW_SECRET_KEY 56 | ); 57 | const functions = await functionApi.listNamespaces(projectId); 58 | for (const functionSrv of functions) { 59 | await functionApi 60 | .deleteNamespace(functionSrv.id) 61 | .then( 62 | async () => await functionApi.waitNamespaceIsDeleted(functionSrv.id) 63 | ) 64 | .catch(); 65 | } 66 | }; 67 | 68 | const removeContainers = async (region, projectId) => { 69 | const containerApi = new ContainerApi( 70 | CONTAINERS_API_URL + `/${region}`, 71 | process.env.SCW_SECRET_KEY 72 | ); 73 | const containers = await containerApi.listNamespaces(projectId); 74 | for (const container of containers) { 75 | await containerApi 76 | .deleteNamespace(container.id) 77 | .then(async () => await containerApi.waitNamespaceIsDeleted(container.id)) 78 | .catch(); 79 | } 80 | }; 81 | 82 | const removeRegistryNamespaces = async (region, projectId) => { 83 | const registryApi = new RegistryApi( 84 | REGISTRY_API_URL + `/${region}`, 85 | process.env.SCW_SECRET_KEY 86 | ); 87 | const registries = await registryApi.listRegistryNamespace(projectId); 88 | for (const registry of registries) { 89 | await registryApi.deleteRegistryNamespace(registry.id).catch(); 90 | } 91 | }; 92 | -------------------------------------------------------------------------------- /jwt/lib/getJwt.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const BbPromise = require("bluebird"); 4 | const { PRIVACY_PRIVATE } = require("../../shared/constants"); 5 | 6 | module.exports = { 7 | getJwt() { 8 | if (typeof this.listFunctions === "function") { 9 | return BbPromise.bind(this) 10 | .then(() => 11 | this.getNamespaceFromList( 12 | this.namespaceName, 13 | this.provider.getScwProject() 14 | ) 15 | ) 16 | .then(this.setNamespace) 17 | .then(this.getJwtNamespace) 18 | .then(() => this.listFunctions(this.namespace.id)) 19 | .then(this.getJwtFunctions); 20 | } 21 | if (typeof this.listContainers === "function") { 22 | return BbPromise.bind(this) 23 | .then(() => 24 | this.getNamespaceFromList( 25 | this.namespaceName, 26 | this.provider.getScwProject() 27 | ) 28 | ) 29 | .then(this.setNamespace) 30 | .then(this.getJwtNamespace) 31 | .then(() => this.listContainers(this.namespace.id)) 32 | .then(this.getJwtContainers); 33 | } 34 | }, 35 | 36 | setNamespace(namespace) { 37 | if (!namespace) { 38 | throw new Error( 39 | `Namespace <${this.namespaceName}> doesn't exist, you should deploy it first.` 40 | ); 41 | } 42 | this.namespace = namespace; 43 | }, 44 | 45 | getJwtNamespace() { 46 | return this.issueJwtNamespace(this.namespace.id, this.tokenExpirationDate) 47 | .then((response) => 48 | Object.assign(this.namespace, { token: response.token }) 49 | ) 50 | .then(() => 51 | this.serverless.cli.log( 52 | `Namespace <${this.namespace.name}> token (valid until ${this.tokenExpirationDate}):\n${this.namespace.token}\n` 53 | ) 54 | ); 55 | }, 56 | 57 | getJwtFunctions(functions) { 58 | const promises = functions.map((func) => { 59 | if (func.privacy === PRIVACY_PRIVATE) { 60 | return this.issueJwtFunction(func.id, this.tokenExpirationDate) 61 | .then((response) => Object.assign(func, { token: response.token })) 62 | .then(() => 63 | this.serverless.cli.log( 64 | `Function <${func.name}> token (valid until ${this.tokenExpirationDate}):\n${func.token}\n` 65 | ) 66 | ); 67 | } 68 | return undefined; 69 | }); 70 | return Promise.all(promises); 71 | }, 72 | 73 | getJwtContainers(containers) { 74 | const promises = containers.map((container) => { 75 | if (container.privacy === PRIVACY_PRIVATE) { 76 | return this.issueJwtFunction(container.id, this.tokenExpirationDate) 77 | .then((response) => 78 | Object.assign(container, { token: response.token }) 79 | ) 80 | .then(() => 81 | this.serverless.cli.log( 82 | `Container <${container.name}> token (valid until ${this.tokenExpirationDate}):\n${container.token}\n` 83 | ) 84 | ); 85 | } 86 | return undefined; 87 | }); 88 | return Promise.all(promises); 89 | }, 90 | }; 91 | -------------------------------------------------------------------------------- /deploy/scalewayDeploy.js: -------------------------------------------------------------------------------- 1 | const BbPromise = require("bluebird"); 2 | const validate = require("../shared/validate"); 3 | const setUpDeployment = require("../shared/setUpDeployment"); 4 | const createNamespace = require("./lib/createNamespace"); 5 | const createFunctions = require("./lib/createFunctions"); 6 | const createContainers = require("./lib/createContainers"); 7 | const buildAndPushContainers = require("./lib/buildAndPushContainers"); 8 | const uploadCode = require("./lib/uploadCode"); 9 | const deployFunctions = require("./lib/deployFunctions"); 10 | const deployContainers = require("./lib/deployContainers"); 11 | const deployTriggers = require("./lib/deployTriggers"); 12 | const scalewayApi = require("../shared/api/endpoint"); 13 | const domainApi = require("../shared/api/domain"); 14 | 15 | class ScalewayDeploy { 16 | constructor(serverless, options) { 17 | this.serverless = serverless; 18 | this.options = options || {}; 19 | this.provider = this.serverless.getProvider("scaleway"); 20 | this.provider.initialize(this.serverless, this.options); 21 | 22 | const api = scalewayApi.getApi(this); 23 | 24 | Object.assign( 25 | this, 26 | validate, 27 | setUpDeployment, 28 | createNamespace, 29 | createFunctions, 30 | createContainers, 31 | buildAndPushContainers, 32 | uploadCode, 33 | deployFunctions, 34 | deployContainers, 35 | deployTriggers, 36 | domainApi, 37 | api 38 | ); 39 | 40 | function chainContainers() { 41 | if ( 42 | this.provider.serverless.service.custom && 43 | this.provider.serverless.service.custom.containers && 44 | Object.keys(this.provider.serverless.service.custom.containers) 45 | .length !== 0 46 | ) { 47 | return this.createContainers() 48 | .then(this.buildAndPushContainers) 49 | .then(this.deployContainers); 50 | } 51 | return undefined; 52 | } 53 | 54 | function chainFunctions() { 55 | if ( 56 | this.provider.serverless.service.functions && 57 | Object.keys(this.provider.serverless.service.functions).length !== 0 58 | ) { 59 | return this.createFunctions() 60 | .then(this.uploadCode) 61 | .then(this.deployFunctions); 62 | } 63 | return undefined; 64 | } 65 | 66 | this.hooks = { 67 | // Validate serverless.yml, set up default values, configure deployment... 68 | "before:deploy:deploy": () => 69 | BbPromise.bind(this).then(this.setUpDeployment).then(this.validate), 70 | // Every tasks related to functions deployment: 71 | // - Create a namespace if it does not exist 72 | // - Create each functions in API if it does not exist 73 | // - Zip code - zip each function 74 | // - Get Presigned URL and Push code for each function to S3 75 | // - Deploy each function / container 76 | "deploy:deploy": () => 77 | BbPromise.bind(this) 78 | .then(this.createServerlessNamespace) 79 | .then(chainContainers) 80 | .then(chainFunctions) 81 | .then(this.updateServerlessNamespace) 82 | .then(this.deployTriggers), 83 | }; 84 | } 85 | } 86 | 87 | module.exports = ScalewayDeploy; 88 | -------------------------------------------------------------------------------- /tests/multi-region/multi_region.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const fs = require("fs"); 4 | const path = require("path"); 5 | 6 | const { describe, it, expect } = require("@jest/globals"); 7 | 8 | const { execSync } = require("../../shared/child-process"); 9 | const { getTmpDirPath, replaceTextInFile } = require("../utils/fs"); 10 | const { 11 | getServiceName, 12 | serverlessDeploy, 13 | serverlessRemove, 14 | serverlessInvoke, 15 | createProject, 16 | } = require("../utils/misc"); 17 | const { FunctionApi } = require("../../shared/api"); 18 | const { FUNCTIONS_API_URL } = require("../../shared/constants"); 19 | const { removeProjectById } = require("../utils/clean-up"); 20 | 21 | const serverlessExec = path.join("serverless"); 22 | 23 | const scwToken = process.env.SCW_SECRET_KEY; 24 | 25 | const functionTemplateName = path.resolve( 26 | __dirname, 27 | "..", 28 | "..", 29 | "examples", 30 | "python3" 31 | ); 32 | const oldCwd = process.cwd(); 33 | const serviceName = getServiceName(); 34 | 35 | const regions = ["fr-par", "nl-ams", "pl-waw"]; 36 | 37 | describe("test regions", () => { 38 | it.concurrent.each(regions)("region %s", async (region) => { 39 | let options = {}; 40 | options.env = {}; 41 | options.env.SCW_SECRET_KEY = scwToken; 42 | 43 | let projectId, api, namespace, apiUrl; 44 | 45 | // should create project 46 | // not in beforeAll because of a known bug between concurrent tests and async beforeAll 47 | await createProject() 48 | .then((project) => { 49 | projectId = project.id; 50 | }) 51 | .catch((err) => console.error(err)); 52 | options.env.SCW_DEFAULT_PROJECT_ID = projectId; 53 | 54 | // should create working directory 55 | const tmpDir = getTmpDirPath(); 56 | execSync( 57 | `${serverlessExec} create --template-path ${functionTemplateName} --path ${tmpDir}` 58 | ); 59 | process.chdir(tmpDir); 60 | execSync(`npm link ${oldCwd}`); 61 | replaceTextInFile("serverless.yml", "scaleway-python3", serviceName); 62 | expect(fs.existsSync(path.join(tmpDir, "serverless.yml"))).toEqual(true); 63 | expect(fs.existsSync(path.join(tmpDir, "handler.py"))).toEqual(true); 64 | 65 | // should deploy service for region ${region} 66 | apiUrl = `${FUNCTIONS_API_URL}/${region}`; 67 | api = new FunctionApi(apiUrl, scwToken); 68 | options.env.SCW_REGION = region; 69 | serverlessDeploy(options); 70 | namespace = await api 71 | .getNamespaceFromList(serviceName, projectId) 72 | .catch((err) => console.error(err)); 73 | namespace.functions = await api 74 | .listFunctions(namespace.id) 75 | .catch((err) => console.error(err)); 76 | 77 | // should invoke service for region ${region} 78 | const deployedFunction = namespace.functions[0]; 79 | expect(deployedFunction.domain_name.split(".")[3]).toEqual(region); 80 | options.serviceName = deployedFunction.name; 81 | const output = serverlessInvoke(options).toString(); 82 | expect(output).toEqual( 83 | '"Hello From Python3 runtime on Serverless Framework and Scaleway Functions"' 84 | ); 85 | 86 | // should remove service for region ${region} 87 | serverlessRemove(options); 88 | try { 89 | await api.getNamespace(namespace.id); 90 | } catch (err) { 91 | expect(err.response.status).toEqual(404); 92 | } 93 | 94 | // should remove project 95 | await removeProjectById(projectId).catch((err) => console.error(err)); 96 | }); 97 | }); 98 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies within all project spaces, and it also applies when 49 | an individual is representing the project or its community in public spaces. 50 | Examples of representing a project or community include using an official 51 | project e-mail address, posting via an official social media account, or acting 52 | as an appointed representative at an online or offline event. Representation of 53 | a project may be further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at opensource@scaleway.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /deploy/lib/deployTriggers.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const BbPromise = require("bluebird"); 4 | 5 | module.exports = { 6 | deployTriggers() { 7 | this.serverless.cli.log("Deploying triggers..."); 8 | return BbPromise.bind(this) 9 | .then(() => this.manageTriggers(this.functions, true)) 10 | .then(() => this.manageTriggers(this.containers, false)); 11 | }, 12 | 13 | manageTriggers(applications, isFunction) { 14 | if (!applications || !applications.length) { 15 | return undefined; 16 | } 17 | 18 | // For each Functions 19 | const promises = applications.map((application) => 20 | this.getTriggersForApplication(application, isFunction) 21 | .then((appWithTriggers) => 22 | this.deletePreviousTriggersForApplication(appWithTriggers) 23 | ) 24 | .then(() => 25 | this.createNewTriggersForApplication(application, isFunction) 26 | ) 27 | .then((triggers) => 28 | this.printDeployedTriggersForApplication(application, triggers) 29 | ) 30 | ); 31 | 32 | return Promise.all(promises); 33 | }, 34 | 35 | getTriggersForApplication(application, isFunction) { 36 | return this.listTriggersForApplication(application.id, isFunction).then( 37 | (triggers) => ({ 38 | ...application, 39 | currentTriggers: [...triggers], 40 | }) 41 | ); 42 | }, 43 | 44 | deletePreviousTriggersForApplication(application) { 45 | // Delete and re-create every triggers... 46 | const deleteTriggersPromises = application.currentTriggers.map( 47 | (trigger) => { 48 | if ("schedule" in trigger) { 49 | this.deleteCronTrigger(trigger.id); 50 | } else { 51 | this.deleteMessageTrigger(trigger.id); 52 | } 53 | } 54 | ); 55 | 56 | return Promise.all(deleteTriggersPromises); 57 | }, 58 | 59 | createNewTriggersForApplication(application, isFunction) { 60 | // Get application for serverless service, to get events 61 | let serverlessApp; 62 | if (isFunction) { 63 | serverlessApp = 64 | this.provider.serverless.service.functions[application.name]; 65 | } else { 66 | serverlessApp = 67 | this.provider.serverless.service.custom.containers[application.name]; 68 | } 69 | 70 | if (!serverlessApp || !serverlessApp.events) { 71 | return []; 72 | } 73 | 74 | const createTriggersPromises = serverlessApp.events.map((event) => { 75 | if ("schedule" in event) { 76 | this.createCronTrigger(application.id, isFunction, { 77 | schedule: event.schedule.rate, 78 | args: event.schedule.input || {}, 79 | }); 80 | } 81 | if ("nats" in event) { 82 | this.createMessageTrigger(application.id, isFunction, { 83 | name: event.nats.name, 84 | scw_nats_config: event.nats.scw_nats_config, 85 | }); 86 | } 87 | if ("sqs" in event) { 88 | this.createMessageTrigger(application.id, isFunction, { 89 | name: event.sqs.name, 90 | scw_sqs_config: { 91 | queue: event.sqs.queue, 92 | mnq_project_id: 93 | event.sqs.projectId || this.provider.getScwProject(), 94 | mnq_region: event.sqs.region || this.provider.getScwRegion(), 95 | }, 96 | }); 97 | } 98 | }); 99 | 100 | return Promise.all(createTriggersPromises); 101 | }, 102 | 103 | printDeployedTriggersForApplication(application, triggers) { 104 | triggers.forEach(() => 105 | this.serverless.cli.log( 106 | `Deployed a new trigger for application ${application.name}` 107 | ) 108 | ); 109 | return undefined; 110 | }, 111 | }; 112 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribute to `serverless-scaleway-functions` 2 | 3 | ## Topics 4 | 5 | - [Contribute to `serverless-scaleway-functions`](#contribute-to-serverless-scaleway-functions) 6 | - [Topics](#topics) 7 | - [Reporting security issues](#reporting-security-issues) 8 | - [Reporting issues](#reporting-issues) 9 | - [Suggesting a feature](#suggesting-a-feature) 10 | - [Contributing code](#contributing-code) 11 | - [Submit code](#submit-code) 12 | - [Pull Request Guidelines](#pull-request-guidelines) 13 | - [Community Guidelines](#community-guidelines) 14 | 15 | ## Reporting security issues 16 | 17 | At Scaleway we take security seriously. If you have any issues regarding security, 18 | please notify us by sending an email to security@scaleway.com. 19 | 20 | Please DO NOT create a GitHub issue. 21 | 22 | We will follow up with you promptly with more information and a remediation plan. 23 | We currently do not offer a paid security bounty program, but we would love to send some 24 | Scaleway swag your way along with our deepest gratitude for your assistance in making 25 | Scaleway a more secure Cloud ecosystem. 26 | 27 | ## Reporting issues 28 | 29 | A great way to contribute to the project is to send a detailed report when you encounter a bug. 30 | We always appreciate a well-written, thorough bug report, and will thank you for it! 31 | Before opening a new issue, we appreciate you reviewing open issues to see if there are any similar requests. 32 | If there is a match, thumbs up the issue with a 👍 and leave a comment if you have additional information. 33 | 34 | When reporting an issue, please include the npm version number of `serverless-scaleway-functions` that you are using. 35 | 36 | ## Suggesting a feature 37 | 38 | When requesting a feature, some of the questions we want to answer are: 39 | 40 | - What value does this feature bring to end users? 41 | - How urgent is the need (nice to have feature or need to have)? 42 | - Does this align with the goals of this library? 43 | 44 | ## Contributing code 45 | 46 | ### Submit code 47 | 48 | To submit code: 49 | 50 | - Create a fork of the project 51 | - Create a topic branch from where you want to base your work (usually main) 52 | - Add tests to cover contributed code 53 | - Push your commit(s) to your topic branch on your fork 54 | - Open a pull request against `serverless-scaleway-functions` `main` branch that follows [PR guidelines](#pull-request-guidelines) 55 | 56 | The maintainers of `serverless-scaleway-functions` use a "Let's Get This Merged" (LGTM) message in the pull request to note that the commits are ready to merge. 57 | After one or more maintainer states LGTM, we will merge. 58 | If you have questions or comments on your code, feel free to correct these in your branch through new commits. 59 | 60 | ### Pull Request Guidelines 61 | 62 | The goal of the following guidelines is to have Pull Requests (PRs) that are fairly easy to review and comprehend, and code that is easy to maintain in the future. 63 | 64 | - **Pull Request title should respect [conventional commits](https://www.conventionalcommits.org/en/v1.0.0) specifications** and be clear on what is being changed. 65 | 66 | - A fix for local testing will be titled `fix(local-testing): ...` 67 | - A fix for http requests will be titled `fix(http): ...` 68 | 69 | - **Keep it readable for human reviewers** and prefer a subset of functionality (code) with tests and documentation over delivering them separately 70 | 71 | - **Notify Work In Progress PRs** by prefixing the title with `[WIP]` 72 | - **Please, keep us updated.** 73 | We will try our best to merge your PR, but please notice that PRs may be closed after 30 days of inactivity. 74 | 75 | Your pull request should be rebased against the `main` branch. 76 | 77 | Keep in mind only the **pull request title** will be used as the commit message as we stash all commits on merge. 78 | 79 | ## Community Guidelines 80 | 81 | See [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md). 82 | 83 | Thank you for reading through all of this, if you have any questions feel free to [reach us](../README.md#reach-us)! 84 | -------------------------------------------------------------------------------- /tests/utils/misc/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const path = require("path"); 4 | 5 | const { execSync } = require("../../../shared/child-process"); 6 | const { readYamlFile, writeYamlFile } = require("../fs"); 7 | const crypto = require("crypto"); 8 | const { AccountApi } = require("../../../shared/api"); 9 | const { ACCOUNT_API_URL } = require("../../../shared/constants"); 10 | 11 | const logger = console; 12 | 13 | const testServiceIdentifier = "scwtestsls"; 14 | 15 | const serverlessExec = "serverless"; 16 | 17 | const project = process.env.SCW_DEFAULT_PROJECT_ID || process.env.SCW_PROJECT; 18 | const organizationId = process.env.SCW_ORGANIZATION_ID; 19 | const secretKey = process.env.SCW_SECRET_KEY || process.env.SCW_TOKEN; 20 | const region = process.env.SCW_REGION; 21 | 22 | function getServiceName(identifier = "") { 23 | const hrtime = process.hrtime(); 24 | return `${testServiceIdentifier}-${identifier}${hrtime[1]}`; 25 | } 26 | 27 | function mergeOptionsWithEnv(options) { 28 | if (!options) { 29 | options = {}; 30 | } 31 | if (!options.env) { 32 | options.env = {}; 33 | } 34 | 35 | options.env.PATH = process.env.PATH; 36 | 37 | if (!options.env.SCW_DEFAULT_PROJECT_ID) { 38 | options.env.SCW_DEFAULT_PROJECT_ID = project; 39 | } 40 | if (!options.env.SCW_SECRET_KEY) { 41 | options.env.SCW_SECRET_KEY = secretKey; 42 | } 43 | if (!options.env.SCW_REGION) { 44 | options.env.SCW_REGION = region; 45 | } 46 | 47 | return options; 48 | } 49 | 50 | function serverlessDeploy(options) { 51 | options = mergeOptionsWithEnv(options); 52 | return execSync(`${serverlessExec} deploy`, options); 53 | } 54 | 55 | function serverlessInvoke(options) { 56 | options = mergeOptionsWithEnv(options); 57 | return execSync( 58 | `${serverlessExec} invoke --function ${options.serviceName}`, 59 | options 60 | ); 61 | } 62 | 63 | function serverlessRemove(options) { 64 | options = mergeOptionsWithEnv(options); 65 | return execSync(`${serverlessExec} remove`, options); 66 | } 67 | 68 | function createTestService( 69 | tmpDir, 70 | repoDir, 71 | options = { 72 | devModuleDir: "", 73 | templateName: "nodejs10", // Name of the template inside example directory to use for test service 74 | serviceName: null, 75 | serverlessConfigHook: null, // Eventual hook that allows to customize serverless config 76 | runCurrentVersion: false, 77 | } 78 | ) { 79 | const serviceName = options.serviceName || getServiceName(); 80 | 81 | if (!options.templateName) { 82 | throw new Error("Template Name must be provided to create a test service"); 83 | } 84 | 85 | // create a new Serverless service 86 | execSync( 87 | `${serverlessExec} create --template-path ${options.templateName} --path ${tmpDir}` 88 | ); 89 | process.chdir(tmpDir); 90 | 91 | // Install our local version of this repo 92 | // If this is not the first time this has been run, or the repo is already linked for development, this requires --force 93 | execSync(`npm link --force ${repoDir}`); 94 | 95 | const serverlessFilePath = path.join(tmpDir, "serverless.yml"); 96 | let serverlessConfig = readYamlFile(serverlessFilePath); 97 | // Ensure unique service name 98 | serverlessConfig.service = serviceName; 99 | if (options.serverlessConfigHook) { 100 | serverlessConfig = options.serverlessConfigHook(serverlessConfig); 101 | } 102 | writeYamlFile(serverlessFilePath, serverlessConfig); 103 | 104 | return serverlessConfig; 105 | } 106 | 107 | function sleep(ms) { 108 | return new Promise((resolve) => setTimeout(resolve, ms)); 109 | } 110 | 111 | async function createProject() { 112 | const accountApi = new AccountApi(ACCOUNT_API_URL, secretKey); 113 | return accountApi.createProject({ 114 | name: `test-slsframework-${crypto.randomBytes(6).toString("hex")}`, 115 | organization_id: organizationId, 116 | }); 117 | } 118 | 119 | module.exports = { 120 | logger, 121 | testServiceIdentifier, 122 | serverlessExec, 123 | getServiceName, 124 | serverlessDeploy, 125 | serverlessInvoke, 126 | serverlessRemove, 127 | createTestService, 128 | sleep, 129 | createProject, 130 | }; 131 | -------------------------------------------------------------------------------- /tests/runtimes/runtimes.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const fs = require("fs"); 4 | const path = require("path"); 5 | 6 | const { getTmpDirPath } = require("../utils/fs"); 7 | const { 8 | getServiceName, 9 | serverlessDeploy, 10 | serverlessRemove, 11 | createProject, 12 | sleep, 13 | createTestService, 14 | serverlessInvoke, 15 | } = require("../utils/misc"); 16 | 17 | const { FunctionApi } = require("../../shared/api"); 18 | const { FUNCTIONS_API_URL } = require("../../shared/constants"); 19 | const { describe, it, expect } = require("@jest/globals"); 20 | const { removeProjectById } = require("../utils/clean-up"); 21 | 22 | const scwRegion = process.env.SCW_REGION; 23 | const scwToken = process.env.SCW_SECRET_KEY; 24 | 25 | const functionApiUrl = `${FUNCTIONS_API_URL}/${scwRegion}`; 26 | const devModuleDir = path.resolve(__dirname, "..", ".."); 27 | const examplesDir = path.resolve(devModuleDir, "examples"); 28 | 29 | const oldCwd = process.cwd(); 30 | 31 | /* Some examples are already indirectly tested in other tests, so we don't test them again here. For 32 | * example, container-schedule and nodejs-schedule are tested in triggers, python3 in multi_regions, 33 | * etc... */ 34 | const exampleRepositories = [ 35 | "go", 36 | "multiple", 37 | "nodejs-es-modules", 38 | "php", 39 | "rust", 40 | "secrets", 41 | ]; 42 | 43 | describe("test runtimes", () => { 44 | it.concurrent.each(exampleRepositories)( 45 | "test runtimes %s", 46 | async (runtime) => { 47 | let options = {}; 48 | options.env = {}; 49 | options.env.SCW_SECRET_KEY = scwToken; 50 | options.env.SCW_REGION = scwRegion; 51 | 52 | let api, projectId; 53 | 54 | // Should create project 55 | await createProject() 56 | .then((project) => { 57 | projectId = project.id; 58 | }) 59 | .catch((err) => console.error(err)); 60 | options.env.SCW_DEFAULT_PROJECT_ID = projectId; 61 | 62 | // should create service for runtime ${runtime} in tmp directory 63 | const tmpDir = getTmpDirPath(); 64 | const serviceName = getServiceName(runtime); 65 | createTestService(tmpDir, oldCwd, { 66 | devModuleDir, 67 | templateName: path.resolve(examplesDir, runtime), 68 | serviceName: serviceName, 69 | runCurrentVersion: true, 70 | }); 71 | 72 | expect(fs.existsSync(path.join(tmpDir, "serverless.yml"))).toEqual(true); 73 | expect(fs.existsSync(path.join(tmpDir, "package.json"))).toEqual(true); 74 | 75 | // should deploy service for runtime ${runtime} to scaleway 76 | let optionsWithSecrets = options; 77 | if (runtime === "secrets") { 78 | optionsWithSecrets.env.ENV_SECRETC = "valueC"; 79 | optionsWithSecrets.env.ENV_SECRET3 = "value3"; 80 | } 81 | serverlessDeploy(optionsWithSecrets); 82 | 83 | api = new FunctionApi(functionApiUrl, scwToken); 84 | let namespace = await api 85 | .getNamespaceFromList(serviceName, projectId) 86 | .catch((err) => console.error(err)); 87 | namespace.functions = await api 88 | .listFunctions(namespace.id) 89 | .catch((err) => console.error(err)); 90 | 91 | // should invoke function for runtime ${runtime} from scaleway 92 | const deployedApplication = namespace.functions[0]; 93 | await sleep(30000); 94 | process.chdir(tmpDir); 95 | optionsWithSecrets.serviceName = deployedApplication.name; 96 | const output = serverlessInvoke(optionsWithSecrets).toString(); 97 | expect(output).not.toEqual(""); 98 | 99 | if (runtime === "secrets") { 100 | expect(output).toEqual( 101 | '{"env_vars":["env_notSecret1","env_notSecretA","env_secret1","env_secret2","env_secret3","env_secretA","env_secretB","env_secretC"]}' 102 | ); 103 | } 104 | 105 | // should remove service for runtime ${runtime} from scaleway 106 | process.chdir(tmpDir); 107 | serverlessRemove(optionsWithSecrets); 108 | try { 109 | await api.getNamespace(namespace.id); 110 | } catch (err) { 111 | expect(err.response.status).toEqual(404); 112 | } 113 | 114 | // Should delete project 115 | await removeProjectById(projectId).catch((err) => console.error(err)); 116 | } 117 | ); 118 | }); 119 | -------------------------------------------------------------------------------- /tests/provider/scalewayProvider.test.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | 4 | const ScalewayProvider = require("../../provider/scalewayProvider"); 5 | const { createTmpDir } = require("../utils/fs"); 6 | const { afterAll, beforeAll, describe, it, expect } = require("@jest/globals"); 7 | 8 | class MockServerless { 9 | constructor() { 10 | this.service = {}; 11 | this.service.provider = {}; 12 | 13 | this.cli = {}; 14 | this.cli.log = (logMsg) => { 15 | console.log(logMsg); 16 | }; 17 | } 18 | 19 | setProvider(provName, prov) { 20 | this.service.provider = prov; 21 | } 22 | } 23 | 24 | describe("Scaleway credentials test", () => { 25 | this.expectedToken = null; 26 | this.expectedProject = null; 27 | 28 | this.serverless = new MockServerless(); 29 | this.prov = new ScalewayProvider(this.serverless); 30 | 31 | beforeAll(() => { 32 | // Override scw config file location 33 | this.dummyScwConfigDir = createTmpDir(); 34 | this.dummyScwConfigPath = path.join(this.dummyScwConfigDir, "config.yml"); 35 | 36 | ScalewayProvider.scwConfigFile = this.dummyScwConfigPath; 37 | }); 38 | 39 | afterAll(() => { 40 | // Delete the dummy config file and directory 41 | if (fs.existsSync(this.dummyScwConfigPath)) { 42 | fs.unlinkSync(this.dummyScwConfigPath); 43 | } 44 | 45 | if (fs.existsSync(this.dummyScwConfigDir)) { 46 | fs.rmdirSync(this.dummyScwConfigDir); 47 | } 48 | }); 49 | 50 | this.checkCreds = (options) => { 51 | // Set the credentials 52 | this.prov.setCredentials(options); 53 | 54 | // Check they're as expected 55 | expect(this.prov.scwToken).toEqual(this.expectedToken); 56 | expect(this.prov.scwProject).toEqual(this.expectedProject); 57 | }; 58 | 59 | // ------------------------------------- 60 | // These tests must be written in order of increasing precedence, each one getting superceded by the next. 61 | // ------------------------------------- 62 | 63 | it("should return nothing when no credentials found", () => { 64 | this.expectedToken = ""; 65 | this.expectedProject = ""; 66 | 67 | this.checkCreds({}); 68 | }); 69 | 70 | it("should read from scw config file if present", () => { 71 | // Write the dummy file 72 | const dummyScwConfigContents = 73 | "secret_key: scw-key\ndefault_project_id: scw-proj\n"; 74 | fs.writeFileSync(this.dummyScwConfigPath, dummyScwConfigContents); 75 | 76 | this.expectedToken = "scw-key"; 77 | this.expectedProject = "scw-proj"; 78 | 79 | this.checkCreds({}); 80 | }); 81 | 82 | it("should take values from serverless.yml if present", () => { 83 | this.expectedToken = "conf-token"; 84 | this.expectedProject = "conf-proj"; 85 | 86 | this.serverless.service.provider.scwToken = this.expectedToken; 87 | this.serverless.service.provider.scwProject = this.expectedProject; 88 | 89 | this.checkCreds({}); 90 | }); 91 | 92 | it("should read from legacy environment variables if present", () => { 93 | let originalToken = process.env.SCW_TOKEN; 94 | let originalProject = process.env.SCW_PROJECT; 95 | 96 | this.expectedToken = "legacy-token"; 97 | this.expectedProject = "legacy-proj"; 98 | 99 | process.env.SCW_TOKEN = this.expectedToken; 100 | process.env.SCW_PROJECT = this.expectedProject; 101 | 102 | this.checkCreds({}); 103 | 104 | process.env.SCW_TOKEN = originalToken; 105 | process.env.SCW_PROJECT = originalProject; 106 | }); 107 | 108 | it("should read from environment variables if present", () => { 109 | let originalToken = process.env.SCW_SECRET_KEY; 110 | let originalProject = process.env.SCW_DEFAULT_PROJECT_ID; 111 | 112 | this.expectedToken = "env-token"; 113 | this.expectedProject = "env-proj"; 114 | 115 | process.env.SCW_SECRET_KEY = this.expectedToken; 116 | process.env.SCW_DEFAULT_PROJECT_ID = this.expectedProject; 117 | 118 | this.checkCreds({}); 119 | 120 | process.env.SCW_SECRET_KEY = originalToken; 121 | process.env.SCW_DEFAULT_PROJECT_ID = originalProject; 122 | }); 123 | 124 | it("should read credentials from options if present", () => { 125 | let options = {}; 126 | options["scw-token"] = "opt-token"; 127 | options["scw-project"] = "opt-proj"; 128 | 129 | this.expectedToken = "opt-token"; 130 | this.expectedProject = "opt-proj"; 131 | 132 | this.checkCreds(options); 133 | }); 134 | }); 135 | -------------------------------------------------------------------------------- /tests/deploy/buildAndPushContainers.test.js: -------------------------------------------------------------------------------- 1 | const rewire = require("rewire"); 2 | const { Readable } = require("stream"); 3 | const { describe, it, expect } = require("@jest/globals"); 4 | 5 | const buildAndPushContainers = rewire( 6 | "../../deploy/lib/buildAndPushContainers.js" 7 | ); 8 | const extractStreamContents = buildAndPushContainers.__get__( 9 | "extractStreamContents" 10 | ); 11 | const findErrorInBuildOutput = buildAndPushContainers.__get__( 12 | "findErrorInBuildOutput" 13 | ); 14 | 15 | describe("extractStreamContents", () => { 16 | it("should extract the contents of a stream", async () => { 17 | const stream = new Readable(); 18 | stream.push("line1"); 19 | stream.push("line2"); 20 | stream.push("line3"); 21 | stream.push(null); // finishes the stream 22 | 23 | const actual = await extractStreamContents(stream, false); 24 | const expected = ["line1", "line2", "line3"]; 25 | expect(actual).toEqual(expected); 26 | }); 27 | }); 28 | 29 | describe("findErrorInBuildOutput", () => { 30 | it("should return undefined if there is no error in the build output", () => { 31 | const buildOutput = [ 32 | '{"stream":"Step 1/6 : FROM python:3.10.3-alpine3.15"}', 33 | '{"stream":"\n"}', 34 | '{"stream":"Step 2/6 : WORKDIR /usr/src/app"}', 35 | '{"stream":"\n"}', 36 | '{"stream":" ---\u003e a6d61425220f\n"}', 37 | '{"stream":"Step 3/6 : COPY requirements.txt ."}', 38 | '{"stream":"\n"}', 39 | '{"stream":" ---\u003e 2c7ea80fe765\n"}', 40 | '{"stream":"Step 4/6 : RUN pip install -qr requirements.txt"}', 41 | '{"stream":"\n"}', 42 | '{"stream":" ---\u003e 1956f056d5ef\n"}', 43 | '{"stream":"Step 5/6 : COPY server.py ."}', 44 | '{"stream":"\n"}', 45 | '{"stream":" ---\u003e 7cd18093d4d5\n"}', 46 | '{"stream":"Step 6/6 : CMD ["python3", "./server.py"]"}', 47 | '{"stream":"\n"}', 48 | '{"stream":" ---\u003e 18a3aff98512\n"}', 49 | '{"aux":{"ID":"sha256:18a3aff985122ac60ac1a333e036bf80633070d1c84cb41d6a8140cb23c2b1a0"}}', 50 | '{"stream":"Successfully built 18a3aff98512\n"}', 51 | '{"stream":"Successfully tagged rg.fr-par.scw.cloud/funcscwcontainerc5xpewjq/first:latest\n"}', 52 | ]; 53 | 54 | const actual = findErrorInBuildOutput(buildOutput); 55 | const expected = undefined; 56 | expect(actual).toEqual(expected); 57 | }); 58 | 59 | it("should return the error message if there is an error in the build output (step failed)", () => { 60 | const buildOutput = [ 61 | '{"stream":"Step 1/7 : FROM python:3.10.3-alpine3.15"}', 62 | '{"stream":"\n"}', 63 | '{"stream":" ---\u003e 82926ff1b668\n"}', 64 | '{"stream":"Step 2/7 : WORKDIR /usr/src/app"}', 65 | '{"stream":"\n"}', 66 | '{"stream":" ---\u003e a6d61425220f\n"}', 67 | '{"stream":"Step 3/7 : RUN unknown_command"}', 68 | '{"stream":"\n"}', 69 | '{"stream":" ---\u003e Running in 45fe4e8ec27b\n"}', 70 | '{"stream":"\u001b[91m/bin/sh: unknown_command: not found\n\u001b[0m"}', 71 | '{"errorDetail":{"code":127,"message":"The command \'/bin/sh -c unknown_command\' returned a non-zero code: 127"},"error":"The command \'/bin/sh -c unknown_command\' returned a non-zero code: 127"}', 72 | ]; 73 | 74 | const actual = findErrorInBuildOutput(buildOutput); 75 | const expected = 76 | "The command '/bin/sh -c unknown_command' returned a non-zero code: 127"; 77 | expect(actual).toEqual(expected); 78 | }); 79 | 80 | it("should return the error message if there is an error in the build output (pull failed)", () => { 81 | const buildOutput = [ 82 | '{"stream":"Step 1/6 : FROM rg.fr-par.scw.cloud/some-private-registry/python:3.10.3-alpine3.15"}', 83 | '{"stream":"\n"}', 84 | '{"errorDetail":{"message":"Head \\"https://rg.fr-par.scw.cloud/v2/some-private-registry/python/manifests/3.10.3-alpine3.15\\": error parsing HTTP 403 response body: no error details found in HTTP response body: \\"{\\"details\\":[{\\"action\\":\\"read\\",\\"resource\\":\\"api_namespace\\"},{\\"action\\":\\"read\\",\\"resource\\":\\"registry_image\\"}],\\"message\\":\\"insufficient permissions\\",\\"type\\":\\"permissions_denied\\"}\\""},"error":"Head \\"https://rg.fr-par.scw.cloud/v2/some-private-registry/python/manifests/3.10.3-alpine3.15\\": error parsing HTTP 403 response body: no error details found in HTTP response body: \\"{\\"details\\":[{\\"action\\":\\"read\\",\\"resource\\":\\"api_namespace\\"},{\\"action\\":\\"read\\",\\"resource\\":\\"registry_image\\"}],\\"message\\":\\"insufficient permissions\\",\\"type\\":\\"permissions_denied\\"}"}', 85 | ]; 86 | 87 | const actual = findErrorInBuildOutput(buildOutput); 88 | const expected = 89 | 'Head "https://rg.fr-par.scw.cloud/v2/some-private-registry/python/manifests/3.10.3-alpine3.15": error parsing HTTP 403 response body: no error details found in HTTP response body: "{"details":[{"action":"read","resource":"api_namespace"},{"action":"read","resource":"registry_image"}],"message":"insufficient permissions","type":"permissions_denied"}"'; 90 | expect(actual).toEqual(expected); 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /tests/triggers/triggers.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const fs = require("fs"); 4 | const path = require("path"); 5 | 6 | const { getTmpDirPath, replaceTextInFile } = require("../utils/fs"); 7 | const { 8 | getServiceName, 9 | serverlessDeploy, 10 | serverlessRemove, 11 | createProject, 12 | createTestService, 13 | } = require("../utils/misc"); 14 | 15 | const { FunctionApi, ContainerApi } = require("../../shared/api"); 16 | const { 17 | FUNCTIONS_API_URL, 18 | CONTAINERS_API_URL, 19 | } = require("../../shared/constants"); 20 | const { describe, it, expect } = require("@jest/globals"); 21 | const { removeProjectById } = require("../utils/clean-up"); 22 | 23 | const scwRegion = process.env.SCW_REGION; 24 | const scwToken = process.env.SCW_SECRET_KEY; 25 | 26 | const functionApiUrl = `${FUNCTIONS_API_URL}/${scwRegion}`; 27 | const containerApiUrl = `${CONTAINERS_API_URL}/${scwRegion}`; 28 | 29 | const devModuleDir = path.resolve(__dirname, "..", ".."); 30 | const oldCwd = process.cwd(); 31 | const examplesDir = path.resolve(devModuleDir, "examples"); 32 | 33 | const runtimesToTest = [ 34 | { name: "nodejs-schedule", isFunction: true }, 35 | { name: "container-schedule", isFunction: false }, 36 | ]; 37 | 38 | describe("test triggers", () => { 39 | it.concurrent.each(runtimesToTest)("triggers for %s", async (runtime) => { 40 | let options = {}; 41 | options.env = {}; 42 | options.env.SCW_SECRET_KEY = scwToken; 43 | options.env.SCW_REGION = scwRegion; 44 | 45 | let projectId, api; 46 | let namespace = {}; 47 | 48 | // should create project 49 | // not in beforeAll because of a known bug between concurrent tests and async beforeAll 50 | await createProject() 51 | .then((project) => { 52 | projectId = project.id; 53 | }) 54 | .catch((err) => console.error(err)); 55 | options.env.SCW_DEFAULT_PROJECT_ID = projectId; 56 | 57 | // should create service in tmp directory 58 | const tmpDir = getTmpDirPath(); 59 | const serviceName = getServiceName(runtime.name); 60 | const config = createTestService(tmpDir, oldCwd, { 61 | devModuleDir, 62 | templateName: path.resolve(examplesDir, runtime.name), 63 | serviceName: serviceName, 64 | runCurrentVersion: true, 65 | }); 66 | expect(fs.existsSync(path.join(tmpDir, "serverless.yml"))).toEqual(true); 67 | expect(fs.existsSync(path.join(tmpDir, "package.json"))).toEqual(true); 68 | 69 | // should deploy function service to scaleway 70 | process.chdir(tmpDir); 71 | serverlessDeploy(options); 72 | if (runtime.isFunction) { 73 | api = new FunctionApi(functionApiUrl, scwToken); 74 | namespace = await api 75 | .getNamespaceFromList(serviceName, projectId) 76 | .catch((err) => console.error(err)); 77 | namespace.functions = await api 78 | .listFunctions(namespace.id) 79 | .catch((err) => console.error(err)); 80 | } else { 81 | api = new ContainerApi(containerApiUrl, scwToken); 82 | namespace = await api 83 | .getNamespaceFromList(serviceName, projectId) 84 | .catch((err) => console.error(err)); 85 | namespace.containers = await api 86 | .listContainers(namespace.id) 87 | .catch((err) => console.error(err)); 88 | } 89 | 90 | // should create cronjob for function 91 | let deployedApplication; 92 | let triggerInputs; 93 | if (runtime.isFunction) { 94 | deployedApplication = namespace.functions[0]; 95 | triggerInputs = config.functions.first.events[0].schedule.input; 96 | } else { 97 | deployedApplication = namespace.containers[0]; 98 | triggerInputs = config.custom.containers.first.events[0].schedule.input; 99 | } 100 | const deployedTriggers = await api 101 | .listTriggersForApplication(deployedApplication.id, runtime.isFunction) 102 | .catch((err) => console.error(err)); 103 | 104 | expect(deployedTriggers.length).toEqual(1); 105 | for (const key in triggerInputs) { 106 | expect(deployedTriggers[0].args[key]).toEqual(triggerInputs[key]); 107 | } 108 | expect(deployedTriggers[0].schedule).toEqual("1 * * * *"); 109 | 110 | // should remove services from scaleway 111 | process.chdir(tmpDir); 112 | serverlessRemove(options); 113 | try { 114 | await api.getNamespace(namespace.id); 115 | } catch (err) { 116 | expect(err.response.status).toEqual(404); 117 | } 118 | 119 | // should throw error invalid schedule 120 | replaceTextInFile("serverless.yml", "1 * * * *", "10 minutes"); 121 | try { 122 | await expect(serverlessDeploy(options)).rejects.toThrow(Error); 123 | } catch (err) { 124 | // If not try catch, test would fail 125 | } 126 | 127 | // should throw error invalid triggerType 128 | replaceTextInFile("serverless.yml", "schedule:", "queue:"); 129 | try { 130 | await expect(serverlessDeploy(options)).rejects.toThrow(Error); 131 | } catch (err) { 132 | // If not try catch, test would fail 133 | } 134 | 135 | // should remove project 136 | await removeProjectById(projectId).catch((err) => console.error(err)); 137 | }); 138 | }); 139 | -------------------------------------------------------------------------------- /provider/scalewayProvider.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const fs = require("fs"); 4 | const os = require("os"); 5 | const path = require("path"); 6 | const yaml = require("js-yaml"); 7 | 8 | const BbPromise = require("bluebird"); 9 | const { FUNCTIONS_API_URL } = require("../shared/constants"); 10 | const { CONTAINERS_API_URL } = require("../shared/constants"); 11 | const { REGISTRY_API_URL } = require("../shared/constants"); 12 | const { DEFAULT_REGION } = require("../shared/constants"); 13 | 14 | const providerName = "scaleway"; 15 | 16 | class ScalewayProvider { 17 | static scwConfigFile = path.join( 18 | os.homedir(), 19 | ".config", 20 | "scw", 21 | "config.yaml" 22 | ); 23 | 24 | static getProviderName() { 25 | return providerName; 26 | } 27 | 28 | constructor(serverless) { 29 | this.serverless = serverless; 30 | this.provider = this; 31 | this.serverless.setProvider(providerName, this); 32 | } 33 | 34 | getScwProject() { 35 | return this.scwProject; 36 | } 37 | 38 | getScwRegion() { 39 | return this.scwRegion; 40 | } 41 | 42 | getFunctionCredentials() { 43 | return { 44 | apiUrl: this.apiFunctionUrl, 45 | token: this.scwToken, 46 | }; 47 | } 48 | 49 | getContainerCredentials() { 50 | return { 51 | apiUrl: this.apiContainerUrl, 52 | token: this.scwToken, 53 | }; 54 | } 55 | 56 | setCredentials(options) { 57 | // On serverless info command we do not want log pollution from authentication. 58 | // This is necessary to use it in an automated environment. 59 | let hideLog = false; 60 | if ( 61 | this.serverless.configurationInput && 62 | this.serverless.configurationInput.service && 63 | this.serverless.configurationInput.service === "serverlessInfo" 64 | ) { 65 | hideLog = true; 66 | } 67 | 68 | if (options["scw-token"] && options["scw-project"]) { 69 | if (!hideLog) { 70 | this.serverless.cli.log( 71 | "Using credentials from command line parameters" 72 | ); 73 | } 74 | 75 | this.scwToken = options["scw-token"]; 76 | this.scwProject = options["scw-project"]; 77 | } else if ( 78 | process.env.SCW_SECRET_KEY && 79 | process.env.SCW_DEFAULT_PROJECT_ID 80 | ) { 81 | if (!hideLog) { 82 | this.serverless.cli.log("Using credentials from system environment"); 83 | } 84 | 85 | this.scwToken = process.env.SCW_SECRET_KEY; 86 | this.scwProject = process.env.SCW_DEFAULT_PROJECT_ID; 87 | } else if (process.env.SCW_TOKEN && process.env.SCW_PROJECT) { 88 | if (!hideLog) { 89 | this.serverless.cli.log("Using credentials from system environment"); 90 | this.serverless.cli.log( 91 | "NOTICE: you are using deprecated environment variable notation," 92 | ); 93 | this.serverless.cli.log( 94 | "please update to SCW_SECRET_KEY and SCW_DEFAULT_PROJECT_ID" 95 | ); 96 | } 97 | 98 | this.scwToken = process.env.SCW_TOKEN; 99 | this.scwProject = process.env.SCW_PROJECT; 100 | } else if ( 101 | this.serverless.service.provider.scwToken || 102 | this.serverless.service.provider.scwProject 103 | ) { 104 | if (!hideLog) { 105 | this.serverless.cli.log("Using credentials from serverless.yml"); 106 | } 107 | 108 | this.scwToken = this.serverless.service.provider.scwToken; 109 | this.scwProject = this.serverless.service.provider.scwProject; 110 | } else if (fs.existsSync(ScalewayProvider.scwConfigFile)) { 111 | if (!hideLog) { 112 | this.serverless.cli.log( 113 | `Using credentials from ${ScalewayProvider.scwConfigFile}` 114 | ); 115 | } 116 | 117 | let fileData = fs.readFileSync(ScalewayProvider.scwConfigFile, "utf8"); 118 | let scwConfig = yaml.load(fileData); 119 | 120 | this.scwToken = scwConfig.secret_key; 121 | this.scwProject = scwConfig.default_project_id; 122 | this.scwRegion = scwConfig.default_region; 123 | } else { 124 | if (!hideLog) { 125 | this.serverless.cli.log( 126 | "Unable to locate Scaleway provider credentials" 127 | ); 128 | } 129 | 130 | this.scwToken = ""; 131 | this.scwProject = ""; 132 | } 133 | } 134 | 135 | setApiURL(options) { 136 | if (options["scw-region"]) { 137 | this.scwRegion = options["scw-region"]; 138 | } else if (process.env.SCW_REGION) { 139 | this.scwRegion = process.env.SCW_REGION; 140 | } else { 141 | this.scwRegion = 142 | this.serverless.service.provider.scwRegion || DEFAULT_REGION; 143 | } 144 | this.apiFunctionUrl = 145 | process.env.SCW_FUNCTION_URL || `${FUNCTIONS_API_URL}/${this.scwRegion}`; 146 | this.apiContainerUrl = 147 | process.env.SCW_CONTAINER_URL || 148 | `${CONTAINERS_API_URL}/${this.scwRegion}`; 149 | this.registryApiUrl = `${REGISTRY_API_URL}/${this.scwRegion}/`; 150 | } 151 | 152 | initialize(serverless, options) { 153 | this.serverless = serverless; 154 | this.options = options; 155 | 156 | return new BbPromise((resolve) => { 157 | this.setCredentials(options); 158 | this.setApiURL(options); 159 | resolve(); 160 | }); 161 | } 162 | } 163 | 164 | module.exports = ScalewayProvider; 165 | -------------------------------------------------------------------------------- /tests/containers/containers_private_registry.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const crypto = require("crypto"); 4 | const Docker = require("dockerode"); 5 | const fs = require("fs"); 6 | const path = require("path"); 7 | 8 | const { afterAll, beforeAll, describe, it, expect } = require("@jest/globals"); 9 | 10 | const { getTmpDirPath, replaceTextInFile } = require("../utils/fs"); 11 | const { 12 | getServiceName, 13 | serverlessDeploy, 14 | serverlessRemove, 15 | serverlessInvoke, 16 | createProject, 17 | } = require("../utils/misc"); 18 | const { ContainerApi, RegistryApi } = require("../../shared/api"); 19 | const { execSync } = require("../../shared/child-process"); 20 | const { 21 | CONTAINERS_API_URL, 22 | REGISTRY_API_URL, 23 | } = require("../../shared/constants"); 24 | const { removeProjectById } = require("../utils/clean-up"); 25 | 26 | const serverlessExec = path.join("serverless"); 27 | 28 | describe("Build and deploy on container with a base image private", () => { 29 | const scwRegion = process.env.SCW_REGION; 30 | const scwToken = process.env.SCW_SECRET_KEY; 31 | const apiUrl = `${CONTAINERS_API_URL}/${scwRegion}`; 32 | const registryApiUrl = `${REGISTRY_API_URL}/${scwRegion}/`; 33 | const templateName = path.resolve( 34 | __dirname, 35 | "..", 36 | "..", 37 | "examples", 38 | "container" 39 | ); 40 | const tmpDir = getTmpDirPath(); 41 | 42 | let options = {}; 43 | options.env = {}; 44 | options.env.SCW_SECRET_KEY = scwToken; 45 | options.env.SCW_REGION = scwRegion; 46 | 47 | let oldCwd, 48 | serviceName, 49 | projectId, 50 | api, 51 | namespace, 52 | containerName, 53 | registryApi; 54 | 55 | const originalImageRepo = "python"; 56 | const imageTag = "3-alpine"; 57 | let privateRegistryImageRepo; 58 | let privateRegistryNamespaceId; 59 | 60 | beforeAll(async () => { 61 | oldCwd = process.cwd(); 62 | serviceName = getServiceName(); 63 | api = new ContainerApi(apiUrl, scwToken); 64 | registryApi = new RegistryApi(registryApiUrl, scwToken); 65 | 66 | await createProject().then((project) => { 67 | projectId = project.id; 68 | }); 69 | options.env.SCW_DEFAULT_PROJECT_ID = projectId; 70 | 71 | // pull the base image, create a private registry, push it into that registry, and remove the image locally 72 | // to check that the image is pulled at build time 73 | const registryName = `private-registry-${crypto 74 | .randomBytes(16) 75 | .toString("hex")}`; 76 | const privateRegistryNamespace = await registryApi.createRegistryNamespace({ 77 | name: registryName, 78 | project_id: projectId, 79 | }); 80 | privateRegistryNamespaceId = privateRegistryNamespace.id; 81 | 82 | privateRegistryImageRepo = `rg.${scwRegion}.scw.cloud/${registryName}/python`; 83 | 84 | const docker = new Docker(); 85 | const pullStream = await docker 86 | .pull(`${originalImageRepo}:${imageTag}`) 87 | .then(); 88 | // Wait for pull to finish 89 | await new Promise((res) => docker.modem.followProgress(pullStream, res)); 90 | const originalImage = await docker.getImage( 91 | `${originalImageRepo}:${imageTag}` 92 | ); 93 | await originalImage.tag({ repo: privateRegistryImageRepo, tag: imageTag }); 94 | const privateRegistryImage = docker.getImage( 95 | `${privateRegistryImageRepo}:${imageTag}` 96 | ); 97 | await privateRegistryImage.push({ 98 | stream: false, 99 | username: "nologin", 100 | password: scwToken, 101 | }); 102 | await privateRegistryImage.remove(); 103 | }); 104 | 105 | afterAll(async () => { 106 | await removeProjectById(projectId).catch(); 107 | }); 108 | 109 | it("should create service in tmp directory", async () => { 110 | execSync( 111 | `${serverlessExec} create --template-path ${templateName} --path ${tmpDir}` 112 | ); 113 | process.chdir(tmpDir); 114 | execSync(`npm link ${oldCwd}`); 115 | replaceTextInFile("serverless.yml", "scaleway-container", serviceName); 116 | replaceTextInFile( 117 | path.join("my-container", "Dockerfile"), 118 | "FROM python:3-alpine", 119 | `FROM ${privateRegistryImageRepo}:${imageTag}` 120 | ); 121 | expect(fs.existsSync(path.join(tmpDir, "serverless.yml"))).toBe(true); 122 | expect(fs.existsSync(path.join(tmpDir, "my-container"))).toBe(true); 123 | }); 124 | 125 | it("should deploy service/container to scaleway", async () => { 126 | serverlessDeploy(options); 127 | namespace = await api.getNamespaceFromList(serviceName, projectId); 128 | namespace.containers = await api.listContainers(namespace.id); 129 | containerName = namespace.containers[0].name; 130 | }); 131 | 132 | it("should invoke container from scaleway", async () => { 133 | await api.waitContainersAreDeployed(namespace.id); 134 | options.serviceName = containerName; 135 | const output = serverlessInvoke(options).toString(); 136 | expect(output).toBe('{"message":"Hello, World from Scaleway Container !"}'); 137 | }); 138 | 139 | it("should remove service from scaleway", async () => { 140 | serverlessRemove(options); 141 | try { 142 | await api.getNamespace(namespace.id); 143 | } catch (err) { 144 | expect(err.response.status).toBe(404); 145 | } 146 | }); 147 | }); 148 | -------------------------------------------------------------------------------- /tests/shared/validate.tests.js: -------------------------------------------------------------------------------- 1 | const validate = require("../../shared/validate"); 2 | const { describe, beforeEach, it, expect } = require("@jest/globals"); 3 | 4 | class MockProvider { 5 | constructor() { 6 | this.serverless = { 7 | service: { 8 | functions: {}, 9 | custom: { 10 | containers: {}, 11 | }, 12 | }, 13 | }; 14 | } 15 | 16 | addFunction(funcName) { 17 | this.serverless.service.functions[funcName] = {}; 18 | } 19 | 20 | addContainer(contName) { 21 | this.serverless.service.custom.containers[contName] = {}; 22 | } 23 | } 24 | 25 | describe("Configuration validation test", () => { 26 | // Add validation to this object 27 | Object.assign(this, validate); 28 | 29 | this.provider = null; 30 | beforeEach(() => { 31 | // Set up new dummy provider 32 | this.provider = new MockProvider(); 33 | }); 34 | 35 | it("Should validate a container when it is defined", () => { 36 | this.provider.addContainer("foobar"); 37 | 38 | expect(this.isDefinedFunction("foobar")).toEqual(false); 39 | expect(this.isDefinedContainer("foobar")).toEqual(true); 40 | expect(this.isDefinedFunction("baz")).toEqual(false); 41 | expect(this.isDefinedContainer("baz")).toEqual(false); 42 | }); 43 | 44 | it("Should validate a function when it is defined", () => { 45 | this.provider.addFunction("qux"); 46 | 47 | expect(this.isDefinedFunction("qux")).toEqual(true); 48 | expect(this.isDefinedContainer("qux")).toEqual(false); 49 | expect(this.isDefinedFunction("baz")).toEqual(false); 50 | expect(this.isDefinedContainer("baz")).toEqual(false); 51 | }); 52 | 53 | it("Should not validate a container when none are defined", () => { 54 | expect(this.isDefinedContainer("qux")).toEqual(false); 55 | }); 56 | 57 | it("Should not validate a function when none are defined", () => { 58 | expect(this.isDefinedFunction("qux")).toEqual(false); 59 | }); 60 | 61 | describe("SQS trigger validation", () => { 62 | it("Should validate a valid SQS trigger", () => { 63 | const validTrigger = { 64 | name: "my-sqs-trigger", 65 | queue: "my-queue-name", 66 | projectId: "12345678-1234-1234-1234-123456789012", 67 | region: "fr-par", 68 | }; 69 | 70 | expect(() => 71 | this.validateTriggers([{ sqs: validTrigger }]) 72 | ).not.toThrow(); 73 | }); 74 | 75 | it("Should validate SQS trigger without optional fields", () => { 76 | const validTrigger = { 77 | name: "my-sqs-trigger", 78 | queue: "my-queue-name", 79 | }; 80 | 81 | expect(() => 82 | this.validateTriggers([{ sqs: validTrigger }]) 83 | ).not.toThrow(); 84 | }); 85 | 86 | it("Should reject SQS trigger with invalid name", () => { 87 | const invalidTrigger = { 88 | name: "a", // too short 89 | queue: "my-queue-name", 90 | }; 91 | 92 | const errors = this.validateTriggers([{ sqs: invalidTrigger }]); 93 | expect(errors).toHaveLength(1); 94 | expect(errors[0]).toContain('Invalid trigger "a": name is invalid'); 95 | }); 96 | 97 | it("Should reject SQS trigger with invalid queue name", () => { 98 | const invalidTrigger = { 99 | name: "my-sqs-trigger", 100 | queue: "a", // too short 101 | }; 102 | 103 | const errors = this.validateTriggers([{ sqs: invalidTrigger }]); 104 | expect(errors).toHaveLength(1); 105 | expect(errors[0]).toContain( 106 | 'Invalid trigger "my-sqs-trigger": queue is invalid' 107 | ); 108 | }); 109 | 110 | it("Should reject SQS trigger with invalid projectId", () => { 111 | const invalidTrigger = { 112 | name: "my-sqs-trigger", 113 | queue: "my-queue-name", 114 | projectId: "invalid-project-id", 115 | }; 116 | 117 | const errors = this.validateTriggers([{ sqs: invalidTrigger }]); 118 | expect(errors).toHaveLength(1); 119 | expect(errors[0]).toContain( 120 | 'Invalid trigger "my-sqs-trigger": projectId is invalid' 121 | ); 122 | }); 123 | 124 | it("Should reject SQS trigger with invalid region", () => { 125 | const invalidTrigger = { 126 | name: "my-sqs-trigger", 127 | queue: "my-queue-name", 128 | region: "invalid-region", 129 | }; 130 | 131 | const errors = this.validateTriggers([{ sqs: invalidTrigger }]); 132 | expect(errors).toHaveLength(1); 133 | expect(errors[0]).toContain( 134 | 'Invalid trigger "my-sqs-trigger": region is unknown' 135 | ); 136 | }); 137 | 138 | it("Should reject SQS trigger without name", () => { 139 | const invalidTrigger = { 140 | queue: "my-queue-name", 141 | }; 142 | 143 | const errors = this.validateTriggers([{ sqs: invalidTrigger }]); 144 | expect(errors).toHaveLength(1); 145 | expect(errors[0]).toContain(": name is invalid"); 146 | }); 147 | 148 | it("Should reject SQS trigger without queue", () => { 149 | const invalidTrigger = { 150 | name: "my-sqs-trigger", 151 | }; 152 | 153 | const errors = this.validateTriggers([{ sqs: invalidTrigger }]); 154 | expect(errors).toHaveLength(1); 155 | expect(errors[0]).toContain( 156 | 'Invalid trigger "my-sqs-trigger": queue is invalid' 157 | ); 158 | }); 159 | }); 160 | }); 161 | -------------------------------------------------------------------------------- /shared/api/containers.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { manageError } = require("./utils"); 4 | 5 | module.exports = { 6 | listContainers(namespaceId) { 7 | const containersUrl = `namespaces/${namespaceId}/containers`; 8 | return this.apiManager 9 | .get(containersUrl) 10 | .then((response) => response.data.containers || []) 11 | .catch(manageError); 12 | }, 13 | 14 | createContainer(params) { 15 | return this.apiManager 16 | .post("containers", params) 17 | .then((response) => response.data) 18 | .catch(manageError); 19 | }, 20 | 21 | updateContainer(containerId, params) { 22 | const updateUrl = `containers/${containerId}`; 23 | return this.apiManager 24 | .patch(updateUrl, params) 25 | .then((response) => response.data) 26 | .catch(manageError); 27 | }, 28 | 29 | deployContainer(containerId) { 30 | return this.apiManager 31 | .post(`containers/${containerId}/deploy`, {}) 32 | .then((response) => response.data) 33 | .catch(manageError); 34 | }, 35 | 36 | /** 37 | * Deletes the container by containerId 38 | * @param {UUID} containerId 39 | * @returns container with status deleting 40 | */ 41 | deleteContainer(containerId) { 42 | return this.apiManager 43 | .delete(`/containers/${containerId}`) 44 | .then((response) => response.data) 45 | .catch(manageError); 46 | }, 47 | 48 | /** 49 | * Get container information by containerId 50 | * @param {UUID} containerId 51 | * @returns container. 52 | */ 53 | getContainer(containerId) { 54 | return this.apiManager 55 | .get(`containers/${containerId}`) 56 | .then((response) => response.data) 57 | .catch(manageError); 58 | }, 59 | 60 | waitContainersAreDeployed(namespaceId) { 61 | return this.apiManager 62 | .get(`namespaces/${namespaceId}/containers`) 63 | .then((response) => { 64 | const containers = response.data.containers || []; 65 | let containersAreReady = true; 66 | for (let i = 0; i < containers.length; i += 1) { 67 | const container = response.data.containers[i]; 68 | if (container.status === "error") { 69 | throw new Error(container.error_message); 70 | } 71 | if (container.status !== "ready") { 72 | containersAreReady = false; 73 | break; 74 | } 75 | } 76 | if (!containersAreReady) { 77 | return new Promise((resolve) => { 78 | setTimeout( 79 | () => resolve(this.waitContainersAreDeployed(namespaceId)), 80 | 5000 81 | ); 82 | }); 83 | } 84 | return containers; 85 | }) 86 | .catch(manageError); 87 | }, 88 | 89 | /** 90 | * 91 | * @param {UUID} containerId id of the container to check 92 | * @param {String} wantedStatus wanted function status before leaving the wait status. 93 | * @returns 94 | */ 95 | waitForContainerStatus(containerId, wantedStatus) { 96 | return this.getContainer(containerId) 97 | .then((func) => { 98 | if (func.status === "error") { 99 | throw new Error(func.error_message); 100 | } 101 | 102 | if (func.status !== wantedStatus) { 103 | return new Promise((resolve) => { 104 | setTimeout( 105 | () => 106 | resolve(this.waitForContainerStatus(containerId, wantedStatus)), 107 | 5000 108 | ); 109 | }); 110 | } 111 | 112 | return func; 113 | }) 114 | .catch((err) => { 115 | // toleration on 4XX errors because on some status, for exemple deleting the API 116 | // will return a 404 err code if item has been deleted. 117 | if (err.response.status >= 500) { 118 | throw new Error(err); 119 | } 120 | }); 121 | }, 122 | 123 | /** 124 | * Waiting for all domains to be ready on a container 125 | * @param {UUID} containerId 126 | * @returns 127 | */ 128 | waitDomainsAreDeployedContainer(containerId) { 129 | return this.listDomainsContainer(containerId).then((domains) => { 130 | let domainsAreReady = true; 131 | 132 | for (let i = 0; i < domains.length; i += 1) { 133 | const domain = domains[i]; 134 | 135 | if (domain.status === "error") { 136 | throw new Error(domain.error_message); 137 | } 138 | 139 | if (domain.status !== "ready") { 140 | domainsAreReady = false; 141 | break; 142 | } 143 | } 144 | if (!domainsAreReady) { 145 | return new Promise((resolve) => { 146 | setTimeout( 147 | () => resolve(this.waitDomainsAreDeployedContainer(containerId)), 148 | 5000 149 | ); 150 | }); 151 | } 152 | return domains; 153 | }); 154 | }, 155 | 156 | /** 157 | * listDomains is used to read all domains of a wanted container. 158 | * @param {Number} containerId the id of the container to read domains. 159 | * @returns a Promise with request result. 160 | */ 161 | listDomainsContainer(containerId) { 162 | const domainsUrl = `domains?container_id=${containerId}`; 163 | 164 | return this.apiManager 165 | .get(domainsUrl) 166 | .then((response) => response.data.domains) 167 | .catch(manageError); 168 | }, 169 | }; 170 | -------------------------------------------------------------------------------- /shared/api/functions.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { manageError } = require("./utils"); 4 | 5 | module.exports = { 6 | listFunctions(namespaceId) { 7 | const functionsUrl = `namespaces/${namespaceId}/functions`; 8 | return this.apiManager 9 | .get(functionsUrl) 10 | .then((response) => response.data.functions || []) 11 | .catch(manageError); 12 | }, 13 | 14 | createFunction(params) { 15 | return this.apiManager 16 | .post("functions", params) 17 | .then((response) => response.data) 18 | .catch(manageError); 19 | }, 20 | 21 | updateFunction(functionId, params) { 22 | const updateUrl = `functions/${functionId}`; 23 | return this.apiManager 24 | .patch(updateUrl, params) 25 | .then((response) => response.data) 26 | .catch(manageError); 27 | }, 28 | 29 | deployFunction(functionId, params) { 30 | return this.apiManager 31 | .post(`functions/${functionId}/deploy`, params) 32 | .then((response) => response.data) 33 | .catch(manageError); 34 | }, 35 | 36 | getPresignedUrl(functionId, archiveSize) { 37 | return this.apiManager 38 | .get(`functions/${functionId}/upload-url?content_length=${archiveSize}`) 39 | .then((response) => response.data) 40 | .catch(manageError); 41 | }, 42 | 43 | /** 44 | * Deletes the function by functionId 45 | * @param {UUID} functionId 46 | * @returns function with status deleting. 47 | */ 48 | deleteFunction(functionId) { 49 | return this.apiManager 50 | .delete(`functions/${functionId}`) 51 | .then((response) => response.data) 52 | .catch(manageError); 53 | }, 54 | 55 | /** 56 | * Get function information by functionId 57 | * @param {UUID} functionId 58 | * @returns function. 59 | */ 60 | getFunction(functionId) { 61 | return this.apiManager 62 | .get(`/functions/${functionId}`) 63 | .then((response) => response.data) 64 | .catch(manageError); 65 | }, 66 | 67 | waitFunctionsAreDeployed(namespaceId) { 68 | return this.listFunctions(namespaceId).then((functions) => { 69 | let functionsAreReady = true; 70 | for (let i = 0; i < functions.length; i += 1) { 71 | const func = functions[i]; 72 | if (func.status === "error") { 73 | throw new Error(func.error_message); 74 | } 75 | if (func.status !== "ready") { 76 | functionsAreReady = false; 77 | break; 78 | } 79 | } 80 | if (!functionsAreReady) { 81 | return new Promise((resolve) => { 82 | setTimeout( 83 | () => resolve(this.waitFunctionsAreDeployed(namespaceId)), 84 | 5000 85 | ); 86 | }); 87 | } 88 | return functions; 89 | }); 90 | }, 91 | 92 | /** 93 | * 94 | * @param {UUID} functionId id of the function to check 95 | * @param {String} wantedStatus wanted function status before leaving the wait status. 96 | * @returns 97 | */ 98 | waitForFunctionStatus(functionId, wantedStatus) { 99 | return this.getFunction(functionId) 100 | .then((func) => { 101 | if (func.status === "error") { 102 | throw new Error(func.name + ": " + func.error_message); 103 | } 104 | 105 | if (func.status !== wantedStatus) { 106 | return new Promise((resolve) => { 107 | setTimeout( 108 | () => 109 | resolve(this.waitForFunctionStatus(functionId, wantedStatus)), 110 | 5000 111 | ); 112 | }); 113 | } 114 | 115 | return func; 116 | }) 117 | .catch((err) => { 118 | // toleration on 4XX errors because on some status, for exemple deleting the API 119 | // will return a 404 err code if item has been deleted. 120 | if (err.response === undefined) { 121 | // if we have a raw Error 122 | throw err; 123 | } else if (err.response.status >= 500) { 124 | // if we have a CustomError, we can check the status 125 | throw new Error(err); 126 | } 127 | }); 128 | }, 129 | 130 | /** 131 | * listDomains is used to read all domains of a wanted function. 132 | * @param {Number} functionId the id of the function to read domains. 133 | * @returns a Promise with request result. 134 | */ 135 | listDomainsFunction(functionId) { 136 | const domainsUrl = `domains?function_id=${functionId}`; 137 | 138 | return this.apiManager 139 | .get(domainsUrl) 140 | .then((response) => response.data.domains) 141 | .catch(manageError); 142 | }, 143 | 144 | /** 145 | * Waiting for all domains to be ready on a function 146 | * @param {UUID} functionId 147 | * @returns 148 | */ 149 | waitDomainsAreDeployedFunction(functionId) { 150 | return this.listDomainsFunction(functionId).then((domains) => { 151 | let domainsAreReady = true; 152 | 153 | for (let i = 0; i < domains.length; i += 1) { 154 | const domain = domains[i]; 155 | 156 | if (domain.status === "error") { 157 | throw new Error(domain.error_message); 158 | } 159 | 160 | if (domain.status !== "ready") { 161 | domainsAreReady = false; 162 | break; 163 | } 164 | } 165 | if (!domainsAreReady) { 166 | return new Promise((resolve) => { 167 | setTimeout( 168 | () => resolve(this.waitDomainsAreDeployedFunction(functionId)), 169 | 5000 170 | ); 171 | }); 172 | } 173 | return domains; 174 | }); 175 | }, 176 | }; 177 | -------------------------------------------------------------------------------- /deploy/lib/buildAndPushContainers.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Docker = require("dockerode"); 4 | const path = require("path"); 5 | const fs = require("fs"); 6 | 7 | const docker = new Docker(); 8 | 9 | function extractStreamContents(stream, verbose) { 10 | return new Promise((resolve, reject) => { 11 | const streamContent = []; 12 | 13 | stream.on("data", (data) => { 14 | const streamData = data.toString().replace("\n", ""); 15 | streamContent.push(streamData); 16 | 17 | if (verbose) { 18 | console.log(streamData); 19 | } 20 | }); 21 | 22 | stream.on("end", () => { 23 | resolve(streamContent); 24 | }); 25 | stream.on("error", reject); 26 | }); 27 | } 28 | 29 | function findErrorInBuildOutput(buildOutput) { 30 | for (const buildStepLog of buildOutput) { 31 | if (buildStepLog.startsWith('{"errorDetail":{')) { 32 | let errorDetail; 33 | try { 34 | errorDetail = JSON.parse(buildStepLog)["errorDetail"]; 35 | } catch { 36 | return ""; 37 | } 38 | 39 | if (errorDetail !== undefined && errorDetail["message"] !== undefined) { 40 | return errorDetail["message"]; 41 | } 42 | 43 | return JSON.stringify(errorDetail); 44 | } 45 | } 46 | } 47 | 48 | function getFilesInBuildContextDirectory(directory) { 49 | let files = []; 50 | 51 | try { 52 | const dirents = fs.readdirSync(directory, { withFileTypes: true }); 53 | 54 | dirents.forEach((dirent) => { 55 | const absolutePath = path.join(directory, dirent.name); 56 | if (dirent.isDirectory()) { 57 | const subFiles = getFilesInBuildContextDirectory(absolutePath); 58 | 59 | // Prepend the current directory name to each subfile path 60 | const relativeSubFiles = subFiles.map((subFile) => 61 | path.join(dirent.name, subFile) 62 | ); 63 | files = files.concat(relativeSubFiles); 64 | } else if (dirent.isFile() && dirent.name !== ".dockerignore") { 65 | // Don't include .dockerignore file in result 66 | files.push(dirent.name); 67 | } 68 | }); 69 | } catch (err) { 70 | console.error(`Error reading directory ${directory}:`, err); 71 | } 72 | 73 | return files; 74 | } 75 | 76 | module.exports = { 77 | async buildAndPushContainers() { 78 | // used for pushing 79 | const auth = { 80 | username: "any", 81 | password: this.provider.scwToken, 82 | }; 83 | 84 | // used for building: see https://docs.docker.com/engine/api/v1.37/#tag/Image/operation/ImageBuild 85 | const registryAuth = {}; 86 | registryAuth["rg." + this.provider.scwRegion + ".scw.cloud"] = { 87 | username: "any", 88 | password: this.provider.scwToken, 89 | }; 90 | 91 | try { 92 | await docker.checkAuth(registryAuth); 93 | } catch (err) { 94 | throw new Error(`Docker error : ${err}`); 95 | } 96 | 97 | const containerNames = Object.keys(this.containers); 98 | const promises = containerNames.map((containerName) => { 99 | const container = this.containers[containerName]; 100 | if (container["directory"] === undefined) { 101 | return; 102 | } 103 | const imageName = `${this.namespace.registry_endpoint}/${container.name}:latest`; 104 | 105 | this.serverless.cli.log( 106 | `Building and pushing container ${container.name} to: ${imageName} ...` 107 | ); 108 | 109 | // eslint-disable-next-line no-async-promise-executor 110 | return new Promise(async (resolve, reject) => { 111 | let files = getFilesInBuildContextDirectory(container.directory); 112 | 113 | const buildStream = await docker.buildImage( 114 | { context: container.directory, src: files }, 115 | { 116 | t: imageName, 117 | registryconfig: registryAuth, 118 | } 119 | ); 120 | const buildStreamEvents = await extractStreamContents( 121 | buildStream, 122 | this.provider.options.verbose 123 | ); 124 | 125 | const buildError = findErrorInBuildOutput(buildStreamEvents); 126 | if (buildError !== undefined) { 127 | reject(`Build did not succeed, error: ${buildError}`); 128 | return; 129 | } 130 | 131 | const image = docker.getImage(imageName); 132 | 133 | const inspectedImage = await image 134 | .inspect() 135 | .catch(() => 136 | reject( 137 | `Image ${imageName} does not exist: run --verbose to see errors` 138 | ) 139 | ); 140 | 141 | if (inspectedImage === undefined) { 142 | return; 143 | } 144 | 145 | if (inspectedImage["Architecture"] !== "amd64") { 146 | reject( 147 | "It appears that image have been built with " + 148 | inspectedImage["Architecture"] + 149 | " architecture. " + 150 | "To build a compatible image with Scaleway serverless containers, " + 151 | "the platform of the built image must be `linux/amd64`. " + 152 | "Please pull your image's base image with platform `linux/amd64`: " + 153 | "first (`docker pull --platform=linux/amd64 `), " + 154 | "and just after, run `serverless deploy`. You shouldn't pull the other " + 155 | "image architecture between those two steps." 156 | ); 157 | return; 158 | } 159 | 160 | const pushStream = await image.push(auth); 161 | await extractStreamContents(pushStream, this.provider.options.verbose); 162 | 163 | resolve(); 164 | }); 165 | }); 166 | 167 | return Promise.all(promises); 168 | }, 169 | }; 170 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.4.18 4 | 5 | ### Fixed 6 | 7 | - Messaging (NATS/SQS) triggers now properly work with Serverless Containers #311 8 | 9 | ## 0.4.17 10 | 11 | ### Added 12 | 13 | - Added support for `private_network_id` #303 14 | 15 | ## 0.4.16 16 | 17 | ### Fixed 18 | 19 | - Honor `.dockerignore` when building a container #277 20 | 21 | ## 0.4.15 22 | 23 | ### Added 24 | 25 | - Added support for `nats` event source 26 | 27 | ## 0.4.14 28 | 29 | ### Added 30 | 31 | - Added `healthCheck` to define a health check for containers 32 | - Added `scalingOption` to allow scaling on concurrent requests, cpu usage or memory usage 33 | 34 | ### Fixed 35 | 36 | - Updating an existing function or container `sandbox` option was not working 37 | 38 | ### Changed 39 | 40 | - 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. 41 | 42 | ## 0.4.13 43 | 44 | ### Changed 45 | 46 | - HTTP calls to `api.scaleway.com` are now made with a custom user agent #245 47 | 48 | ## 0.4.12 49 | 50 | ### Fixed 51 | 52 | - Clarified documentation on currently supported Serverless Framework versions #213 53 | 54 | ### Added 55 | 56 | - Added option to configure `sandbox` for functions and containers #224 57 | 58 | ## 0.4.10 59 | 60 | ### Changed 61 | 62 | - Display a deprecation warning when running `serverless logs` command #212 63 | 64 | ## 0.4.9 65 | 66 | ### Fixed 67 | 68 | - Rate limit error when creating many functions at the same time #210 69 | 70 | ## 0.4.8 71 | 72 | ### Fixed 73 | 74 | - Error undefined directory field when creating a container from a registry image 75 | 76 | ## 0.4.7 77 | 78 | ### Added 79 | 80 | - Typescript example 81 | - Troubleshooting documentation 82 | - Allow to define image instead of building them 83 | - Using local testing packages in code samples 84 | - Flexible resource limits (vCPU / RAM) 85 | 86 | ### Fixed 87 | 88 | - Github actions for CI 89 | - Documentation 90 | 91 | ## 0.4.6 92 | 93 | ### Added 94 | 95 | - Local testing example for Go and Python #149 96 | 97 | ### Fixed 98 | 99 | - Error display on `serverless invoke` command #148 100 | - Timeout format in containe examples #145 101 | - Security deps #143 #144 102 | 103 | ## 0.4.5 104 | 105 | ### Added 106 | 107 | - `httpOption` parameter 108 | - Support for PHP runtime 109 | 110 | ### Fixed 111 | 112 | - Cron regex was different from console 113 | 114 | ## 0.4.4 115 | 116 | ### Added 117 | 118 | - Support for Rust files (`.rs`) 119 | 120 | ### Fixed 121 | 122 | - `js-yaml` dependency 123 | 124 | ## O.4.3 125 | 126 | ### Added 127 | 128 | - `description` field is now supported in serverless config files 129 | 130 | ### Fixed 131 | 132 | - Registry image is now forced by serverless framework to ensure consitency 133 | - Project_id added to requests to avoid multiple results if same namespace name is used 134 | - Clean documentaion and examples 135 | 136 | ## 0.4.2 137 | 138 | ### Added 139 | 140 | - Support for custom domains on containers 141 | - `maxConcurrency` parameter for containers 142 | - Support of pulling private images 143 | - More details on docker build errors 144 | - Support for End of Support and End of Life runtimes 145 | 146 | ### Fixed 147 | 148 | - Dependencies + code cleaning 149 | 150 | ## 0.4.1 151 | 152 | ### Added 153 | 154 | - clearer error messages when building a container with a different architecture than expected `amd64` [#95](https://github.com/scaleway/serverless-scaleway-functions/pull/95) 155 | 156 | ### Fixed 157 | 158 | - fix tests [#96](https://github.com/scaleway/serverless-scaleway-functions/pull/96) 159 | 160 | ## 0.4.0 161 | 162 | ### Added 163 | 164 | - `serverless info` command to work with serverless compose 165 | - `serverless invoke` command 166 | - Custom Domains support 167 | - `singleSource` parameter 168 | 169 | ### Changed 170 | 171 | - Documentation 172 | - Examples 173 | - Contributing guideline 174 | 175 | ## 0.3.2 176 | 177 | ### Fixed 178 | 179 | - `serverless jwt` command was using old jwt API 180 | 181 | ### Changed 182 | 183 | - Configuration files now have a default region instead of placeholder 184 | - Upgrade major version on outdated packages 185 | 186 | ## 0.3.1 187 | 188 | ### Added 189 | 190 | - Runtime validation using API 191 | - Runtimes can now be changed on update function 192 | 193 | ## 0.3.0 194 | 195 | ### Added 196 | 197 | - Runtimes are now listed from the Scaleway API, this allow faster releases without modyfiing serverless framework [#65](https://github.com/scaleway/serverless-scaleway-functions/pull/65) 198 | - Constants for runtime availability [#65](https://github.com/scaleway/serverless-scaleway-functions/pull/65) 199 | - API : function to list all runtimes [#65](https://github.com/scaleway/serverless-scaleway-functions/pull/65) 200 | - Support secret environement variables [#64](https://github.com/scaleway/serverless-scaleway-functions/pull/64) 201 | 202 | ### Fixed 203 | 204 | - Tests are now working properly [#69](https://github.com/scaleway/serverless-scaleway-functions/pull/69) 205 | - js-yaml usage fix for tests [#69](https://github.com/scaleway/serverless-scaleway-functions/pull/69) 206 | 207 | ## 0.2.8 208 | 209 | ### Added 210 | 211 | - Multi region support [#62](https://github.com/scaleway/serverless-scaleway-functions/pull/62) 212 | - Support for new environment variables `SCW_SECRET_KEY` and `SCW_DEFAULT_PROJECT_ID` [#61](https://github.com/scaleway/serverless-scaleway-functions/pull/61) 213 | - Region parameter in examples [#62](https://github.com/scaleway/serverless-scaleway-functions/pull/62) 214 | 215 | ### Fixed 216 | 217 | - Integration tests now use proper login API [#62](https://github.com/scaleway/serverless-scaleway-functions/pull/62) 218 | - **Regression** could not create Go functions [#67](https://github.com/scaleway/serverless-scaleway-functions/pull/67) 219 | 220 | --- 221 | 222 | ### Changelog notice 223 | 224 | All notable changes to this project will be documented in this file. 225 | 226 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 227 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 228 | -------------------------------------------------------------------------------- /tests/shared/secrets.test.js: -------------------------------------------------------------------------------- 1 | const { 2 | expect: jestExpect, 3 | describe, 4 | it, 5 | beforeEach, 6 | } = require("@jest/globals"); 7 | const argon2 = require("argon2"); 8 | const secrets = require("../../shared/secrets"); 9 | 10 | describe("convertObjectToModelSecretsArray", () => { 11 | it("should transform an object to a secrets array as the API expects", () => { 12 | const actual = secrets.convertObjectToModelSecretsArray({ 13 | env_secretA: "valueA", 14 | env_secretB: "valueB", 15 | }); 16 | const expected = [ 17 | { key: "env_secretA", value: "valueA" }, 18 | { key: "env_secretB", value: "valueB" }, 19 | ]; 20 | jestExpect(actual).toEqual(expected); 21 | }); 22 | 23 | it("should transform an empty object to an empty array", () => { 24 | const actual = secrets.convertObjectToModelSecretsArray({}); 25 | const expected = []; 26 | jestExpect(actual).toEqual(expected); 27 | }); 28 | }); 29 | 30 | describe("resolveSecretValue", () => { 31 | beforeEach(() => { 32 | const consoleSpy = jest.spyOn(console, "log").mockImplementation(); 33 | consoleSpy.mockClear(); 34 | }); 35 | 36 | it("should get a raw secret", () => { 37 | const actual = secrets.resolveSecretValue("env_secretA", "valueA", console); 38 | const expected = "valueA"; 39 | jestExpect(actual).toEqual(expected); 40 | jestExpect(console.log).toHaveBeenCalledTimes(0); 41 | }); 42 | 43 | it("should get a raw secret with special characters", () => { 44 | const actual = secrets.resolveSecretValue( 45 | "env_secretA", 46 | "value composed of special characters $^/;", 47 | console 48 | ); 49 | const expected = "value composed of special characters $^/;"; 50 | jestExpect(actual).toEqual(expected); 51 | jestExpect(console.log).toHaveBeenCalledTimes(0); 52 | }); 53 | 54 | it("should get a secret from an environment variable", () => { 55 | const OLD_ENV = process.env; 56 | process.env.ENV_SECRETA = "valueA"; 57 | 58 | const actual = secrets.resolveSecretValue( 59 | "env_secretA", 60 | "${ENV_SECRETA}", 61 | console 62 | ); 63 | process.env = OLD_ENV; 64 | 65 | const expected = process.env.ENV_SECRETA; 66 | jestExpect(actual).toEqual(expected); 67 | jestExpect(console.log).toHaveBeenCalledTimes(0); 68 | }); 69 | 70 | it("should get a secret with empty value from an environment variable", () => { 71 | const OLD_ENV = process.env; 72 | process.env.ENV_SECRETA = ""; 73 | 74 | const actual = secrets.resolveSecretValue( 75 | "env_secretA", 76 | "${ENV_SECRETA}", 77 | console 78 | ); 79 | process.env = OLD_ENV; 80 | 81 | const expected = process.env.ENV_SECRETA; 82 | jestExpect(actual).toEqual(expected); 83 | jestExpect(console.log).toHaveBeenCalledTimes(0); 84 | }); 85 | 86 | it("should return null if environment variable does not exist", () => { 87 | delete process.env.ENV_SECRETA; 88 | const actual = secrets.resolveSecretValue( 89 | "env_secretA", 90 | "${ENV_SECRETA}", 91 | console 92 | ); 93 | 94 | const expected = null; 95 | jestExpect(actual).toEqual(expected); 96 | 97 | jestExpect(console.log).toHaveBeenCalledTimes(1); 98 | jestExpect(console.log).toHaveBeenLastCalledWith( 99 | "WARNING: Env var ENV_SECRETA used in secret env_secretA does not exist: this secret will not be created" 100 | ); 101 | }); 102 | }); 103 | 104 | describe("mergeSecretEnvVars", () => { 105 | beforeEach(() => { 106 | const consoleSpy = jest.spyOn(console, "log").mockImplementation(); 107 | consoleSpy.mockClear(); 108 | }); 109 | 110 | it("should add a secret env var", async () => { 111 | const existingSecretEnvVars = []; 112 | const newSecretEnvVars = [{ key: "env_secretA", value: "valueA" }]; 113 | 114 | const actual = await secrets.mergeSecretEnvVars( 115 | existingSecretEnvVars, 116 | newSecretEnvVars, 117 | console 118 | ); 119 | const expected = [{ key: "env_secretA", value: "valueA" }]; 120 | jestExpect(actual).toEqual(expected); 121 | jestExpect(console.log).toHaveBeenCalledTimes(0); 122 | }); 123 | 124 | it("should update a secret env var", async () => { 125 | const valueAHash = await argon2.hash("valueA", { type: argon2.argon2id }); 126 | const existingSecretEnvVars = [ 127 | { key: "env_secretA", hashed_value: valueAHash }, 128 | ]; 129 | const newSecretEnvVars = [{ key: "env_secretA", value: "newValueA" }]; 130 | 131 | const actual = await secrets.mergeSecretEnvVars( 132 | existingSecretEnvVars, 133 | newSecretEnvVars, 134 | console 135 | ); 136 | const expected = [{ key: "env_secretA", value: "newValueA" }]; 137 | jestExpect(actual).toEqual(expected); 138 | jestExpect(console.log).toHaveBeenCalledTimes(0); 139 | }); 140 | 141 | it("should delete a secret env var", async () => { 142 | const valueAHash = await argon2.hash("valueA", { type: argon2.argon2id }); 143 | const existingSecretEnvVars = [ 144 | { key: "env_secretA", hashed_value: valueAHash }, 145 | ]; 146 | const newSecretEnvVars = []; 147 | 148 | const actual = await secrets.mergeSecretEnvVars( 149 | existingSecretEnvVars, 150 | newSecretEnvVars, 151 | console 152 | ); 153 | const expected = [{ key: "env_secretA", value: null }]; 154 | jestExpect(actual).toEqual(expected); 155 | jestExpect(console.log).toHaveBeenCalledTimes(0); 156 | }); 157 | 158 | it("should add, update and delete secret env vars", async () => { 159 | const valueAHash = await argon2.hash("valueA", { type: argon2.argon2id }); 160 | const valueBHash = await argon2.hash("valueB", { type: argon2.argon2id }); 161 | const existingSecretEnvVars = [ 162 | { key: "env_secretA", hashed_value: valueAHash }, 163 | { key: "env_secretB", hashed_value: valueBHash }, 164 | ]; 165 | const newSecretEnvVars = [ 166 | { key: "env_secretA", value: "newValueA" }, // update 167 | { key: "env_secretC", value: "valueC" }, // add 168 | // env_secretB is deleted 169 | ]; 170 | 171 | const actual = await secrets.mergeSecretEnvVars( 172 | existingSecretEnvVars, 173 | newSecretEnvVars, 174 | console 175 | ); 176 | const expected = [ 177 | { key: "env_secretA", value: "newValueA" }, 178 | { key: "env_secretB", value: null }, 179 | { key: "env_secretC", value: "valueC" }, 180 | ]; 181 | jestExpect(actual).toEqual(expected); 182 | jestExpect(console.log).toHaveBeenCalledTimes(0); 183 | }); 184 | }); 185 | -------------------------------------------------------------------------------- /tests/containers/containers.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Docker = require("dockerode"); 4 | const fs = require("fs"); 5 | const path = require("path"); 6 | 7 | const { afterAll, beforeAll, describe, it, expect } = require("@jest/globals"); 8 | 9 | const { getTmpDirPath, replaceTextInFile } = require("../utils/fs"); 10 | const { 11 | getServiceName, 12 | sleep, 13 | serverlessDeploy, 14 | serverlessInvoke, 15 | serverlessRemove, 16 | createProject, 17 | } = require("../utils/misc"); 18 | const { ContainerApi } = require("../../shared/api"); 19 | const { execSync } = require("../../shared/child-process"); 20 | const { CONTAINERS_API_URL } = require("../../shared/constants"); 21 | const { removeProjectById } = require("../utils/clean-up"); 22 | 23 | const serverlessExec = path.join("serverless"); 24 | 25 | describe("Service Lifecyle Integration Test", () => { 26 | const scwRegion = process.env.SCW_REGION; 27 | const scwToken = process.env.SCW_SECRET_KEY; 28 | const apiUrl = `${CONTAINERS_API_URL}/${scwRegion}`; 29 | const templateName = path.resolve( 30 | __dirname, 31 | "..", 32 | "..", 33 | "examples", 34 | "container" 35 | ); 36 | const tmpDir = getTmpDirPath(); 37 | 38 | let options = {}; 39 | options.env = {}; 40 | options.env.SCW_SECRET_KEY = scwToken; 41 | options.env.SCW_REGION = scwRegion; 42 | 43 | let oldCwd, serviceName, projectId, api, namespace, containerName; 44 | const descriptionTest = "slsfw test description"; 45 | 46 | beforeAll(async () => { 47 | oldCwd = process.cwd(); 48 | serviceName = getServiceName(); 49 | api = new ContainerApi(apiUrl, scwToken); 50 | await createProject() 51 | .then((project) => { 52 | projectId = project.id; 53 | }) 54 | .catch((err) => console.error(err)); 55 | options.env.SCW_DEFAULT_PROJECT_ID = projectId; 56 | }); 57 | 58 | afterAll(async () => { 59 | await removeProjectById(projectId).catch((err) => console.error(err)); 60 | }); 61 | 62 | it("should create service in tmp directory", () => { 63 | execSync( 64 | `${serverlessExec} create --template-path ${templateName} --path ${tmpDir}` 65 | ); 66 | process.chdir(tmpDir); 67 | execSync(`npm link ${oldCwd}`); 68 | replaceTextInFile("serverless.yml", "scaleway-container", serviceName); 69 | replaceTextInFile( 70 | "serverless.yml", 71 | '# description: ""', 72 | `description: "${descriptionTest}"` 73 | ); 74 | expect(fs.existsSync(path.join(tmpDir, "serverless.yml"))).toBe(true); 75 | expect(fs.existsSync(path.join(tmpDir, "my-container"))).toBe(true); 76 | }); 77 | 78 | it("should deploy service/container to scaleway", async () => { 79 | serverlessDeploy(options); 80 | namespace = await api 81 | .getNamespaceFromList(serviceName, projectId) 82 | .catch((err) => console.error(err)); 83 | namespace.containers = await api 84 | .listContainers(namespace.id) 85 | .catch((err) => console.error(err)); 86 | expect(namespace.containers[0].description).toBe(descriptionTest); 87 | containerName = namespace.containers[0].name; 88 | }); 89 | 90 | it("should replace container image with test image", async () => { 91 | // This tests will push a dummy image to the same namespace of the deployed 92 | // container. And then it will modify the image through the API. 93 | // After that we run a serverless deploy to ensure the container image 94 | // is NOT the dummy image. 95 | 96 | // build a new image with same path but different name for testing. 97 | const regImg = namespace.containers[0].registry_image; 98 | const contName = namespace.containers[0].name; 99 | const imageName = regImg.replace(contName, "test-container"); 100 | 101 | const docker = new Docker(); 102 | 103 | // used for pushing 104 | const auth = { 105 | username: "any", 106 | password: scwToken, 107 | }; 108 | 109 | const regEndpoint = `rg.${scwRegion}.scw.cloud`; 110 | const registryAuth = {}; 111 | registryAuth[regEndpoint] = auth; 112 | 113 | await docker.checkAuth(registryAuth); 114 | 115 | await docker.buildImage( 116 | { 117 | context: path.join(tmpDir, "my-container"), 118 | src: ["Dockerfile", "server.py", "requirements.txt"], 119 | }, 120 | { 121 | t: imageName, 122 | registryconfig: registryAuth, 123 | } 124 | ); 125 | const image = docker.getImage(imageName); 126 | await image.push(auth); 127 | 128 | // registry lag 129 | await sleep(60000); 130 | 131 | const params = { 132 | redeploy: false, 133 | registry_image: imageName, 134 | }; 135 | await api 136 | .updateContainer(namespace.containers[0].id, params) 137 | .catch((err) => console.error(err)); 138 | 139 | const nsContainers = await api 140 | .listContainers(namespace.id) 141 | .catch((err) => console.error(err)); 142 | expect(nsContainers[0].registry_image).toBe(imageName); 143 | 144 | serverlessDeploy(options); 145 | 146 | const nsContainersAfterSlsDeploy = await api 147 | .listContainers(namespace.id) 148 | .catch((err) => console.error(err)); 149 | expect(nsContainersAfterSlsDeploy[0].registry_image).not.toContain( 150 | "test-container" 151 | ); 152 | }); 153 | 154 | it("should invoke container from scaleway", async () => { 155 | await api 156 | .waitContainersAreDeployed(namespace.id) 157 | .catch((err) => console.error(err)); 158 | options.serviceName = containerName; 159 | const output = serverlessInvoke(options).toString(); 160 | expect(output).toBe('{"message":"Hello, World from Scaleway Container !"}'); 161 | }); 162 | 163 | it("should deploy updated service/container to scaleway", () => { 164 | replaceTextInFile( 165 | "my-container/server.py", 166 | "Hello, World from Scaleway Container !", 167 | "Container successfully updated" 168 | ); 169 | serverlessDeploy(options); 170 | }); 171 | 172 | it("should invoke updated container from scaleway", async () => { 173 | await api 174 | .waitContainersAreDeployed(namespace.id) 175 | .catch((err) => console.error(err)); 176 | const output = serverlessInvoke(options).toString(); 177 | expect(output).toBe('{"message":"Container successfully updated"}'); 178 | }); 179 | 180 | it("should deploy with registry image specified", () => { 181 | replaceTextInFile( 182 | "serverless.yml", 183 | '# registryImage: ""', 184 | "registryImage: docker.io/library/nginx:latest" 185 | ); 186 | replaceTextInFile("serverless.yml", "# port: 8080", "port: 80"); 187 | serverlessDeploy(options); 188 | }); 189 | 190 | it("should invoke updated container with specified registry image", async () => { 191 | await sleep(30000); 192 | options.serviceName = containerName; 193 | const output = serverlessInvoke(options).toString(); 194 | expect(output).toContain("Welcome to nginx!"); 195 | }); 196 | 197 | it("should remove service from scaleway", async () => { 198 | serverlessRemove(options); 199 | try { 200 | await api.getNamespace(namespace.id); 201 | } catch (err) { 202 | expect(err.response.status).toBe(404); 203 | } 204 | }); 205 | 206 | // TODO: handle error at validation time 207 | // ATM, error is thrown when trying to build the image because the directory is not found, 208 | // instead, we should check at validation time if the directory exists (if not, we create 209 | // a namespace resource for nothing, preventing to delete the project afterwards) 210 | /*it('should throw error container directory not found', () => { 211 | replaceTextInFile('serverless.yml', 'my-container', 'doesnotexist'); 212 | try { 213 | expect(serverlessDeploy(options)).rejects.toThrow(Error); 214 | } catch (err) { 215 | // if not try catch, test would fail 216 | } 217 | });*/ 218 | }); 219 | -------------------------------------------------------------------------------- /deploy/lib/createContainers.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const BbPromise = require("bluebird"); 4 | const singleSource = require("../../shared/singleSource"); 5 | const secrets = require("../../shared/secrets"); 6 | const domainUtils = require("../../shared/domains"); 7 | 8 | const maxConcurrencyDeprecationWarning = `WARNING: maxConcurrency is deprecated and has been replaced by scalingOption of type: concurrentRequests. 9 | Please update your serverless.yml file.`; 10 | 11 | function adaptHealthCheckToAPI(healthCheck) { 12 | if (!healthCheck) { 13 | return null; 14 | } 15 | 16 | // We need to find the type of the health check (tcp, http, ...) 17 | // If httpPath is provided, we default to http, otherwise we default to tcp 18 | let type = healthCheck.httpPath ? "http" : "tcp"; 19 | if (healthCheck.type) { 20 | type = healthCheck.type; 21 | } 22 | 23 | return { 24 | failure_threshold: healthCheck.failureThreshold, 25 | interval: healthCheck.interval, 26 | ...(type === "http" && { http: { path: healthCheck.httpPath || "/" } }), 27 | ...(type === "tcp" && { tcp: {} }), 28 | }; 29 | } 30 | 31 | const scalingOptionToAPIProperty = { 32 | concurrentRequests: "concurrent_requests_threshold", 33 | cpuUsage: "cpu_usage_threshold", 34 | memoryUsage: "memory_usage_threshold", 35 | }; 36 | 37 | function adaptScalingOptionToAPI(scalingOption) { 38 | if (!scalingOption || !scalingOption.type) { 39 | return null; 40 | } 41 | 42 | const property = scalingOptionToAPIProperty[scalingOption.type]; 43 | if (!property) { 44 | throw new Error( 45 | `scalingOption.type must be one of: ${Object.keys( 46 | scalingOptionToAPIProperty 47 | ).join(", ")}` 48 | ); 49 | } 50 | 51 | return { 52 | [property]: scalingOption.threshold, 53 | }; 54 | } 55 | 56 | module.exports = { 57 | createContainers() { 58 | return BbPromise.bind(this) 59 | .then(() => this.listContainers(this.namespace.id)) 60 | .then(this.createOrUpdateContainers); 61 | }, 62 | 63 | deleteContainersByIds(containersIdsToDelete) { 64 | containersIdsToDelete.forEach((containerIdToDelete) => { 65 | this.deleteContainer(containerIdToDelete).then((res) => { 66 | this.serverless.cli.log( 67 | `Container ${res.name} removed from config file, deleting it...` 68 | ); 69 | this.waitForContainerStatus(containerIdToDelete, "deleted").then( 70 | this.serverless.cli.log(`Container ${res.name} deleted`) 71 | ); 72 | }); 73 | }); 74 | }, 75 | 76 | applyDomainsContainer(containerId, customDomains) { 77 | this.listDomainsContainer(containerId).then((domains) => { 78 | const existingDomains = domainUtils.formatDomainsStructure(domains); 79 | const domainsToCreate = domainUtils.getDomainsToCreate( 80 | customDomains, 81 | existingDomains 82 | ); 83 | const domainsIdToDelete = domainUtils.getDomainsToDelete( 84 | customDomains, 85 | existingDomains 86 | ); 87 | 88 | domainsToCreate.forEach((newDomain) => { 89 | const createDomainParams = { 90 | container_id: containerId, 91 | hostname: newDomain, 92 | }; 93 | 94 | this.createDomainAndLog(createDomainParams); 95 | }); 96 | 97 | domainsIdToDelete.forEach((domainId) => { 98 | this.deleteDomain(domainId).then((res) => { 99 | this.serverless.cli.log(`Deleting domain ${res.hostname}`); 100 | }); 101 | }); 102 | }); 103 | }, 104 | 105 | createOrUpdateContainers(foundContainers) { 106 | const { containers } = this.provider.serverless.service.custom; 107 | 108 | const deleteData = singleSource.getElementsToDelete( 109 | this.serverless.configurationInput.singleSource, 110 | foundContainers, 111 | Object.keys(containers) 112 | ); 113 | 114 | this.deleteContainersByIds(deleteData.elementsIdsToRemove); 115 | 116 | const promises = deleteData.serviceNamesRet.map((containerName) => { 117 | const container = Object.assign(containers[containerName], { 118 | name: containerName, 119 | }); 120 | 121 | const foundContainer = foundContainers.find( 122 | (c) => c.name === container.name 123 | ); 124 | 125 | return foundContainer 126 | ? this.updateSingleContainer(container, foundContainer) 127 | : this.createSingleContainer(container); 128 | }); 129 | 130 | return Promise.all(promises).then((updatedContainers) => { 131 | this.containers = updatedContainers; 132 | }); 133 | }, 134 | 135 | createSingleContainer(container) { 136 | const params = { 137 | name: container.name, 138 | environment_variables: container.env, 139 | secret_environment_variables: secrets.convertObjectToModelSecretsArray( 140 | container.secret 141 | ), 142 | namespace_id: this.namespace.id, 143 | description: container.description, 144 | memory_limit: container.memoryLimit, 145 | cpu_limit: container.cpuLimit, 146 | min_scale: container.minScale, 147 | max_scale: container.maxScale, 148 | registry_image: container.registryImage, 149 | max_concurrency: container.maxConcurrency, 150 | timeout: container.timeout, 151 | privacy: container.privacy, 152 | port: container.port, 153 | http_option: container.httpOption, 154 | sandbox: container.sandbox, 155 | health_check: adaptHealthCheckToAPI(container.healthCheck), 156 | scaling_option: adaptScalingOptionToAPI(container.scalingOption), 157 | private_network_id: container.privateNetworkId, 158 | }; 159 | 160 | // checking if there is custom_domains set on container creation. 161 | if (container.custom_domains && container.custom_domains.length > 0) { 162 | this.serverless.cli.log( 163 | "WARNING: custom_domains are available on container update only. " + 164 | "Redeploy your container to apply custom domains. Doc : https://www.scaleway.com/en/docs/compute/containers/how-to/add-a-custom-domain-to-a-container/" 165 | ); 166 | } 167 | 168 | // note about maxConcurrency deprecation 169 | if (container.maxConcurrency) { 170 | this.serverless.cli.log(maxConcurrencyDeprecationWarning); 171 | } 172 | 173 | this.serverless.cli.log(`Creating container ${container.name}...`); 174 | 175 | return this.createContainer(params).then((response) => 176 | Object.assign(response, { directory: container.directory }) 177 | ); 178 | }, 179 | 180 | async updateSingleContainer(container, foundContainer) { 181 | let privateNetworkId = container.privateNetworkId; 182 | const hasToDeletePrivateNetwork = 183 | foundContainer.private_network_id && !container.privateNetworkId; 184 | if (hasToDeletePrivateNetwork) { 185 | privateNetworkId = ""; 186 | } 187 | 188 | const params = { 189 | redeploy: false, 190 | environment_variables: container.env, 191 | secret_environment_variables: await secrets.mergeSecretEnvVars( 192 | foundContainer.secret_environment_variables, 193 | secrets.convertObjectToModelSecretsArray(container.secret), 194 | this.serverless.cli 195 | ), 196 | description: container.description, 197 | memory_limit: container.memoryLimit, 198 | cpu_limit: container.cpuLimit, 199 | min_scale: container.minScale, 200 | max_scale: container.maxScale, 201 | registry_image: container.registryImage 202 | ? container.registryImage 203 | : `${this.namespace.registry_endpoint}/${container.name}:latest`, 204 | max_concurrency: container.maxConcurrency, 205 | timeout: container.timeout, 206 | privacy: container.privacy, 207 | port: container.port, 208 | http_option: container.httpOption, 209 | sandbox: container.sandbox, 210 | health_check: adaptHealthCheckToAPI(container.healthCheck), 211 | scaling_option: adaptScalingOptionToAPI(container.scalingOption), 212 | private_network_id: privateNetworkId, 213 | }; 214 | 215 | // note about maxConcurrency deprecation 216 | if (container.maxConcurrency) { 217 | this.serverless.cli.log(maxConcurrencyDeprecationWarning); 218 | } 219 | 220 | this.serverless.cli.log(`Updating container ${container.name}...`); 221 | 222 | // assign domains 223 | this.applyDomainsContainer(foundContainer.id, container.custom_domains); 224 | 225 | return this.updateContainer(foundContainer.id, params).then((response) => 226 | Object.assign(response, { directory: container.directory }) 227 | ); 228 | }, 229 | }; 230 | --------------------------------------------------------------------------------