Coding
This section is designed for people who want to contribute code things
to Lando. Specifically, that means opening a pull request against one of the many Lando respositories.
Choosing a project
Lando has a distributed model and is powered by over 80 repos so the first step is to find a repo you are interested in and check out the contribution guidelines there.
Good First Issues
Once you are set up and ready to contribute, a good place to start is with issues tagged as good first issue. Peruse the list and see if one or more of those issues is a good fit for you.
If you are having issues with any of the above or need some guidance from one of our guide pros, make sure you join our Slack org and check out the #community and/or #contributors channels.
Making your first pull request
- Create a fork of the repo you are working on.
- Follow the GitHub flow.
- Make sure you complete any requested/applicable tasks in the PR template.
- Make sure all status checks and tests pass.
- Ping the
#contributors
channel in Slack for any needed PR review or any other help you may need advancing your PR. - When all checks, tests and reviews are in the green ping a
maintainer
and/or apply the flag label.
If you have any trouble, need clarification, guidance, help, etc jump into the #contributors
channel in Slack and another contributor will gladly help you out!
After we merge a few pull requests we may grant you write
access to the repository which will allow you to open branches against the repo directly instead of using your fork.
PR Conventions
We are pretty wild west on accepting PRs but here are some good conventions that we def recommend:
- Create branches named something like
ISSUE-NUMBER-BRIEF-DESCRIPTION
for example35-ssh-agent-feature
- Create commit message named something like
#ISSUE-NUMBER: description
for example#35: ensure socket is owned by you
- Associate your issue or issues with the PR
GitHub is able to do some magic with the above and provide better context around commits, issues, PRs and projects.
Writing Plugins
PROBABLY BEST TO WAIT
We are in the process of documenting the Lando 4 API so if you want to write a new plugin we highly recommend waiting for that. We've kept these docs mostly as a courtesy for older Lando 3 Plugin creators.
Lando has an advanced plugin system that allows developers to add and extend Lando's core functionality. A few examples of things you can do with plugins are shown below:
- Create tasks and CLI commands like
lando danceparty
- Create services like
solr
,redis
orruby
- Create recipes like
drupal9
orrubyonrails
- Add sources from which you can initialize your site
- Add initialization wizards for recipes
- Add
bash
orsh
scripts into your services - Augment or alter the
lando
object and its configuration - Hook into various
lando
andapp
runtime events to provide additional functionality.
You can see examples of plugins in the plugins library.
Plugin Loading
Lando will search in the plugins
directory for any path listed in lando.config.pluginDirs
and automatically load in any plugins that it finds. By default, these directories are the Lando source directory and ~/.lando
but note that they are configurable via the Lando global config. In order for Lando to successfully identify and automatically load your plugin, you need to have a directory named after your plugin, e.g. my-plugin
, in one of the directories mentioned above and it needs to include an index.js
.
If there are multiple occurrences of the same-named plugin, Lando will use the last one it finds. This means that lando
will prioritize user plugins over core plugins by default.
A powerful corollary to this is that individual user apps can implement plugins that override or replace core plugin behavior.
Plugins are no longer loaded from apps by default!
As of 3.0.0-rc.2
, Lando will no longer look for plugins in your app's root directory by default. We intend to eventually provide better scaffolding around grabbing and loading plugins directly from your Landofile but for now this is left to the user.
Plugin Anatomy
At the bare minimum, your plugin needs to have the following structure to be recognized and autoloaded by Lando.
./
|-- index.js
However, Lando will also look for other folders and files and automatically load in those as well. These "special" paths are shown below:
./
|-- compose Services that get autoloaded first
|-- lib Unit testable libraries that do not require the `lando` object
|-- recipes Recipes that get autoloaded
|-- scripts BASH or SH scripts that get injected into every container
|-- services Services that get autoloaded last
|-- sources Initialization sources that get autoloaded
|-- tasks Tasks that get autoloaded
|-- test Unit and functional tests
|-- types Services that get autoloaded second
|-- app.js Runs with the `lando` and `app` objects when an app is initialized
|-- index.js Required, runs with the lando` object when plugin gets loaded
Let's go into more depth about each special file below:
index.js
This file is required to exist in every Lando plugin, even if it doesn't do anything. If you do not have this file, then your plugin will not be detected and loaded.
index.js
will get required when your plugin is initially loaded. Generally, you will use this file to hook into Lando's event layer and modify the lando
object itself.
It takes the form of a function that gives you access to the Lando API. Its return
value is merged directly into the lando
object.
A fairly basic example follows:
'use strict';
module.exports = (app, lando) => {
// Log that this plugin has loaded
lando.events.on('post-bootstrap-config', () => {
lando.log.info('Custom plugin loaded!');
});
// Merge some stuff into the lando object and its config
return {
customModule: require('./lib/customMod'),
config: {
mySetting: true,
domain: 'mydomain.com',
};
};
};
app.js
app.js
will get loaded when a Lando app is initialized, e.g. app.init()
is invoked. Generally, you will use this file to hook into the app's event layer and to modify the app object itself.
It takes the form of a function that gives you access to both the App and Lando APIs. Its return
value is mixed directly into the app
object, however, only the config
, composeData
, env
, labels
can be modified.
A fairly basic example follows:
'use strict';
module.exports = (app, lando) => {
// Run my custom script on all my containers after my app starts
const buildServices = _.get(app, 'opts.services', app.services);
app.events.on('post-start', () => lando.engine.run(_.map(buildServices, service => ({
id: `${app.project}_${service}_1`,
cmd: '/helpers/myscript.sh',
compose: app.compose,
project: app.project,
}))));
// Mix in some envvars and docker labels we want to add to all our containers
return {
env: {
WOODEN_SHIPS_FREE_AND_EASY: true,
I_SAW: 'the sign',
},
labels: {
'io.lando.danger-factor': 11,
},
};
};
scripts
Nothing too fancy here. Drop a bunch of scripts ending in .sh
, e.g. myscript.sh
, into this directory and they will get mounted at /helpers
in every Lando service.
tasks
Lando will try to load any .js
file dropped into this directory as a "task" which in the CLI is manifested as a command. Generally, you will want to name the file the same as the task that should be invoked, e.g. env.js
will give you a lando env
command.
Every task has a common interface as follows:
'use strict';
module.exports = lando => ({
command: 'mycommand',
describe: 'Does some pretty crazy shit',
level: 'app',
options: {
power: {
describe: 'Get UNLIMITED POWER!!!',
alias: ['p'],
default: false,
boolean: true,
interactive: {
type: 'confirm',
default: false,
message: 'Can you handle the power?',
},
},
},
run: options => {
if (options.power) {
// Try to get our app
// Run our custom app.power method after we initialized
// NOTE: this is not a real app method, we assume the user has added it in their app.js
const app = lando.getApp(options._app.root);
return app.init().then(() => app.power());
} else {
lando.log.warn("MCFLY YOU BOJO! YOU KNOW HOVERBOARDS DON'T FLOAT ON WATER! UNLESS YOU'VE GOT POWERRRR!");
}
},
});
More details about each key are listed as follows:
command - This is required and follows the same syntax as the command
in yargs
describe - Just a basic string to describe your command
level - This corresponds to the needed Lando bootstrap level. Generally, it's fine to omit this but it's safest to set it to app
if you are unsure about what to do.
options - Options follows the syntax as options
as in yargs with one exception: interactive
. interactive
follows the inquirer interface and does include the autocomplete plugin. Lando also adds a weight
property to interface
so you can control the order with which interactive questions are asked.
run - This is the function that will run when the task is invoked. Note that you have access to both options
and lando
here.
compose, types, services
Lando services can now be automatically detected and loaded using our new builder
interface and system. All you need for this to happen is have a structure similar to below:
./
|-- compose|types|services The top level directory
|-- myservice A directory containing your service
|-- builder.js A file that implements the lando builder interface
Lando builders use inheritance so that common functionality and service types can do the same thing with way less code.
The only difference between compose
, types
and services
is that builders located in compose
are loaded before those in types
and builders located in types
are loaded before those in services
. This means that if you want a builder in services
to extend another builder you should define that parent builder in types
or compose
so it is loaded beforehand and thus available for inheritance.
The builder interface is fairly straightforward, although it's often necessary to proceed up the family tree and see how parent builders treat the various options and settings as these can vastly differ from builder to builder. This definitely involves a bit of a learning curve but once learned, it enables the construction of other powerful builders extremely quickly.
An example of the builder
interface being implemented to give Lando an apache
service is shown below:
'use strict';
// Modules
const _ = require('lodash');
// Builder
module.exports = {
name: 'apache',
config: {
version: '2.4',
supported: ['2.4'],
patchesSupported: true,
confSrc: __dirname,
defaultFiles: {
server: 'httpd.conf',
vhosts: 'default.conf',
},
remoteFiles: {
server: '/bitnami/apache/conf/httpd.conf',
vhosts: '/bitnami/apache/conf/bitnami/bitnami.conf',
},
ssl: false,
webroot: '.',
},
parent: '_webserver',
builder: (parent, config) => class LandoApache extends parent {
// id and options generally come from the users landofile
constructor(id, options = {}) {
options = _.merge({}, config, options);
// Use different default for ssl
if (options.ssl) options.defaultFiles.vhosts = 'default-ssl.conf';
// Build the default stuff here
const apache = {
image: `bitnami/apache:${options.version}`,
command: '/app-entrypoint.sh /run.sh',
environment: {
APACHE_HTTP_PORT_NUMBER: '80',
APACHE_HTTPS_PORT_NUMBER: '443',
APACHE_USER: 'www-data',
APACHE_GROUP: 'www-data',
},
ports: ['80'],
user: 'root',
volumes: [
`${options.confDest}/${options.defaultFiles.server}:${options.remoteFiles.server}`,
`${options.confDest}/${options.defaultFiles.vhosts}:${options.remoteFiles.vhosts}`,
],
};
// Send it downstream
super(id, options, {services: _.set({}, options.name, apache)});
};
},
};
Some more details about each key are shown below:
They are all required.
name - A unique string identifier.
config - An arbitrary object of metadata that is accessible in the builder
function. Generally, this is used to set defaults for options that are then interpreted in a parent builder.
parent - The name
of a builder that this builder extends.
builder - This is THE MAIN EVENT, e.g. the actual class
definition for your builder. It is a function that gets whatever you defined above in config
and should only contain a constructor
. id
and options
generally come from the user's Landofile. The contents of the constructor
are up to you but it ultimately needs to call super
with arguments that make sense to the parent
.
recipes
As in the section above, recipes
implements a similar autoload structure with one exception: you can specify an optional init.js
file.
./
|-- recipes The top level directory
|-- myrecipe A directory containing your recipe
|-- builder.js A file that implements the lando builder interface
|-- init.js An optional file that implements the lando init interface
An example of a recipe builder with helper methods removed for brevity is shown below:
'use strict';
// Modules
const _ = require('lodash');
const utils = require('./../../lib/utils');
module.exports = {
name: '_lamp',
parent: '_recipe',
config: {
confSrc: __dirname,
database: 'mysql',
php: '7.2',
via: 'apache',
webroot: '.',
xdebug: false,
},
builder: (parent, config) => class LandoLaemp extends parent {
constructor(id, options = {}) {
options = _.merge({}, config, options);
options.services = _.merge({}, getServices(options), options.services);
options.tooling = _.merge({}, getTooling(options), options.tooling);
// Switch the proxy if needed
if (!_.has(options, 'proxyService')) {
if (_.startsWith(options.via, 'nginx')) options.proxyService = 'appserver_nginx';
else if (_.startsWith(options.via, 'apache')) options.proxyService = 'appserver';
}
options.proxy = _.set({}, options.proxyService, [`${options.app}.${options._app._config.domain}`]);
super(id, options);
};
},
};
An example of the init
interface is shown below:
The sources
property is intentionally left blank but you can check the section below for more details on that.
module.exports = {
name: 'pantheon',
overrides: {
name: {
when: answers => {
answers.name = answers['pantheon-site'];
return false;
},
},
webroot: {
when: () => false,
},
},
options: lando => ({
'pantheon-auth': {
describe: 'A Pantheon machine token',
string: true,
interactive: {
type: 'list',
choices: getTokens(lando.config.home, lando.cache.get(pantheonTokenCache)),
message: 'Select a Pantheon account',
when: answers => showTokenList(answers.recipe, lando.cache.get(pantheonTokenCache)),
weight: 510,
},
},
}),
sources: [],
build: (options, lando) => {},
};
name - Required. This should, but doesn't necessarily need to, match up with the name of the recipe.
overrides - Allows you to override any of the aux options in lando init
. This can allow you to automatically set certain options like recipe
and prevent them from being displayed to the user. For example, the pantheon
source automatically sets the recipe
to also be pantheon
.
options - These are additional options you can merge into the lando init
command. They follow the same interface as discussed in the tasks
section above.
build - This is a function that will run after your source steps are executed and allows you to make any last changes before a Landofile is output. Generally, this is used to augment and modify the Lando config.
sources
Lando will try to load any .js
file dropped into this directory as a "source" which is a place Lando can get code from when running lando init
.
module.exports = {
sources: [{
name: 'github',
label: 'get codez from github',
overrides: {
recipe: {
when: answers => {
answers.recipe = 'myrecipe';
return false;
},
},
},
options: lando => ({
'github-auth': {
describe: 'A GitHub personal access token',
string: true,
interactive: {
type: 'list',
choices: parseTokens(lando.cache.get(githubTokenCache)),
message: 'Select a GitHub user',
when: answers => showTokenList(answers.source, lando.cache.get(githubTokenCache)),
weight: 110,
},
},
}),
build: (options, lando) => ([
{name: 'generate-key', cmd: `/helpers/generate-key.sh ${gitHubLandoKey} ${gitHubLandoKeyComment}`},
{name: 'post-key', func: (options, lando) => {
return postKey(path.join(lando.config.userConfRoot, 'keys'), options['github-auth']);
}},
{name: 'clone-repo', cmd: `/helpers/get-remote-url.sh ${options['github-repo']}`},
{name: 'set-caches', func: (options, lando) => setCaches(options, lando)},
]),
}],
Some more details about each key are shown below:
name - Required. A unique string identifier.
label - A human readable way to describe your source.
overrides - Allows you to override any of the aux options in lando init
. This can allow you to automatically set certain options like recipe
and prevent them from being displayed to the user. For example, the pantheon
source automatically sets the recipe
to also be pantheon
.
options - These are additional options you can merge into the lando init
command. They follow the same interface as discussed in the tasks
section above.
build - This is what will allow your new source to actually DO STUFF. It's an array of command metadata. If you specify a cmd
, it will run inside of a special init
service. If you specify a func
, it will simply invoke that function.
Plugin Examples
Check out some of our core plugins for motivation in creating your own.