@faable/faable 0.0.0-dev
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -0
- package/bin/faable.js +3 -0
- package/dist/api/FaableApi.js +88 -0
- package/dist/api/base_client.js +11 -0
- package/dist/api/context.js +30 -0
- package/dist/api/strategies/apikey.strategy.js +15 -0
- package/dist/api/strategies/oidc.strategy.js +31 -0
- package/dist/commands/apps/index.js +17 -0
- package/dist/commands/configure/index.js +34 -0
- package/dist/commands/deploy/check_environment.js +13 -0
- package/dist/commands/deploy/deploy_command.js +50 -0
- package/dist/commands/deploy/index.js +22 -0
- package/dist/commands/deploy/node-pipeline/analyze_package.js +34 -0
- package/dist/commands/deploy/node-pipeline/build_docker.js +48 -0
- package/dist/commands/deploy/node-pipeline/build_project.js +26 -0
- package/dist/commands/deploy/node-pipeline/index.js +31 -0
- package/dist/commands/deploy/node-pipeline/templates/Dockerfile +18 -0
- package/dist/commands/deploy/node-pipeline/templates/entrypoint.sh +8 -0
- package/dist/commands/deploy/runtime-detect/helpers/has_any_of_files.js +14 -0
- package/dist/commands/deploy/runtime-detect/runtime_detection.js +19 -0
- package/dist/commands/deploy/runtime-detect/strategies/docker.js +9 -0
- package/dist/commands/deploy/runtime-detect/strategies/nodejs.js +49 -0
- package/dist/commands/deploy/upload_tag.js +22 -0
- package/dist/commands/init/index.js +22 -0
- package/dist/commands/init/writeGithubAction.js +108 -0
- package/dist/config.js +10 -0
- package/dist/index.js +48 -0
- package/dist/lib/Configuration.js +38 -0
- package/dist/lib/CredentialsStore.js +36 -0
- package/dist/lib/cmd.js +40 -0
- package/dist/log.js +13 -0
- package/package.json +38 -0
package/README.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="https://faable.com">
|
|
3
|
+
<img src="https://www.faable.com/assets/logo/Emblem.png" height="96">
|
|
4
|
+
<h3 align="center">Faable</h3>
|
|
5
|
+
</a>
|
|
6
|
+
</p>
|
|
7
|
+
|
|
8
|
+
<p align="center">
|
|
9
|
+
Your React, Node.js or Python apps, up to the cloud in seconds.
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
## Faable
|
|
13
|
+
|
|
14
|
+
Faable is the best platform to build modern architectures that scale precisely to meet demand. We handle the hard stuff so you can focus on building cloud ready apps. Make your business cloud driven and join those awesome companies.
|
|
15
|
+
|
|
16
|
+
To install the latest version of Faable CLI:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm i -g @faable/faable
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Documentation
|
|
23
|
+
|
|
24
|
+
For details on how to use Faable CLI, check out our [documentation](https://docs.faable.com/).
|
package/bin/faable.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { __decorate } from 'tslib';
|
|
2
|
+
import { create_base_client } from './base_client.js';
|
|
3
|
+
|
|
4
|
+
function handleError() {
|
|
5
|
+
return function (target, propertyKey, descriptor) {
|
|
6
|
+
const method = descriptor.value;
|
|
7
|
+
descriptor.value = async function (...args) {
|
|
8
|
+
try {
|
|
9
|
+
return await method.bind(this).apply(target, args);
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
const e = error;
|
|
13
|
+
if (e.isAxiosError) {
|
|
14
|
+
const res = e.response;
|
|
15
|
+
if (res) {
|
|
16
|
+
throw new Error(`FaableApi ${e.config.url} ${res.status}: ${res?.data.message}`, { cause: error });
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
throw new Error(`FaableApi ${e.message}`, { cause: error });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
const firstPage = async (res) => {
|
|
28
|
+
const items = (await res).results;
|
|
29
|
+
return items;
|
|
30
|
+
};
|
|
31
|
+
const data = async (res) => {
|
|
32
|
+
const items = (await res).data;
|
|
33
|
+
return items;
|
|
34
|
+
};
|
|
35
|
+
class FaableApi {
|
|
36
|
+
client;
|
|
37
|
+
constructor(config) {
|
|
38
|
+
const { authStrategy, auth } = config;
|
|
39
|
+
this.client = create_base_client();
|
|
40
|
+
const strategy = authStrategy && authStrategy(auth);
|
|
41
|
+
this.client.interceptors.request.use(async function (config) {
|
|
42
|
+
// Do something before request is sent
|
|
43
|
+
const headers = strategy ? await strategy.headers() : {};
|
|
44
|
+
config.headers.set(headers);
|
|
45
|
+
// console.log("all headers");
|
|
46
|
+
// console.log(headers);
|
|
47
|
+
return config;
|
|
48
|
+
}, function (error) {
|
|
49
|
+
// Do something with request error
|
|
50
|
+
return Promise.reject(error);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
static create(config = {}) {
|
|
54
|
+
return new FaableApi(config);
|
|
55
|
+
}
|
|
56
|
+
async list() {
|
|
57
|
+
return firstPage(data(this.client.get(`/app`)));
|
|
58
|
+
}
|
|
59
|
+
async getBySlug(slug) {
|
|
60
|
+
return data(this.client.get(`/app/slug/${slug}`));
|
|
61
|
+
}
|
|
62
|
+
async getRegistry(app_id) {
|
|
63
|
+
return data(this.client.get(`/app/${app_id}/registry`));
|
|
64
|
+
}
|
|
65
|
+
async createDeployment(params) {
|
|
66
|
+
return data(this.client.post(`/deployment`, params));
|
|
67
|
+
}
|
|
68
|
+
async getAppSecrets(app_id) {
|
|
69
|
+
return firstPage(data(this.client.get(`/secret/${app_id}`)));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
__decorate([
|
|
73
|
+
handleError()
|
|
74
|
+
], FaableApi.prototype, "list", null);
|
|
75
|
+
__decorate([
|
|
76
|
+
handleError()
|
|
77
|
+
], FaableApi.prototype, "getBySlug", null);
|
|
78
|
+
__decorate([
|
|
79
|
+
handleError()
|
|
80
|
+
], FaableApi.prototype, "getRegistry", null);
|
|
81
|
+
__decorate([
|
|
82
|
+
handleError()
|
|
83
|
+
], FaableApi.prototype, "createDeployment", null);
|
|
84
|
+
__decorate([
|
|
85
|
+
handleError()
|
|
86
|
+
], FaableApi.prototype, "getAppSecrets", null);
|
|
87
|
+
|
|
88
|
+
export { FaableApi };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { FaableApi } from './FaableApi.js';
|
|
2
|
+
import { apikey_strategy } from './strategies/apikey.strategy.js';
|
|
3
|
+
import { getIDToken } from '@actions/core';
|
|
4
|
+
import { oidc_strategy } from './strategies/oidc.strategy.js';
|
|
5
|
+
|
|
6
|
+
// import { CredentialsStore } from "../lib/CredentialsStore";
|
|
7
|
+
const context = async () => {
|
|
8
|
+
let api;
|
|
9
|
+
if (process.env.FAABLE_APIKEY) {
|
|
10
|
+
const apikey = process.env.FAABLE_APIKEY;
|
|
11
|
+
api = FaableApi.create({ authStrategy: apikey_strategy, auth: { apikey } });
|
|
12
|
+
}
|
|
13
|
+
//const store = new CredentialsStore();
|
|
14
|
+
//(await store.loadCredentials())?.apikey
|
|
15
|
+
// Github actions environment
|
|
16
|
+
if (process.env.GITHUB_ACTIONS === 'true') {
|
|
17
|
+
try {
|
|
18
|
+
const idToken = process.env.FAABLE_ID_TOKEN || await getIDToken("https://faable.com");
|
|
19
|
+
api = FaableApi.create({ authStrategy: oidc_strategy, auth: { idToken } });
|
|
20
|
+
}
|
|
21
|
+
catch (_) {
|
|
22
|
+
console.error("Error fetching token, configure 'permissions: id-token: write'");
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
api,
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export { context };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const apikey_strategy = (config) => {
|
|
2
|
+
const { apikey } = config;
|
|
3
|
+
if (!apikey) {
|
|
4
|
+
throw new Error("Missing apikey.");
|
|
5
|
+
}
|
|
6
|
+
return {
|
|
7
|
+
headers: async () => {
|
|
8
|
+
return {
|
|
9
|
+
Authorization: `Basic ${Buffer.from(`${apikey}:`).toString("base64")}`,
|
|
10
|
+
};
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export { apikey_strategy };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { create_base_client } from '../base_client.js';
|
|
2
|
+
|
|
3
|
+
const exchangeGithubOidcToken = async (gh_token) => {
|
|
4
|
+
const client = create_base_client();
|
|
5
|
+
const res = await client.post("/auth/github-oidc", {
|
|
6
|
+
token: gh_token
|
|
7
|
+
});
|
|
8
|
+
const { token } = res.data;
|
|
9
|
+
console.log("Obtained github token exchange");
|
|
10
|
+
console.log(token);
|
|
11
|
+
return token;
|
|
12
|
+
};
|
|
13
|
+
const oidc_strategy = (config) => {
|
|
14
|
+
const { idToken } = config;
|
|
15
|
+
if (!idToken) {
|
|
16
|
+
throw new Error("Missing idToken.");
|
|
17
|
+
}
|
|
18
|
+
let token = "";
|
|
19
|
+
return {
|
|
20
|
+
headers: async () => {
|
|
21
|
+
if (!token) {
|
|
22
|
+
token = await exchangeGithubOidcToken(idToken);
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
Authorization: `Bearer ${token}`,
|
|
26
|
+
};
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export { oidc_strategy };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { context } from '../../api/context.js';
|
|
2
|
+
|
|
3
|
+
const apps = {
|
|
4
|
+
command: "apps",
|
|
5
|
+
describe: "Manage Faable Cloud Apps",
|
|
6
|
+
builder: (yargs) => {
|
|
7
|
+
return yargs.showHelpOnFail(false);
|
|
8
|
+
},
|
|
9
|
+
handler: async (args) => {
|
|
10
|
+
const { api } = await context();
|
|
11
|
+
const apps = await api.list();
|
|
12
|
+
console.log("Apps...");
|
|
13
|
+
console.log(apps.map((app) => app.name));
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export { apps };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { CredentialsStore } from '../../lib/CredentialsStore.js';
|
|
2
|
+
import prompts from 'prompts';
|
|
3
|
+
|
|
4
|
+
const configure = {
|
|
5
|
+
command: "configure",
|
|
6
|
+
describe: "Configure Faable CLI",
|
|
7
|
+
builder: (yargs) => {
|
|
8
|
+
return yargs
|
|
9
|
+
.option("remove", {
|
|
10
|
+
alias: "d",
|
|
11
|
+
type: "boolean",
|
|
12
|
+
description: "Delete current configuration",
|
|
13
|
+
default: false,
|
|
14
|
+
})
|
|
15
|
+
.showHelpOnFail(false);
|
|
16
|
+
},
|
|
17
|
+
handler: async (args) => {
|
|
18
|
+
const { app_name, workdir, api, remove } = args;
|
|
19
|
+
const store = new CredentialsStore();
|
|
20
|
+
if (remove) {
|
|
21
|
+
await store.deleteCredentials();
|
|
22
|
+
}
|
|
23
|
+
const { apikey } = await prompts([
|
|
24
|
+
{
|
|
25
|
+
type: "text",
|
|
26
|
+
name: "apikey",
|
|
27
|
+
message: "What is your Faable ApiKey?",
|
|
28
|
+
},
|
|
29
|
+
]);
|
|
30
|
+
await store.saveApiKey({ apikey });
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export { configure };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { log } from '../../log.js';
|
|
2
|
+
import { upload_tag } from './upload_tag.js';
|
|
3
|
+
import { check_environment } from './check_environment.js';
|
|
4
|
+
import { context } from '../../api/context.js';
|
|
5
|
+
import { build_node } from './node-pipeline/index.js';
|
|
6
|
+
import { runtime_detection } from './runtime-detect/runtime_detection.js';
|
|
7
|
+
import { cmd } from '../../lib/cmd.js';
|
|
8
|
+
|
|
9
|
+
const deploy_command = async (args) => {
|
|
10
|
+
const workdir = args.workdir || process.cwd();
|
|
11
|
+
const { api } = await context();
|
|
12
|
+
// Resolve runtime
|
|
13
|
+
const { app_name, runtime } = await runtime_detection(workdir);
|
|
14
|
+
const name = args.app_slug || app_name;
|
|
15
|
+
if (!name) {
|
|
16
|
+
throw new Error("Missing <app_name>");
|
|
17
|
+
}
|
|
18
|
+
// Get app from Faable API
|
|
19
|
+
const app = await api.getBySlug(name);
|
|
20
|
+
// Check if we can build docker images
|
|
21
|
+
await check_environment();
|
|
22
|
+
log.info(`🚀 Deploying ${app.name} (${app.id}) runtime=${runtime.name}-${runtime.version}`);
|
|
23
|
+
// get environment variables
|
|
24
|
+
const env_vars = await api.getAppSecrets(app.id);
|
|
25
|
+
let type;
|
|
26
|
+
if (runtime.name == "node") {
|
|
27
|
+
const node_result = await build_node(app, {
|
|
28
|
+
workdir,
|
|
29
|
+
runtime,
|
|
30
|
+
env_vars,
|
|
31
|
+
});
|
|
32
|
+
type = node_result.type;
|
|
33
|
+
}
|
|
34
|
+
else if (runtime.name == "docker") {
|
|
35
|
+
type = "node";
|
|
36
|
+
await cmd(`docker build -t ${app.id} .`, {
|
|
37
|
+
enableOutput: true,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
throw new Error(`No build pipeline for runtime=${runtime.name}`);
|
|
42
|
+
}
|
|
43
|
+
// Upload to Faable registry
|
|
44
|
+
const { upload_tagname } = await upload_tag({ app, api });
|
|
45
|
+
// Create a deployment for this image
|
|
46
|
+
await api.createDeployment({ app_id: app.id, image: upload_tagname, type });
|
|
47
|
+
log.info(`🌍 Deployment created -> https://${app.url}`);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export { deploy_command };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { deploy_command } from './deploy_command.js';
|
|
2
|
+
|
|
3
|
+
const deploy = {
|
|
4
|
+
command: "deploy [app_slug]",
|
|
5
|
+
describe: "Deploy a faable app",
|
|
6
|
+
builder: (yargs) => {
|
|
7
|
+
return yargs
|
|
8
|
+
.positional("app_slug", {
|
|
9
|
+
type: "string",
|
|
10
|
+
description: "App slug",
|
|
11
|
+
})
|
|
12
|
+
.option("workdir", {
|
|
13
|
+
alias: "w",
|
|
14
|
+
type: "string",
|
|
15
|
+
description: "Working directory",
|
|
16
|
+
})
|
|
17
|
+
.showHelpOnFail(false);
|
|
18
|
+
},
|
|
19
|
+
handler: deploy_command,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export { deploy };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path__default from 'path';
|
|
3
|
+
import { log } from '../../../log.js';
|
|
4
|
+
import * as R from 'ramda';
|
|
5
|
+
|
|
6
|
+
const analyze_package = async (params) => {
|
|
7
|
+
const workdir = params.workdir;
|
|
8
|
+
const package_file = path__default.join(path__default.resolve(workdir), "package.json");
|
|
9
|
+
log.info(`Loading config from package.json`);
|
|
10
|
+
const pkg = await fs.readJSON(package_file);
|
|
11
|
+
// Check if build is required to run
|
|
12
|
+
const build_script = process.env.FAABLE_NPM_BUILD_SCRIPT
|
|
13
|
+
? process.env.FAABLE_NPM_BUILD_SCRIPT
|
|
14
|
+
: pkg?.scripts["build"]
|
|
15
|
+
? "build"
|
|
16
|
+
: null;
|
|
17
|
+
if (!build_script) {
|
|
18
|
+
log.info(`No build script on package.json`);
|
|
19
|
+
}
|
|
20
|
+
let type = "node";
|
|
21
|
+
// Detect nextjs deployment type
|
|
22
|
+
const next_dep = R.lensPath(["dependencies", "next"]);
|
|
23
|
+
const next_devdep = R.lensPath(["devDependencies", "next"]);
|
|
24
|
+
if (R.view(next_dep, pkg) || R.view(next_devdep, pkg)) {
|
|
25
|
+
type = "next";
|
|
26
|
+
}
|
|
27
|
+
log.info(`⚡️ Detected deployment type=${type}`);
|
|
28
|
+
return {
|
|
29
|
+
build_script,
|
|
30
|
+
type,
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export { analyze_package };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { log } from '../../../log.js';
|
|
2
|
+
import { cmd } from '../../../lib/cmd.js';
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
import Handlebars from 'handlebars';
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
import { Configuration } from '../../../lib/Configuration.js';
|
|
8
|
+
|
|
9
|
+
const __filename$1 = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname$1 = path.dirname(__filename$1);
|
|
11
|
+
const templates_dir = path.join(__dirname$1, "templates");
|
|
12
|
+
const dockerfile = fs.readFileSync(`${templates_dir}/Dockerfile`).toString();
|
|
13
|
+
const entrypoint = fs
|
|
14
|
+
.readFileSync(`${templates_dir}/entrypoint.sh`)
|
|
15
|
+
.toString("utf-8");
|
|
16
|
+
Handlebars.registerHelper("escape", function (variable) {
|
|
17
|
+
//const escaped_quotes = variable.replace(/(['"])/g, "\\$1");
|
|
18
|
+
const escaped_lines = variable
|
|
19
|
+
.replace(/(['`\\])/g, "\\$1")
|
|
20
|
+
.replace(/([$])/g, "\\$1");
|
|
21
|
+
return escaped_lines.split("\n").join("\\n");
|
|
22
|
+
//return escaped_lines.split("\n").join("\\n");
|
|
23
|
+
});
|
|
24
|
+
// Docker template file
|
|
25
|
+
const docker_template = Handlebars.compile(dockerfile);
|
|
26
|
+
const entrypoint_template = Handlebars.compile(entrypoint);
|
|
27
|
+
const build_docker = async (props) => {
|
|
28
|
+
const { app, workdir, template_context } = props;
|
|
29
|
+
const entrypoint_custom = entrypoint_template(template_context);
|
|
30
|
+
const start_command = Configuration.instance().startCommand;
|
|
31
|
+
log.info(`⚙️ Start command: ${start_command}`);
|
|
32
|
+
// NOTE: use slim to build projects
|
|
33
|
+
const linux_distro = "slim";
|
|
34
|
+
const from = [template_context.from, linux_distro].filter((e) => e).join("-");
|
|
35
|
+
log.info(`Using docker image ${from}`);
|
|
36
|
+
// run template
|
|
37
|
+
const dockerfile = docker_template({
|
|
38
|
+
from,
|
|
39
|
+
entry_script: entrypoint_custom,
|
|
40
|
+
start_command,
|
|
41
|
+
});
|
|
42
|
+
log.info(`📦 Packaging inside a docker image`);
|
|
43
|
+
// Build options
|
|
44
|
+
const timeout = 10 * 60 * 1000; // 10 minute timeout
|
|
45
|
+
await cmd(`docker build --platform linux/amd64 -t ${app.id} ${workdir} -f -<<EOF\n${dockerfile}\nEOF`, { timeout, enableOutput: true });
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export { build_docker };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { log } from '../../../log.js';
|
|
2
|
+
import { cmd } from '../../../lib/cmd.js';
|
|
3
|
+
import { Configuration } from '../../../lib/Configuration.js';
|
|
4
|
+
|
|
5
|
+
const build_project = async (args) => {
|
|
6
|
+
const build_script = args.build_script;
|
|
7
|
+
const build_command = build_script
|
|
8
|
+
? `npm run ${build_script}`
|
|
9
|
+
: Configuration.instance().buildCommand;
|
|
10
|
+
if (build_command) {
|
|
11
|
+
const cwd = args.cwd || process.cwd();
|
|
12
|
+
log.info(`⚙️ Building project [${build_command}]...`);
|
|
13
|
+
const timeout = 1000 * 60 * 30; // 30 minute timeout
|
|
14
|
+
await cmd(build_command, {
|
|
15
|
+
timeout,
|
|
16
|
+
cwd,
|
|
17
|
+
enableOutput: true,
|
|
18
|
+
...(args?.env ? { env: args?.env } : {}),
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
log.info(`⚡️ No build step`);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export { build_project };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { build_docker } from './build_docker.js';
|
|
2
|
+
import { analyze_package } from './analyze_package.js';
|
|
3
|
+
import { build_project } from './build_project.js';
|
|
4
|
+
import * as R from 'ramda';
|
|
5
|
+
import { log } from '../../../log.js';
|
|
6
|
+
|
|
7
|
+
const build_node = async (app, options) => {
|
|
8
|
+
// log.info(`🚀 Build Toolchain ${app.name} [${app.id}]`);
|
|
9
|
+
const { workdir, runtime, env_vars = [] } = options;
|
|
10
|
+
if (!runtime.version) {
|
|
11
|
+
throw new Error("Runtime version not specified for node");
|
|
12
|
+
}
|
|
13
|
+
// Analyze package.json to check if build is needed
|
|
14
|
+
const { build_script, type } = await analyze_package({ workdir });
|
|
15
|
+
// Environment variables
|
|
16
|
+
const env = R.fromPairs(env_vars.map((e) => [e.name, e.value]));
|
|
17
|
+
log.info(`Building with env variables ${Object.keys(env).join(",")}`);
|
|
18
|
+
// Do build
|
|
19
|
+
await build_project({ build_script, env });
|
|
20
|
+
// Bundle project inside a docker image
|
|
21
|
+
await build_docker({
|
|
22
|
+
app,
|
|
23
|
+
workdir,
|
|
24
|
+
template_context: {
|
|
25
|
+
from: `node:${runtime.version}`,
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
return { type };
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export { build_node };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
FROM {{from}}
|
|
2
|
+
LABEL com.faable.cloud="FaableCloud"
|
|
3
|
+
LABEL description="Faablecloud automatic deployment"
|
|
4
|
+
|
|
5
|
+
WORKDIR /faable/app
|
|
6
|
+
|
|
7
|
+
# Environment variables for runtime
|
|
8
|
+
ENV PORT=80
|
|
9
|
+
ENV NODE_ENV=production
|
|
10
|
+
ENV START_COMMAND="{{start_command}}"
|
|
11
|
+
|
|
12
|
+
# Copy Usercode
|
|
13
|
+
COPY . .
|
|
14
|
+
|
|
15
|
+
# Entrypoint stript
|
|
16
|
+
RUN echo '{{{escape entry_script}}}' >> entrypoint.sh
|
|
17
|
+
|
|
18
|
+
CMD ["/bin/sh", "./entrypoint.sh"]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path__default from 'path';
|
|
3
|
+
|
|
4
|
+
const has_any_of_files = (files, workdir) => {
|
|
5
|
+
for (let name of files) {
|
|
6
|
+
const filename = path__default.join(workdir, name);
|
|
7
|
+
if (fs.existsSync(filename)) {
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
return false;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export { has_any_of_files };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { strategy_nodejs } from './strategies/nodejs.js';
|
|
2
|
+
import * as R from 'ramda';
|
|
3
|
+
import { has_any_of_files } from './helpers/has_any_of_files.js';
|
|
4
|
+
import { strategy_docker } from './strategies/docker.js';
|
|
5
|
+
|
|
6
|
+
const runtime_detection = async (workdir) => {
|
|
7
|
+
const has = R.curry(has_any_of_files);
|
|
8
|
+
const strategy = R.cond([
|
|
9
|
+
[has(["package.json"]), R.always(strategy_nodejs)],
|
|
10
|
+
// [has(["requirements.txt"]), R.always(strategy_python)],
|
|
11
|
+
[has(["Dockerfile"]), R.always(strategy_docker)],
|
|
12
|
+
])(workdir);
|
|
13
|
+
if (!strategy) {
|
|
14
|
+
throw new Error("Cannot detect project type");
|
|
15
|
+
}
|
|
16
|
+
return strategy(workdir);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export { runtime_detection };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path__default from 'path';
|
|
3
|
+
import { cmd } from '../../../../lib/cmd.js';
|
|
4
|
+
import { log } from '../../../../log.js';
|
|
5
|
+
|
|
6
|
+
const getCurrentNodeVersion = async () => {
|
|
7
|
+
const out = await cmd(`node --version`);
|
|
8
|
+
const [_, version] = out.stdout.toString().trim().split("v");
|
|
9
|
+
return version;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Strategy to detect app name from package.json
|
|
13
|
+
* @param api
|
|
14
|
+
* @param workdir
|
|
15
|
+
* @returns
|
|
16
|
+
*/
|
|
17
|
+
const strategy_nodejs = async (workdir) => {
|
|
18
|
+
const packageJSONFile = path__default.join(workdir, "package.json");
|
|
19
|
+
// Check we have a valid name
|
|
20
|
+
const { name, engines } = fs.readJSONSync(packageJSONFile);
|
|
21
|
+
if (!name) {
|
|
22
|
+
throw new Error("Missing name in package.json");
|
|
23
|
+
}
|
|
24
|
+
// Use engines.node if found
|
|
25
|
+
let runtime_version = await getCurrentNodeVersion();
|
|
26
|
+
if (engines?.node) {
|
|
27
|
+
try {
|
|
28
|
+
const check_cmd = `npm view node@"${engines.node}" version | tail -n 1 | cut -d "'" -f2`;
|
|
29
|
+
const out = await cmd(check_cmd);
|
|
30
|
+
runtime_version = out.stdout.toString().trim();
|
|
31
|
+
log.info(`Using node@${runtime_version} from engines in package.json (${engines.node})`);
|
|
32
|
+
}
|
|
33
|
+
catch (e) {
|
|
34
|
+
log.info(`Node version defined in engines in package.json is not valid (${engines.node}), using current version ${runtime_version}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
log.info(`Node version ${runtime_version}`);
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
app_name: name,
|
|
42
|
+
runtime: {
|
|
43
|
+
name: "node",
|
|
44
|
+
version: runtime_version,
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export { strategy_nodejs };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { log } from '../../log.js';
|
|
2
|
+
import { cmd } from '../../lib/cmd.js';
|
|
3
|
+
|
|
4
|
+
const upload_tag = async (args) => {
|
|
5
|
+
const { api, app } = args;
|
|
6
|
+
log.info(`🔁 Uploading...`);
|
|
7
|
+
const registry = await api.getRegistry(app.id);
|
|
8
|
+
// Registry login
|
|
9
|
+
const { user, password, hostname, image } = registry;
|
|
10
|
+
await cmd(`echo "${password}" | docker login --username "${user}" --password-stdin ${hostname}`);
|
|
11
|
+
// Tag image for production
|
|
12
|
+
const upload_tagname = `${hostname}/${image}`;
|
|
13
|
+
await cmd(`docker tag ${app.id} ${upload_tagname}`);
|
|
14
|
+
// Upload the image to faable registry
|
|
15
|
+
await cmd(`docker push ${upload_tagname}`);
|
|
16
|
+
log.info(`✅ Upload completed.`);
|
|
17
|
+
return {
|
|
18
|
+
upload_tagname,
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export { upload_tag };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { writeGithubAction } from './writeGithubAction.js';
|
|
2
|
+
|
|
3
|
+
const init = {
|
|
4
|
+
command: ["initialize", "$0"],
|
|
5
|
+
describe: "Initialize Faable",
|
|
6
|
+
builder: (yargs) => {
|
|
7
|
+
return yargs
|
|
8
|
+
.option("overwrite", {
|
|
9
|
+
alias: "o",
|
|
10
|
+
type: "boolean",
|
|
11
|
+
description: "Overwrite generated file",
|
|
12
|
+
default: false,
|
|
13
|
+
})
|
|
14
|
+
.showHelpOnFail(false);
|
|
15
|
+
},
|
|
16
|
+
handler: async (args) => {
|
|
17
|
+
const { overwrite } = args;
|
|
18
|
+
await writeGithubAction({ overwrite });
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export { init };
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import path__default from 'path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import { log } from '../../log.js';
|
|
4
|
+
import prompts from 'prompts';
|
|
5
|
+
import { CredentialsStore } from '../../lib/CredentialsStore.js';
|
|
6
|
+
import yaml from 'yaml';
|
|
7
|
+
|
|
8
|
+
const store = new CredentialsStore();
|
|
9
|
+
const get_workflows_dir = () => {
|
|
10
|
+
return path__default.join(process.cwd(), ".github", "workflows");
|
|
11
|
+
};
|
|
12
|
+
const get_default_action = () => {
|
|
13
|
+
return path__default.join(get_workflows_dir(), "deploy.yml");
|
|
14
|
+
};
|
|
15
|
+
const demandConfig = async (force = false) => {
|
|
16
|
+
const creds = await store.loadCredentials();
|
|
17
|
+
if (creds?.apikey) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const { apikey } = await prompts([
|
|
21
|
+
{
|
|
22
|
+
type: "text",
|
|
23
|
+
name: "apikey",
|
|
24
|
+
message: "What is your Faable ApiKey?",
|
|
25
|
+
},
|
|
26
|
+
]);
|
|
27
|
+
await store.saveApiKey({ apikey });
|
|
28
|
+
};
|
|
29
|
+
const tentativeName = async () => {
|
|
30
|
+
try {
|
|
31
|
+
const pkg = path__default.join(process.cwd(), "package.json");
|
|
32
|
+
const { name } = await fs.readJSON(pkg);
|
|
33
|
+
return name;
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
const checkPackageManager = async () => {
|
|
40
|
+
if (fs.existsSync(path__default.join(process.cwd(), "package-lock.json"))) {
|
|
41
|
+
return "npm";
|
|
42
|
+
}
|
|
43
|
+
if (fs.existsSync(path__default.join(process.cwd(), "yarn.lock"))) {
|
|
44
|
+
return "yarn";
|
|
45
|
+
}
|
|
46
|
+
throw new Error("No package-lock.json or yarn.lock file found");
|
|
47
|
+
};
|
|
48
|
+
const writeGithubAction = async (params = {}) => {
|
|
49
|
+
const { overwrite } = params;
|
|
50
|
+
await demandConfig();
|
|
51
|
+
const gh_action_file = get_default_action();
|
|
52
|
+
// Already configured
|
|
53
|
+
if (!overwrite && fs.pathExistsSync(gh_action_file)) {
|
|
54
|
+
log.info(`Github action is already configured.`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const onCancel = (prompt) => {
|
|
58
|
+
log.info("Cancel");
|
|
59
|
+
process.exit(0);
|
|
60
|
+
};
|
|
61
|
+
const { app_name } = await prompts([
|
|
62
|
+
{
|
|
63
|
+
type: "text",
|
|
64
|
+
name: "app_name",
|
|
65
|
+
initial: await tentativeName(),
|
|
66
|
+
message: "Which app are you deploying",
|
|
67
|
+
},
|
|
68
|
+
], { onCancel });
|
|
69
|
+
const manager = await checkPackageManager();
|
|
70
|
+
const action = {
|
|
71
|
+
name: "Deploy to Faable",
|
|
72
|
+
on: {
|
|
73
|
+
push: {
|
|
74
|
+
branches: ["main"],
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
permissions: {
|
|
78
|
+
"id-token": "write",
|
|
79
|
+
"contents": "read"
|
|
80
|
+
},
|
|
81
|
+
jobs: {
|
|
82
|
+
deploy: {
|
|
83
|
+
"runs-on": "ubuntu-latest",
|
|
84
|
+
steps: [
|
|
85
|
+
{ uses: "actions/checkout@v2" },
|
|
86
|
+
{
|
|
87
|
+
uses: "actions/setup-node@v4",
|
|
88
|
+
with: {
|
|
89
|
+
"node-version": "lts/*",
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
...(manager == "npm" ? [{ run: "npm ci" }] : []),
|
|
93
|
+
...(manager == "yarn"
|
|
94
|
+
? [{ run: "yarn install --frozen-lockfile" }]
|
|
95
|
+
: []),
|
|
96
|
+
{
|
|
97
|
+
name: "Deploy to Faable",
|
|
98
|
+
run: `npx @faable/faable deploy ${app_name}`,
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
await fs.writeFile(gh_action_file, yaml.stringify(action));
|
|
105
|
+
log.info(`Written ${gh_action_file}`);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export { writeGithubAction };
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path__default from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
|
|
5
|
+
const __filename$1 = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname$1 = path__default.dirname(__filename$1);
|
|
7
|
+
const pkg = fs.readJSONSync(path__default.join(__dirname$1, "..", "package.json"));
|
|
8
|
+
const version = pkg.version;
|
|
9
|
+
|
|
10
|
+
export { version };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import yargs from 'yargs';
|
|
2
|
+
import { hideBin } from 'yargs/helpers';
|
|
3
|
+
import { apps } from './commands/apps/index.js';
|
|
4
|
+
import { configure } from './commands/configure/index.js';
|
|
5
|
+
import { deploy } from './commands/deploy/index.js';
|
|
6
|
+
import { log } from './log.js';
|
|
7
|
+
import { init } from './commands/init/index.js';
|
|
8
|
+
import { version } from './config.js';
|
|
9
|
+
import { Configuration } from './lib/Configuration.js';
|
|
10
|
+
|
|
11
|
+
const yg = yargs();
|
|
12
|
+
yg.scriptName("faable")
|
|
13
|
+
.middleware(function (argv) {
|
|
14
|
+
console.log(`Faable CLI ${version}`);
|
|
15
|
+
}, true)
|
|
16
|
+
.option("c", {
|
|
17
|
+
alias: "config",
|
|
18
|
+
description: "Path to the local `faable.json` file",
|
|
19
|
+
string: true,
|
|
20
|
+
})
|
|
21
|
+
.middleware(function (argv) {
|
|
22
|
+
if (argv.config) {
|
|
23
|
+
Configuration.instance().setConfigFile(argv.config, {
|
|
24
|
+
ignoreWarnings: false,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
Configuration.instance();
|
|
29
|
+
}
|
|
30
|
+
}, true)
|
|
31
|
+
.command(deploy)
|
|
32
|
+
.command(apps)
|
|
33
|
+
.command(configure)
|
|
34
|
+
.command(init)
|
|
35
|
+
.demandCommand(1)
|
|
36
|
+
.help()
|
|
37
|
+
.fail(function (msg, err) {
|
|
38
|
+
if (err) {
|
|
39
|
+
log.error(`❌ ${err.message}`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (msg) {
|
|
44
|
+
yg.showHelp();
|
|
45
|
+
log.info(msg);
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
.parse(hideBin(process.argv), {});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import path__default from 'path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import { log } from '../log.js';
|
|
4
|
+
|
|
5
|
+
class Configuration {
|
|
6
|
+
static _instance;
|
|
7
|
+
config = {};
|
|
8
|
+
constructor() {
|
|
9
|
+
// Try to read default config file
|
|
10
|
+
this.setConfigFile("faable.json", { ignoreWarnings: true });
|
|
11
|
+
}
|
|
12
|
+
setConfigFile(file, options) {
|
|
13
|
+
const config_file = path__default.join(process.cwd(), file);
|
|
14
|
+
if (fs.existsSync(config_file)) {
|
|
15
|
+
this.config = fs.readJSONSync(config_file);
|
|
16
|
+
log.info(`Loaded configuration from: ${file}`);
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
if (!options.ignoreWarnings) {
|
|
20
|
+
log.warn(`Cannot read Faable config file ${file}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
static instance() {
|
|
25
|
+
if (!Configuration._instance) {
|
|
26
|
+
Configuration._instance = new Configuration();
|
|
27
|
+
}
|
|
28
|
+
return Configuration._instance;
|
|
29
|
+
}
|
|
30
|
+
get startCommand() {
|
|
31
|
+
return this.config.startCommand || "npm run start";
|
|
32
|
+
}
|
|
33
|
+
get buildCommand() {
|
|
34
|
+
return this.config.buildCommand;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export { Configuration };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import path__default from 'path';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
import { log } from '../log.js';
|
|
5
|
+
|
|
6
|
+
class CredentialsStore {
|
|
7
|
+
log;
|
|
8
|
+
faable_home;
|
|
9
|
+
constructor(log$1 = log) {
|
|
10
|
+
this.log = log$1;
|
|
11
|
+
this.faable_home = path__default.join(os.homedir(), ".faable");
|
|
12
|
+
}
|
|
13
|
+
async deleteCredentials() {
|
|
14
|
+
await fs.remove(this.faable_home);
|
|
15
|
+
this.log.info(`Deleted credentials`);
|
|
16
|
+
}
|
|
17
|
+
get credentials_path() {
|
|
18
|
+
return path__default.join(this.faable_home, "credentials");
|
|
19
|
+
}
|
|
20
|
+
async saveApiKey(config) {
|
|
21
|
+
await fs.ensureDir(this.faable_home);
|
|
22
|
+
await fs.writeJSON(this.credentials_path, config);
|
|
23
|
+
this.log.info(`Stored apikey`);
|
|
24
|
+
}
|
|
25
|
+
async loadCredentials() {
|
|
26
|
+
if (!fs.existsSync(this.credentials_path)) {
|
|
27
|
+
// No credentials found
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
// Return credentials
|
|
31
|
+
const config = await fs.readJSON(this.credentials_path);
|
|
32
|
+
return config || {};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export { CredentialsStore };
|
package/dist/lib/cmd.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { spawn } from 'promisify-child-process';
|
|
2
|
+
import { log } from '../log.js';
|
|
3
|
+
|
|
4
|
+
const cmd = async (cmd, config) => {
|
|
5
|
+
// Defaults
|
|
6
|
+
const enableOutput = config?.enableOutput || false;
|
|
7
|
+
const timeout = config?.timeout;
|
|
8
|
+
const cwd = config?.cwd;
|
|
9
|
+
const child = spawn("/bin/bash", ["-c", cmd], {
|
|
10
|
+
encoding: "utf8",
|
|
11
|
+
stdio: enableOutput ? "inherit" : "pipe",
|
|
12
|
+
timeout,
|
|
13
|
+
cwd,
|
|
14
|
+
env: {
|
|
15
|
+
...process.env,
|
|
16
|
+
...config?.env,
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
const out_data = [];
|
|
20
|
+
child.stderr?.on("data", (data) => {
|
|
21
|
+
out_data.push(data);
|
|
22
|
+
});
|
|
23
|
+
child.stdout?.on("data", (data) => {
|
|
24
|
+
out_data.push(data);
|
|
25
|
+
});
|
|
26
|
+
try {
|
|
27
|
+
const result = await child;
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
const output = out_data.map((b) => b.toString()).join("\n");
|
|
32
|
+
log.error(error?.message);
|
|
33
|
+
if (output) {
|
|
34
|
+
log.error(output);
|
|
35
|
+
}
|
|
36
|
+
throw new Error(`Command error: ${cmd}`);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export { cmd };
|
package/dist/log.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@faable/faable",
|
|
3
|
+
"main": "dist/index.js",
|
|
4
|
+
"dependencies": {
|
|
5
|
+
"@actions/core": "^3.0.0",
|
|
6
|
+
"axios": "^1.13.2",
|
|
7
|
+
"fs-extra": "^11.3.2",
|
|
8
|
+
"handlebars": "^4.7.8",
|
|
9
|
+
"pino": "^10.1.0",
|
|
10
|
+
"pino-pretty": "^13.1.3",
|
|
11
|
+
"promisify-child-process": "^4.1.2",
|
|
12
|
+
"prompts": "^2.4.2",
|
|
13
|
+
"ramda": "^0.32.0",
|
|
14
|
+
"tslib": "^2.8.1",
|
|
15
|
+
"yaml": "^2.8.2",
|
|
16
|
+
"yargs": "^18.0.0"
|
|
17
|
+
},
|
|
18
|
+
"version": "0.0.0-dev",
|
|
19
|
+
"bin": {
|
|
20
|
+
"faable": "bin/faable.js"
|
|
21
|
+
},
|
|
22
|
+
"type": "module",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"author": "Marc Pomar <marc@faable.com>",
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"bugs": {
|
|
27
|
+
"url": "https://github.com/faablecloud/faable/issues"
|
|
28
|
+
},
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"registry": "https://registry.npmjs.org/",
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "https://github.com/faablecloud/faable.git"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://github.com/faablecloud/faable#readme"
|
|
38
|
+
}
|