├── 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 |
--------------------------------------------------------------------------------