@faable/faable 1.3.15 → 1.4.1

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.
@@ -1,9 +1,9 @@
1
- import { ConfigStore } from '../lib/ConfigStore.js';
1
+ import { CredentialsStore } from '../lib/CredentialsStore.js';
2
2
  import { FaableApi } from './FaableApi.js';
3
3
  import { apikey_strategy } from './strategies/apikey.strategy.js';
4
4
 
5
5
  const context = async () => {
6
- const store = new ConfigStore();
6
+ const store = new CredentialsStore();
7
7
  const apikey = process.env.FAABLE_APIKEY || (await store.loadCredentials())?.apikey;
8
8
  return {
9
9
  api: FaableApi.create({ authStrategy: apikey_strategy, auth: { apikey } }),
@@ -1,4 +1,4 @@
1
- import { ConfigStore } from '../../lib/ConfigStore.js';
1
+ import { CredentialsStore } from '../../lib/CredentialsStore.js';
2
2
  import prompts from 'prompts';
3
3
 
4
4
  const configure = {
@@ -16,7 +16,7 @@ const configure = {
16
16
  },
17
17
  handler: async (args) => {
18
18
  const { app_name, workdir, api, remove } = args;
19
- const store = new ConfigStore();
19
+ const store = new CredentialsStore();
20
20
  if (remove) {
21
21
  await store.deleteCredentials();
22
22
  }
@@ -1,8 +1,8 @@
1
- import { cmd } from './cmd.js';
1
+ import { cmd } from '../../lib/cmd.js';
2
2
 
3
3
  const check_environment = async () => {
4
4
  try {
5
- await cmd("docker", ["ps"]);
5
+ await cmd("docker ps");
6
6
  }
7
7
  catch (error) {
8
8
  console.log(error);
@@ -1,55 +1,43 @@
1
1
  import { log } from '../../log.js';
2
- import { bundle_docker } from './bundle_docker.js';
3
2
  import { upload_tag } from './upload_tag.js';
4
3
  import { check_environment } from './check_environment.js';
5
- import { analyze_package } from './analyze_package.js';
6
4
  import { context } from '../../api/context.js';
7
- import { build_project } from './build_project.js';
8
- import fs from 'fs-extra';
9
- import path__default from 'path';
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';
10
8
 
11
- const get_app = async (api, app_slug, workdir) => {
12
- let slug;
13
- if (app_slug) {
14
- slug = app_slug;
15
- }
16
- else {
17
- const packageJSONFile = path__default.join(workdir, "package.json");
18
- if (!fs.existsSync(packageJSONFile)) {
19
- throw new Error("Not found package.json");
20
- }
21
- const { name } = fs.readJSONSync(packageJSONFile);
22
- if (!name) {
23
- throw new Error("Missing name in package.json");
24
- }
25
- slug = name;
26
- }
27
- return api.getBySlug(slug);
28
- };
29
9
  const deploy_command = async (args) => {
30
10
  const workdir = args.workdir || process.cwd();
31
- // Get registry data from api.faable.com
32
11
  const { api } = await context();
33
- const app = await get_app(api, args.app_slug, workdir);
12
+ // Resolve runtime
13
+ const { app_name, runtime, runtime_version } = 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);
34
20
  // Check if we can build docker images
35
21
  await check_environment();
36
- log.info(`🚀 Preparing to build ${app.name} [${app.id}]`);
37
- // Analyze package.json to check if build is needed
38
- const { build_script, type } = await analyze_package({ workdir });
39
- await build_project({ app, build_script });
40
- // Bundle project inside a docker image
41
- const { tagname } = await bundle_docker({
42
- app,
43
- workdir,
44
- template_context: {
45
- from: "node:18.12.0-slim",
46
- start_script: "start",
47
- },
48
- });
22
+ log.info(`🚀 Deploying [${app.name}] runtime=${runtime}-${runtime_version} app_id=${app.id}`);
23
+ let type;
24
+ if (runtime == "node") {
25
+ const node_result = await build_node(app, workdir);
26
+ type = node_result.type;
27
+ }
28
+ else if (runtime == "docker") {
29
+ type = "node";
30
+ await cmd(`docker build -t ${app.id} .`, {
31
+ enableOutput: true,
32
+ });
33
+ }
34
+ else {
35
+ throw new Error(`No build pipeline for runtime=${runtime}`);
36
+ }
49
37
  // Upload to Faable registry
50
- const { image_tag } = await upload_tag({ app, api, tagname });
38
+ const { upload_tagname } = await upload_tag({ app, api });
51
39
  // Create a deployment for this image
52
- await api.createDeployment({ app_id: app.id, image: image_tag, type });
40
+ await api.createDeployment({ app_id: app.id, image: upload_tagname, type });
53
41
  log.info(`🌍 Deployment created -> https://${app.url}`);
54
42
  };
55
43
 
@@ -1,16 +1,12 @@
1
1
  import fs from 'fs-extra';
2
2
  import path__default from 'path';
3
- import { log } from '../../log.js';
3
+ import { log } from '../../../log.js';
4
4
 
5
5
  const analyze_package = async (params) => {
6
6
  const workdir = params.workdir;
7
7
  const package_file = path__default.join(path__default.resolve(workdir), "package.json");
8
8
  log.info(`Loading config from package.json...`);
9
9
  const pkg = await fs.readJSON(package_file);
10
- // Package must have a start script
11
- if (!pkg?.scripts?.start) {
12
- throw new Error("Missing start script");
13
- }
14
10
  // Check if build is required to run
15
11
  const build_script = process.env.FAABLE_NPM_BUILD_SCRIPT
16
12
  ? process.env.FAABLE_NPM_BUILD_SCRIPT
@@ -25,7 +21,7 @@ const analyze_package = async (params) => {
25
21
  if (pkg.dependencies["next"]) {
26
22
  type = "next";
27
23
  }
28
- log.info(`⚙️ Detected deployment type=${type}`);
24
+ log.info(`⚡️ Detected deployment type=${type}`);
29
25
  return {
30
26
  build_script,
31
27
  type,
@@ -0,0 +1,25 @@
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
+ ? `yarn 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 * 100; // 100 minute timeout
14
+ await cmd(build_command, {
15
+ timeout,
16
+ cwd,
17
+ enableOutput: true,
18
+ });
19
+ }
20
+ else {
21
+ log.info(`⚡️ No build step`);
22
+ }
23
+ };
24
+
25
+ export { build_project };
@@ -1,13 +1,14 @@
1
- import { log } from '../../log.js';
2
- import { cmd } from './cmd.js';
1
+ import { log } from '../../../log.js';
2
+ import { cmd } from '../../../lib/cmd.js';
3
3
  import fs from 'fs-extra';
4
4
  import Handlebars from 'handlebars';
5
5
  import * as path from 'path';
6
6
  import { fileURLToPath } from 'url';
7
+ import { Configuration } from '../../../lib/Configuration.js';
7
8
 
8
9
  const __filename = fileURLToPath(import.meta.url);
9
10
  const __dirname = path.dirname(__filename);
10
- const templates_dir = path.join(__dirname, "../../../templates");
11
+ const templates_dir = path.join(__dirname, "templates");
11
12
  const dockerfile = fs.readFileSync(`${templates_dir}/Dockerfile`).toString();
12
13
  const entrypoint = fs
13
14
  .readFileSync(`${templates_dir}/entrypoint.sh`)
@@ -27,21 +28,14 @@ const bundle_docker = async (props) => {
27
28
  const { app, workdir, template_context } = props;
28
29
  const entrypoint_custom = entrypoint_template(template_context);
29
30
  const dockerfile = docker_template({
30
- ...template_context,
31
+ from: template_context.from,
31
32
  entry_script: entrypoint_custom,
33
+ start_command: Configuration.instance().startCommand,
32
34
  });
33
35
  log.info(`📦 Packaging inside a docker image`);
34
- const tagname = app.id;
36
+ // Build options
35
37
  const timeout = 10 * 60 * 1000; // 10 minute timeout
36
- const command = [
37
- "-c",
38
- `docker build -t ${tagname} ${workdir} -f -<<EOF\n${dockerfile}\nEOF`,
39
- ];
40
- await cmd("/bin/bash", command, { timeout, enableOutput: true });
41
- log.info(`⚙️ Image ready [tag:${tagname}]`);
42
- return {
43
- tagname,
44
- };
38
+ await cmd(`docker build -t ${app.id} ${workdir} -f -<<EOF\n${dockerfile}\nEOF`, { timeout, enableOutput: true });
45
39
  };
46
40
 
47
41
  export { bundle_docker };
@@ -0,0 +1,21 @@
1
+ import { bundle_docker } from './bundle_docker.js';
2
+ import { analyze_package } from './analyze_package.js';
3
+ import { build_project } from './build_project.js';
4
+
5
+ const build_node = async (app, workdir) => {
6
+ // log.info(`🚀 Build Toolchain ${app.name} [${app.id}]`);
7
+ // Analyze package.json to check if build is needed
8
+ const { build_script, type } = await analyze_package({ workdir });
9
+ await build_project({ app, build_script });
10
+ // Bundle project inside a docker image
11
+ await bundle_docker({
12
+ app,
13
+ workdir,
14
+ template_context: {
15
+ from: "node:18.12.0-slim",
16
+ },
17
+ });
18
+ return { type };
19
+ };
20
+
21
+ export { build_node };
@@ -7,7 +7,7 @@ WORKDIR /faable/app
7
7
  # Environment variables for runtime
8
8
  ENV PORT=80
9
9
  ENV NODE_ENV=production
10
- ENV START_SCRIPT=start
10
+ ENV START_COMMAND="{{start_command}}"
11
11
 
12
12
  # Copy Usercode
13
13
  COPY . .
@@ -5,4 +5,4 @@ NPM_VERSION=$(npm --version)
5
5
  YARN_VERSION=$(yarn --version)
6
6
 
7
7
  echo "Faable Cloud · [node $NODE_VERSION] [npm $NPM_VERSION] [yarn $YARN_VERSION]"
8
- npm run $START_SCRIPT
8
+ eval $START_COMMAND
@@ -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,20 @@
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_python } from './strategies/python.js';
5
+ import { strategy_docker } from './strategies/docker.js';
6
+
7
+ const runtime_detection = async (workdir) => {
8
+ const has = R.curry(has_any_of_files);
9
+ const strategy = R.cond([
10
+ [has(["package.json"]), R.always(strategy_nodejs)],
11
+ [has(["requirements.txt"]), R.always(strategy_python)],
12
+ [has(["Dockerfile"]), R.always(strategy_docker)],
13
+ ])(workdir);
14
+ if (!strategy) {
15
+ throw new Error("Cannot detect project type");
16
+ }
17
+ return strategy(workdir);
18
+ };
19
+
20
+ export { runtime_detection };
@@ -0,0 +1,8 @@
1
+ const strategy_docker = async (workdir) => {
2
+ return {
3
+ runtime: "docker",
4
+ runtime_version: "docker",
5
+ };
6
+ };
7
+
8
+ export { strategy_docker };
@@ -0,0 +1,24 @@
1
+ import fs from 'fs-extra';
2
+ import path__default from 'path';
3
+
4
+ /**
5
+ * Strategy to detect app name from package.json
6
+ * @param api
7
+ * @param workdir
8
+ * @returns
9
+ */
10
+ const strategy_nodejs = async (workdir) => {
11
+ const packageJSONFile = path__default.join(workdir, "package.json");
12
+ // Check we have a valid name
13
+ const { name } = fs.readJSONSync(packageJSONFile);
14
+ if (!name) {
15
+ throw new Error("Missing name in package.json");
16
+ }
17
+ return {
18
+ app_name: name,
19
+ runtime: "node",
20
+ runtime_version: "18",
21
+ };
22
+ };
23
+
24
+ export { strategy_nodejs };
@@ -0,0 +1,22 @@
1
+ import fs from 'fs-extra';
2
+ import path__default from 'path';
3
+
4
+ const strategy_python = async (workdir) => {
5
+ const runtime_config = path__default.join(workdir, "runtime.txt");
6
+ // Default runtime
7
+ let runtime_version = "3.11.3";
8
+ // Select runtime based on config
9
+ if (fs.existsSync(runtime_config)) {
10
+ const runtime_data = fs.readFileSync(runtime_config).toString();
11
+ if (!runtime_data.startsWith("python-")) {
12
+ throw new Error("runtime.txt must have runtime format with python-<version>");
13
+ }
14
+ runtime_version = runtime_data.split("-")[1];
15
+ }
16
+ return {
17
+ runtime: "python",
18
+ runtime_version,
19
+ };
20
+ };
21
+
22
+ export { strategy_python };
@@ -1,22 +1,21 @@
1
1
  import { log } from '../../log.js';
2
- import { cmd } from './cmd.js';
2
+ import { cmd } from '../../lib/cmd.js';
3
3
 
4
4
  const upload_tag = async (args) => {
5
- const { api, app, tagname } = args;
5
+ const { api, app } = args;
6
6
  log.info(`🔁 Uploading...`);
7
7
  const registry = await api.getRegistry(app.id);
8
8
  // Registry login
9
9
  const { user, password, hostname, image } = registry;
10
- const docker_login_cmd = `echo "${password}" | docker login --username ${user} --password-stdin ${hostname}`;
11
- await cmd("/bin/bash", ["-c", docker_login_cmd]);
10
+ await cmd(`echo "${password}" | docker login --username ${user} --password-stdin ${hostname}`);
12
11
  // Tag image for production
13
- const image_tag = `${hostname}/${image}`;
14
- await cmd("docker", ["tag", tagname, image_tag]);
12
+ const upload_tagname = `${hostname}/${image}`;
13
+ await cmd(`docker tag ${app.id} ${upload_tagname}`);
15
14
  // Upload the image to faable registry
16
- await cmd("docker", ["push", image_tag]);
15
+ await cmd(`docker push ${upload_tagname}`);
17
16
  log.info(`✅ Upload completed.`);
18
17
  return {
19
- image_tag,
18
+ upload_tagname,
20
19
  };
21
20
  };
22
21
 
@@ -2,13 +2,13 @@ import path__default from 'path';
2
2
  import fs from 'fs-extra';
3
3
  import { log } from '../../log.js';
4
4
  import prompts from 'prompts';
5
- import { ConfigStore } from '../../lib/ConfigStore.js';
5
+ import { CredentialsStore } from '../../lib/CredentialsStore.js';
6
6
  import yaml from 'yaml';
7
7
 
8
8
  class ConfigurationHelper {
9
9
  store;
10
10
  constructor() {
11
- this.store = new ConfigStore();
11
+ this.store = new CredentialsStore();
12
12
  }
13
13
  get workflows_dir() {
14
14
  return path__default.join(process.cwd(), ".github", "workflows");
package/dist/index.js CHANGED
@@ -6,11 +6,27 @@ import { deploy } from './commands/deploy/index.js';
6
6
  import { log } from './log.js';
7
7
  import { init } from './commands/init/index.js';
8
8
  import { version } from './config.js';
9
+ import { Configuration } from './lib/Configuration.js';
9
10
 
10
11
  const yg = yargs();
11
12
  yg.scriptName("faable")
12
13
  .middleware(function (argv) {
13
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
+ }
14
30
  }, true)
15
31
  .command(deploy)
16
32
  .command(apps)
@@ -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
+ return Configuration._instance;
27
+ }
28
+ return new Configuration();
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 };
@@ -3,7 +3,7 @@ import os from 'os';
3
3
  import fs from 'fs-extra';
4
4
  import { log } from '../log.js';
5
5
 
6
- class ConfigStore {
6
+ class CredentialsStore {
7
7
  log;
8
8
  faable_home;
9
9
  constructor(log$1 = log) {
@@ -33,4 +33,4 @@ class ConfigStore {
33
33
  }
34
34
  }
35
35
 
36
- export { ConfigStore };
36
+ export { CredentialsStore };
@@ -1,11 +1,11 @@
1
1
  import { spawn } from 'promisify-child-process';
2
2
 
3
- const cmd = async (cmd, args, params) => {
3
+ const cmd = async (cmd, config) => {
4
4
  // Defaults
5
- const enableOutput = params?.enableOutput || false;
6
- const timeout = params?.timeout;
7
- const cwd = params?.cwd;
8
- const child = spawn(cmd, args, {
5
+ const enableOutput = config?.enableOutput || false;
6
+ const timeout = config?.timeout;
7
+ const cwd = config?.cwd;
8
+ const child = spawn("/bin/bash", ["-c", cmd], {
9
9
  encoding: "utf8",
10
10
  stdio: enableOutput ? "inherit" : "pipe",
11
11
  timeout,
@@ -23,9 +23,9 @@ const cmd = async (cmd, args, params) => {
23
23
  return result;
24
24
  }
25
25
  catch (error) {
26
- const output = out_data.map((b) => b.toString()).join("\n");
27
- console.log(output);
28
- throw new Error(`Error running command`);
26
+ out_data.map((b) => b.toString()).join("\n");
27
+ // console.log(output);
28
+ throw new Error(`Command error: ${cmd}`);
29
29
  }
30
30
  };
31
31
 
package/dist/log.js CHANGED
@@ -1,7 +1,6 @@
1
1
  import pino from 'pino';
2
2
  import 'pino-pretty';
3
3
 
4
- //import * as core from "@actions/core";
5
4
  const log = pino({
6
5
  transport: {
7
6
  target: "pino-pretty",
package/package.json CHANGED
@@ -9,11 +9,12 @@
9
9
  "pino-pretty": "^9.4.0",
10
10
  "promisify-child-process": "^4.1.1",
11
11
  "prompts": "^2.4.2",
12
+ "ramda": "^0.29.0",
12
13
  "tslib": "^2.4.1",
13
14
  "yaml": "^2.2.2",
14
15
  "yargs": "^17.6.2"
15
16
  },
16
- "version": "1.3.15",
17
+ "version": "1.4.1",
17
18
  "bin": {
18
19
  "faable": "bin/faable.js"
19
20
  },
@@ -1,21 +0,0 @@
1
- import { log } from '../../log.js';
2
- import { cmd } from './cmd.js';
3
-
4
- const build_project = async (args) => {
5
- const build_script = args.build_script;
6
- if (build_script) {
7
- const cwd = args.cwd || process.cwd();
8
- log.info(`⚡️ Running build [${build_script}]...`);
9
- const timeout = 1000 * 60 * 100; // 100 minute timeout
10
- await cmd("yarn", ["run", build_script], {
11
- timeout,
12
- cwd,
13
- enableOutput: true,
14
- });
15
- }
16
- else {
17
- log.info(`⚡️ No build step`);
18
- }
19
- };
20
-
21
- export { build_project };