Jen - noun (in Chinese philosophy) a compassionate love for humanity or for the world as a whole.
Jen is a CLI tool for scaffolding new microservices based on Go templates, onboarding them with your CI/CD and infra, and augmenting them with your DevOps scripts for their entire life-time.
Throughout this document, the terms "scaffolding", "rendering" and "templating" are used interchangeably and all basically refer to the same idea of creating a project's general skeleton and boilerplate code from templates files.
We were not satisfied with existing project scaffolding tools, which often are language-specific and leave you on your own once your project has been generated. Many DevOps shell scripts (ie: registering your project with CI/CD and other infra, promoting it from staging to prod...) need to be maintained, organized and shared separately. They typically require similar inputs to those provided during scaffolding (ie: project name, cluster, cloud region, team...), yet you have to pass that information as arguments all over again every time you invoke them.
As DevOps, we have many concerns to address, such as:
- How do we organize and share reusable project templates?
- What about project-specific and common DevOps shell scripts?
- How do we deal with those scripts' arguments/variables?
Jen aims to provide a very simple framework to answer all those questions.
- Put all you project templates in a git repo with a specific structure, including shell scripts specific to each template or common to all.
- Jen automatically clones that repo locally upon first use.
- When scaffolding a project, Jen prompts user for which template to use and all values required by template and associated shell scripts.
- All that information gets stored in a
jen.yamlfile in the project's root dir and remains available everytime you run ajen ...command anywhere within your project's directory structure. - Use jen throughout your project's life-time to run companion shell scripts using same variables.
Download and install latest release from github.
Create a git repo to store all your templates and scripts, using the following structure:
bin(scripts common to all templates)templatesTEMPLATE_NAMEspec.yaml(defines actions/steps/variables)src(template files to render, but this dir can be named whatever you want)bin(template-specific scripts)
DevOps-oriented shell scripts can be packaged and distributed with your jen templates and can be either "shared" (all projects can use them, regardless of which template they use) or "template-specific" (only accessible when a specific template is used).
When executing any action or shell command, jen always prepends your PATH env var with the template-specific bin directory, followed by the shared one. That means you can override shared scripts at the template level by redefining scripts with the same name as shared ones.
JEN_CLONE: Local directory where jen will clone your jen git repo (defaults to~/.jen/repo)JEN_REPO: URL of your templates git repo to cloneJEN_SUBDIR: Optional sub-directory within your repo where to look for jen files. This can be useful when your git repo also contains other things.
This hello-world example is available on github. Don't hesitate to explore it there to better understand how it works (the template itself is much more instructive and interesting than the final output).
- Configure jen to point to jen's example templates and scripts:
$ export JEN_REPO=git@github.com:Samasource/jen.git
$ export JEN_SUBDIR=examples- Create a new project directory:
$ mkdir foobar
$ cd foobar
$ jen do create- If it's your first time, jen will automatically clone your templates git repo into
$JEN_HOME/repo. - Because the current dir is not initialized with jen yet, it asks for confirmation. Type
yand pressEnter. - Jen then shows a list of available templates from that repo. Right now there's only one
hello-worldexample, so just pressEnter. That choice gets saved tojen.yamlfile in current dir and identifies your project as jen-initialized. - Because the
createaction calls out to thepromptaction, you are now prompted for variable values. Answer the different prompts (notice how it automatically suggests the current dir namefoobaras default project name). Your values also get saved tojen.yamlfile. - The
createaction then callsrenderstep to render thehello-worldtemplate files to current dir. - If in previous prompts you opted for installing your project in CI/CD, the
installaction will be called now to simulate that. - At this point, typically, you would commit your project to git, including the
jen.yamlfile.
Let's have a look at our project's jen.yaml file that has just been created:
cat jen.yaml
version: 0.2.0
templatename: hello-world
vars:
INSTALL: true
NEWRELIC: true
PROJECT: foobar
PSQL: true
TEAM: devopsBut there's a dedicated command for viewing variables, which can be invoked from anywhere within your project structure:
$ jen list vars
INSTALL: true
NEWRELIC: true
PROJECT: foobar
PSQL: true
TEAM: devopsWe are now ready to call different project actions with jen do ACTION, but first let's see what actions the hello-world example defines:
$ jen list actions
create
install
prompt
uninstallWe have already discussed about create and prompt. Now, install and uninstall are meant to register/unregister your project with your CI/CD pipeline and infra, but here they just call dummy bash scripts that simulate the real thing. For example:
$ jen do install
Creating docker image repo for project foobar
Done.
Creating triggers on CI/CD pipelines for project foobar
Done.You can also run jen do without specifying action and it will prompt you for which one to execute:
$ jen do
? Select action to execute [Use arrows to move, type to filter]
> create
install
prompt
uninstallTypically, we would always go through higher-level actions to call scripts and shell commands, but we can also invoke them directly using jen exec CMD ARG1 ARG2 .... However, let's first see what scripts the hello-world example defines:
$ jen list scripts
create-cicd-triggers
create-docker-repo
remove-cicd-triggers
remove-docker-repoThese are the scripts that are present either in the templates' shared bin dir or in this template's specific bin dir, if any.
Let's try one of them:
$ jen exec remove-cicd-triggers
Removing triggers from CI/CD pipelines for project foobar
Done.Or even simpler, just run jen exec alone to let it prompt you for custom script to execute:
$ jen exec
? Select script to execute [Use arrows to move, type to filter]
> create-cicd-triggers
create-docker-repo
remove-cicd-triggers
remove-docker-repoJust keep in mind that you are not limited to custom scripts, you can execute really any shell command with the jen exec CMD ARG1 ARG2 ... syntax.
You can even start a sub-shell with your custom shell scripts added to $PATH and your project's variables as environment:
$ jen shellYou are then free to call as many shell scripts and shell commands as you want, until you do exit. For example:
$ echo $PROJECT
foobar
$ remove-cicd-triggers
Removing triggers from CI/CD pipelines for project foobar
Done.
$ exitNote that this command is just a shorthand for:
$ jen exec $SHELLWhere the $SHELL variable is typically set to your current shell, but you can also explicitly specify any of bash, zsh, sh...
To pull latest version of templates git repo:
$ jen pullWhen you're done experimenting with the examples, don't forget to delete the jen examples repo clone from your machine (at $JEN_HOME/repo or ~/.jen/repo) and to make jen point to your own template repo.
Each template has a spec.yaml file in its root that specifies how to render the template, what variables to prompt user and what actions user can invoke throughout the project's life-time.
It has this general structure:
version: 0.2.0
description: ...
actions:
ACTION1:
- STEP1: ...
- STEP2: ...
- STEP3: ...
ACTION2:
- STEP1: ...Actions are named operations that can be invoked by user via the jen do ACTION command, or as part of another action (using the do step). The order of actions is irrelevant, much like the order of function definitions within any source code.
You can have any arbitrary actions with any names in your template specs, however it is recommended to follow the convention of having at least the following actions:
create:- first invoke the
promptaction below - then render project template
- first invoke the
prompt:- prompt user for variable values
Optionally, also include those actions:
install:- register your project with CI/CD pipelines and infra
uninstall:- unregister your project from CI/CD pipelines and infra
Each action is composed of one or many steps that are executed sequentially when the action is invoked (their order is therefore important).
Steps have predefined names and purposes:
if: conditionally invokes child stepsdo: executes another action by name (much like a function call)exec: executes a shell command, including shell scripts, with project vars in environmentrender: renders template into current dir, using project varsinput: prompts user for a single free-form string varchoice: prompts user for a single string var among a list of multiple proposed choicesoption: prompts user for single boolean var as a yes/no questionoptions: prompts user for multiple boolean vars as a list of toggles
The following is the spec.yaml file of hello-world template in jen's examples:
# Version of jen file format (for future compatibility checks)
version: 0.2.0
# Description displayed to user during template selection
description: The customary Hello World example
# Actions are sets of steps that can be invoked by user by their name
actions:
# By convention, the "create" action is in charge of scaffolding the project initially
create:
# This step invokes the "prompt" action defined below
- do: prompt
# This step renders the "./src" template sub-dir into current dir
- render: ./src
# This step only executes its child steps if given expression evaluates to true
- if: .INSTALL
then:
# This step invokes the "install" action defined below
- do: install
# By convention, the "prompt" action is in charge of prompting user for project
# variables. It is typically invoked as the first step of "create" action above, but
# can also be invoked manually by user at a later time to modify variables or to
# associate a template with an existing project that was not initially generated by
# jen.
prompt:
# The "input" step prompts user for a single string variable
- input:
question: Project name
var: PROJECT
# Here we use the special "projectDirName" variable to propose to the user the
# project's directory name as default project name, which is most often the case.
default: "{{ .projectDirName }}"
# The "option" step prompts user for a single boolean variable as a yes/no question
- option:
question: Do you want to register project {{ .PROJECT }} in infrastructure?
var: INSTALL
default: true
# The "options" step prompts user for multiple boolean variables as a list of toggles
- options:
question: Select desired features
items:
- text: PostgreSQL database
var: PSQL
default: true
- text: NewRelic instrumentation
var: NEWRELIC
default: false
# The "choice" step prompts user for a single string value from a list of choices.
- choice:
question: What is your team?
var: TEAM
default: backend
items:
- text: Back End
value: backend
- text: Front End
value: frontend
- text: DevOps
value: devops
- text: Site Reliability Engineering
value: sre
- text: Customer Success Engineering
value: cse
# By convention, the "install" action is in charge of setting the project up with CI/CD
# and infra. It is typically invoked as the last step of the "create" action.
install:
# The "exec" step allows to invoke shell commands, including shell scripts, while
# passing them all project variables as env vars. In this case, it specifies a list
# of multiple commands to execute.
- exec:
- create-docker-repo
- create-cicd-triggers
# By convention, the "uninstall" action is in charge of removing the project from infra
uninstall:
# The "confirm" step is similar to "if", however it prompts user with given message and
# only upon confirmation executes steps in the "then" clause.
- confirm: Are you sure you want to completely uninstall project {{.PROJECT}} from infrastructure?
then:
# Here the "exec" step is invoked multiple times, each executing a single command
- exec: remove-docker-repo
- exec: remove-cicd-triggersJen leverages the Go templating engine described here and augments its built-in functions with the very helpful sprig function library.
Those template expressions can be used in templates, user prompts, and file/directory names, as described in following sections.
By default, all files in a template are copied as is, without rendering their content as templates. Template rendering can however be activated or deactivate selectively on a per-file/directory basis, by appending a .tmpl or .notmpl extension to file/directory names. Applying those
extensions to a directory affects all child files recursively, unless overriden down the tree. Note that the .tmpl and .notmpl extensions are automatically stripped away from target file/ directory names.
To override the no templating default, you can simply append a .tmpl extension to the name of the root directory passed to the render step, ie:
- render: ./src.tmplSometimes, it's not enough to completely turn rendering on or off for an entire file. For instance, if you need to intermix jen templating expressions with other templating that also use double-braces (ie: helm charts) within the same file, you can escape your double-braces by using {{{ and }}}, which will be rendered to {{ and }} respectively.
File and directory names can include template expressions enclosed between double-braces (ie: {{.PROJECT}}.sql)
Files and directories can be selectively included/excluded by embedding a double-square-bracket expression in their name, which must evaluate to true in order for the file/directory to be included in render.
Take the following template directory structure as example:
srcdatabase[[.DB]]migration.godriver.go
Only when the DB var evaluates to true will the database[[.DB]] directory and its content be rendered to project directory. The double-square-bracket expression will also automatically get stripped away from the target dir name:
srcdatabasemigration.godriver.go
Pure conditional directories - that is, those for which the name only contains a double-square-bracket expression - are treated as a special case. If their expression evaluates to true, they get collapsed and their contents get placed directly into parent directory.
That is very useful to group multiple files and folders under a same conditional expression, without actually introducing an extra directory level in final output. For example, given this template structure:
src[[.DB]]migration.godriver.go
If DB is true, the following structure will be rendered to target directory:
srcmigration.godriver.go
For prompt steps (input, choice, option, options), you can use template expressions within messages, proposed choices and default values, by enclosing those expressions between {{ and }}.
As the conditional for if steps is always a template expression, do not enclose them between double-braces, ie:
- if: .INSTALL
then:
- ...Placeholders are a lightweight alternative to go template expressions, which can be used as plain text anywhere in file/dir names and template files. Because placeholders are processed using plain search-and-replace, ensure they have improbable names that don't risk conflicting with anything else (ie: "projekt").
For example, you can define the following placeholders in your template spec:
placeholders:
projekt: "{{ .PROJECT | lower }}"
Projekt: "{{ .PROJECT | title }}"
PROJEKT: "{{ .PROJECT | upper }}"You can then use these placeholders anywhere without any adornments. For example, the text "MY PROJEKT FILE.TXT" is equivalent to "MY {{.PROJECT | upper}} FILE.TXT".
This feature was inspired by the way we were previously creating new projects by duplicating an existing project and doing a search-and-replace for the project name in different case variants. That strategy was very simple and effective, as long as the project name was a very distinct string that did not appear in any other undesired contexts, hence our choice of projekt as something that you are (hopefully!) very
unlikely to encounter in your project for any other reason than those placeholders!
Let's say you want developers to be able to add multiple endpoints to a microservice, each one with its own sub-dir and source files. To achieve that you simply need to put your endpoint template files in a separate sub-dir than the main template files. For example, if your project's main template files are in a project sub-dir, you could create another endpoint sub-dir with just your endpoint template files. Then simply create a standalone action that prompts user for endpoint-specific values and then renders the endpoint sub-dir using those values.
See hello-world example template for a demonstration of adding multiple endpoints to an already generated project.
The endpoint scenario described in previous section is fine, except that the files and directories you generate for each endpoint will typically not just sit there in your project. You probably also need to reference them from some parent source file. That means that for each endpoint you add to the project, you would need to insert referencing code into some existing file.
To that end, Jen supports special template files named "inserts" and marked with the .insert extension (or with .insert. anywhere in their name) that are intended to be inserted into a target file of same name (minus the .insert extension) that must already exist in project at same path location.
Each insert template file may define one or more insertion sections, each delimited by <<< START_REGEX and >>> END_REGEX lines. The START_REGEX and END_REGEX expressions are optional, but at least one of them must be specified. Those start and end regular expressions allow to find the insertion location in target file for the section's template body. For example:
<<< ^List of endpoints
Definition of endpoint {{.NAME}} for path {{.PATH}}
>>> ^$
The start regex above (^List of endpoints) serves to find first line that starts with List of endpoints, then the end regex (^$) serves to find first empty line following start line. Jen will then insert the template's body (Definition of endpoint...) before that empty line.
If you need to insert text in different locations of same file, you can specify multiple sections, each delimited by <<< and >>> markers.
All text outside delimited sections simply gets discarded/ignored.
The rules for finding insertion point is as follows:
- If you specify only start regex, insertion will happen right after first matching start line.
- If you specify only end regex, insertion will happen right before first matching end line.
- If you specify both start and end regexes, insertion will happen right before first matching end line after first matching start line.
See hello-world example template for a demonstration of inserting multiple snippets into an existing source file at a specific insertion location.
For complete regex syntax reference, see the RE2 wiki.
To make your scripts more robust and self documented, you can use the jen require VAR1 VAR2 ... command in their first few lines (typically after set -e to make script fail in case of missing variable):
#!/bin/bash
set -e
jen require PROJECT TEAM
echo "You are now garanteed that the $PROJECT and $TEAM variables can be used safely"To associate a template with an existing project that was not initially generated by jen, without doing any scaffolding, you just have to invoke the jen do prompt command in the root of the existing project. This assumes your templates follow the recommended convention of having the standard create and prompt actions (where the create action first calls prompt and then does the template rendering). In that case, calling the prompt action alone in a non-jen-initialized project will first ask you to select the template to associate the project with, and then will prompt you for variable values and save them to the jen.yaml file. From that point, your project is initialized and associated with a template. You just need to commit the jen.yaml file into git.
- Add reusable modules (including both templates and scripts).
- Add support for injecting snippets in specific sections of files in a second time (ie: adding multiple endpoints to an existing service).
- Add
jen confirm MESSAGEcommand for scripts to use for confirming dangerous operations like uninstalling (the command returns either 0 or 1, depending on whether user responds Yes or No respectively). - Add
setstep to set multiple variables. - Add
--dry-runflag (automatically turns on--verbose?). - Add regex validation for
inputprompt. - Add more example templates, for go, node...
- Fix
choicestep to pre-select current value, if any. - Allow special
.tmpland.notmplextensions to be placed before actual extension (ie:file.tmpl.txt), to allow file editor to recognize them better during template editing.