lenv-cli

$ npm install --global lenv-cli

Usage no npm install needed!

<script type="module">
  import lenvCli from 'https://cdn.skypack.dev/lenv-cli';
</script>

README

Install

NPM

$ npm install --global lenv-cli

YARN

$ yarn global add lenv-cli

Run

$ lenv

What is LENV?

LENV is a CLI tool which allows to configure and build docker-compose file from modules, secrets and automation artifacts.

What is module?

Module is a small piece of configuration. It may contain multiple configuration variants. For example local, staging and production. End user of LENV CLI has to select which modules to run and configuration variant for each module. LENV will resolve dependencies and use all required modules to build docker-compose file.

How to create a module?

Modules are built from multiple files. At least two files are required:

  • Module declaration file ./modules/MODULE_DIR/*.module.js
  • Module configuration variant ./modules/MODULE_DIR/configs/*.js

Module declaration file has only two fields:

module.exports = {
  name: "mongodb", // module name
  defaultConfig: "local", // default configuration variant
};

Module configuration variant:

module.exports = {
  name: "local", // variant name
  requires: [
      /* 
        Array of dependencies. 
        Values are names of other modules.
        (optional)
      */
  ],
  jobs: [
      /* 
        Array of jobs required for this configuration. 
        Values are names of other modules.
        (optional)
      */
  ],
  output: {
    /*
        Object with exported variables 
        which other modules may use
        (optional)
      */
  },
  docker: {
    /*
        This object will be merged into the final
        docker-compose file
        (optional)
      */
  },
};

How to share variables between modules?

There are to steps.

  • Export values from one module
  • Import values in another

How to export values?

Add them into the output section

module.exports = {
  name: "local", // variant name
  output: {
    host: "mongodb"
  },
};

How to import values?

We can use values from other modules inside two sections:

  • output
  • docker

There is a function getVar. Input of the function is a string MODULE_NAME.VARIABLE_NAME. Output is a Promise.

Example of usage:

module.exports = {
  name: "local", // variant name
  requires: [
    "mongodb",
  ],
  docker: {
    services: {
        api: {
          /* ....... */
          environment: {
            DB_HOST: ({ getVar }) => getVar("mongodb.host")
          }
          /* ....... */
        }
      }
  },
};

If some transformations to the value are required there are two options to implement them:

  • Promise .then
  • async/await

Important!

Don’t forget to add module into the requires section.

When to use dependencies?

There are two main reasons to add module as a dependency:

  • To be able to use exported variables (output) from another module.
  • To enable module and to include it into the final docker-compose file.

How to add custom logic?

All configurations are unique and sometimes there is a requirement to add custom logic into the configuration process. There are two options to do this:

  • Jobs
  • Custom functions

What are Jobs?

During build process LENV CLI runs some code which is grouped into the Jobs. Jobs are grouped into the Stages.

Out of the box there are two stages:

  • build
  • run

And two jobs:

  • docker-compose
  • docker-compose up One job in one stage.

LENV runs all jobs in one stage simultaneously. LENV will not continue to the next stage in case not all of the jobs from the current stage have Success state.

A few examples for jobs:

  • Check that code repository is cloned
  • Check that /etc/hosts contains required records
  • There is a dynamic configuration which requires user input or manual actions

How to add Stages?

LENV checks for stages inside lenv.config.js .

module.exports={
    runStages:[
        "prepare", 
        "build",
        "run"
    ]
}

In case file doesn’t exist the default fallback is:

module.exports={
    runStages:[
        "build",
        "run"
    ]
}

Note

It is possible to completely replace list of stages, but:

  • if build stage doesn’t exist LENV will not create docker-compose file
  • if run stage doesn’t exist LENV will not run containers automatically

How to add Jobs?

LENV looks for jobs at ./jobs/*.job.js

module.exports = {
  name: "check-sources", // job name
  stage: "prepare", // stage to run the job
  requires: [
      /* 
        Array of dependencies. 
        Values are names of other modules.
        (optional)
      */
  ],
  jobs: [
      /* 
        Array of jobs required for this configuration. 
        Values are names of other modules.
        (optional)
      */
  ],
  body: (params) => {
        // code to be executed inside JS function
    }
};

requires and jobs sections are identical to the same sections in modules. More information here

body is a simple JS function which receives object with parameters:

interface Params {
  /* 
      log, error and info allows to log message to the job's
      output with different level
    */
  log: (msg: string) => void,
  error: (msg: string) => void,
  info: (msg: string) => void,

    /*
        adds log to the job's output and updates job status to
      'success' 
    */
  success: (msg?: string) => void, 

    /*
        adds log to the job's output and updates job status to
      'failed' 
    */
  fail:  (msg?: string) => void,

    /*
        updates job status to 'waiting'
      user has to enter string to continue 
    */
  textInput: () => Promise<string>,

    /*
        array of all parameters passed to the job
    */
    args: any[]

  /*
        get and set artifacts
  */
  getArtifacts: () => Record<string, any>,
  updateArtifacts: (data: Record<string, any>) => void,
}

Artifacts

Artifacts are persistent storage for the jobs. They may be used in modules similar to outputs with getArtifact function.

module.exports = {
  jobs: [
    "get-user-token",
    { target: "SERIVCE_NAME" }
  ],
  docker: {
    services: {
        api: {
          /* ....... */
          environment: {
            SERVICE_TOKEN: ({ getArtifact }) => getArtifact("get-user-token.artifactNameForTheToken")
          }
          /* ....... */
        }
      }
  },
};

Job example

module.exports = {
  name: "get-token",
  stage: "prepare",
  body: async ({ textInput, updateArtifacts, success }) => {
        const token = await textInput();
        updateArtifacts({ token });
        success();
    }
};

More job examples here (TBD)

What are custom functions?

TBD

How does LENV resolve dependencies (requires, jobs)?

TBD