Skip to content
This repository has been archived by the owner on Jul 8, 2019. It is now read-only.

Contributing

Tako Schotanus edited this page Nov 26, 2018 · 12 revisions

Contributing to the Launcher Creator Backend

This is divided into several several section. Some have generic information about the design while others will have specific instruction on how to add support for new features.

Creating a Capability

Capabilities are classes that bundle one or more Generators to add a set of features to a generated project that together implement a useful and fleshed-out use-case. Generators do, while Capabilities manage.

For example, a "Database" Capability might call on a Generator that would create a mySQL database service, while using another to create the Secret that will store the database's connection information. Yet another Generator would create the Java code based on the Vert.x framework and copy that to the user's project. The choice of database (mySQL, PostgreSQL, etc) and code language and framework (Node.js, Vert.x, Spring Boot, etc) could be options that the user can pass to the Capability. In that aspect a Capability can be as complex as you want it to be.

These are the requirements for making a Capability:

  • Capabilities are created as subfolders of the catalog/capabilities folder in the root of the Launcher Backend project
  • The folder name should be lower case and dash-separated if it contains several words (eg. "super-duper")
  • A code file named index.ts must exist
  • The code file must implement a class with the same name as the folder but now Camel Cased (and without dashes, eg. "SuperDuper")
  • The class must implement Capability.
  • It's recommended the class extends BaseCapability.
  • The class must always have the following line:
public static readonly sourceDir: string = __dirname;
  • A file named info.json must exist with the following contents:
{
  "type": "capability",
  "name": "Short description",
  "description": "Some longer description for the Capability",
  "props": [
    PropertyDefinition...
  ]
}

The Capability class

A Capability normally just calls on a bunch of Generators to do its work. All of that is done in a single method that gets called when the Capability gets "applied" to the project that we're generating. To do that it must implement Capability.

This is what the API looks like:

public async apply(resources: Resources, props?: any, extra?: any): Promise<Resources>

When called the Capability takes all the Generators it will use, prepares the properties that it will pass on to each of them and calls their apply() function one by one. The end result will be that the generated project will have all the necessary files and a Resource list with all the necessary builds, deployments, services, routes, config maps and secrets the user's project needs to run on OpenShift.

See below for more details on using this API.

Creating a Generator

Generators are classes that make changes to a user's project. Each Generator generally only makes a very specific and limited set of changes, using the UNIX philosophy of "Do one thing and do it well".

For example a Generator named "mysql" might create the necessary OpenShift/K8s Resources to set up a database service on OpenShift. Another one named "node-database" might create the code to connect to a database from Node.js.

Try not to make a Generator do too much, think of who would be maintaining the module for example. If a Generator could generate the code for all the available languages it would soon become pretty complex and all the different people or teams that have the necessary expertise for their particular language would have to collaborate on that one module. Better to create one module for each language. If there are many common elements, for example they use a lot of the same supporting non-code files, consider splitting those out into a different Generator.

So make Generators simple, split them up into several different Generators and move any remaining complexity to the Capabilities.

These are the requirements for making a Generator:

  • Generators are created as subfolders of the catalog/generators folder in the root of the Launcher Backend project
  • The folder name should be lower case and dash-separated if it contains several words (eg. "super-duper")
  • A code file named index.ts must exist
  • The code file must implement a class with the same name as the folder but now Camel Cased (and without dashes, eg. "SuperDuper")
  • The class must implement Generator.
  • It's recommended the class extends BaseGenerator.
  • The class must always have the following line:
public static readonly sourceDir: string = __dirname;
  • A file named info.json must exist with the following contents:
{
  "type": "generator",
  "name": "Short description",
  "description": "Some longer description for the Generator"
}

Adding support for a new Runtime or Platform

A "Platform Generator" is a Generator that handles the basic support requirements for a Runtime. It means that it's responsible for setting up a project to be used with that particular Runtime.

Requirements for a Platform Generator:

  • Everything that's required for all Generators.
  • A set of common files that all projects using that Runtime will need. This normally includes all configuration and build files. At also includes any code that will be shared among all Capabilities.
  • The files should contain a generic README.
  • All those files should be contained in a sub folder called files within the Generator's own folder
  • The Generator folder should be named "platform-runtime" where "runtime" is the lowercase name of the Runtime you want to implement
  • The Generator class should be named "PlatformRuntime" where "Runtime" is camel cased.
  • Update the runtime section in the lib/core/catalog/enums.json to add an entry for your new runtime.
  • Update the props.runtime.values in the info.json files of all the Capabilities that have support for your new runtime.
  • Update the code in the Capabilities that deal with runtimes. Look for runtimeByType().

The whole idea of a Platform Generator is to put as much of the common files and code together into a single place. Other Generators that want to implement a certain feature on top of that runtime would then use the Platform Generator to do most of the heavy lifting: for example they won't have to worry about the OpenShift resources anymore because in most cases what the Platform Generator has added will be sufficient. They would most likely still have a files folder but there would be a lot less files in there because you'd only need to store the files that are not available in the Platform Generator.

The Generator class

A Generator must normally do a bunch of menial tasks: generate OpenShift resources, copy files, make changes to configuration. That sort of thing. All of that is done in a single method that gets called when the Generator gets "applied" to the project that we're generating. To do that it must implement Generator.

This is what the API looks like:

public async apply(resources: Resources, props?: any, extra?: any): Promise<Resources>

When called this method allows the Generator to make changes to the project we're generating and to the OpenShift/K8s Resources list that get passed in. Files can be copied and/or generated. It's also possible to update already pre-existing files (for example to add new dependencies a Maven POM file). When the Generator also needs new Resources to be created in the user's OpenShift project it can add them to the list that gets passed in and in the end that will be sent to OpenShift.

Details for implementing a Generator or Capability class

Looking again at the apply() function:

public async apply(resources: Resources, props?: any, extra?: any): Promise<Resources>

You'll see that the function is always marked async, this allows us to simplify the implementation of the Generator/Capability using await.

It's arguments and their usage:

  • resources - An object of type Resources that makes it really easy to deal with K8s/OpenShift resources.
  • props - An free object containing the properties that can be used to tell the Generator/Capability to change its behavior. It's very recommended to not have any required properties. Make sure to create usable defaults in all cases.
  • extra - An free object the Generator/Capability can use to pass information it considers useful back up the chain. These properties can be used to convey information that can't be obtained, extracted, deduced somehow from the input properties (props).

The method returns the original resources with the Generator/Capability's changes applied (it's not necessary to create a new object, changes can be applied directly to the original)

To make live easier for the Generator/Capability developer it's really recommended that instead of implementing the Generator or Capability interface, as mentioned earlier, the Generator or Capability extends BaseGenerator or BaseCapability respectively instead. The reason for this is that it adds a set of utility methods that make it easy to perform the most common tasks:

sourceDir: string
targetDir: string
generator(generatorConstructor): Generator
copy(from?: string, to?: string): Promise<void>
filesCopied(from?: string, to?: string): Promise<boolean>
transform(pattern: string!string[], transformLine): Promise<number>
updateGav(groupId: string, artifactId: string, version: string, pomFile?: string): Promise<void>
mergePoms(sourcePom?: string, targetPom?: string): Promise<void>
  • sourceDir - The path the the Generator/Capability's own folder
  • targetDir - The path to the generated project's target folder
  • generator - The function to call when you want to apply another Generator as part of this Generator/Capability's work. The generatorConstructor is the reference to the Generator class itself, its "constructor", that you want to apply. You'll get back an instance of that Generator after which you can call its apply() method. (eg. await generator(SuperDuper).apply(resources, { 'foo': true }, extra)
  • copy - Copies all files from the given source file/folder to the destination file/folder. By default it will assume the source is the files sub folder from the Generator/Capability's own folder (ie. this.sourceDir) and the to is the generated project's target folder (ie. this.targetDir).
  • filesCopied - Returns a boolean indicating if all the indicated files have been copied. It uses the same defaults as copy(). This can be used to detect if all the files were copied during a previous execution of this or another Generator/Capability.
  • transform - Transforms the indicated files using the given transformer. The pattern can be a file path with a glob pattern, eg. src/**/*.java. It can even be an array of patterns. All paths will be taken as being relative to the generated project's target folder (ie. this.targetDir). Take a look at transformers to see which are available and how they work.
  • updateGav - Updates the GroupId, ArtifactId and Version of a Maven POM file. The pomFile is a reference to the POM file that should be updated. This path will be relative to the generated project's target folder (ie. this.targetDir). By default it will refer to the pom.xml file in the root of the target folder.
  • mergePoms - Merges the contents of the sourcePom into the targetPom. The sourcePom path will be relative to the Generator/Capability's own folder (ie. this.sourceDir) and by default will refer to merge/pom.xml. If you make sure to put your POM file that has to merged in that location you don't need to specify anything else. The targetPom is a reference to the POM file that should be updated. This path will be relative to the generated project's target folder (ie. this.targetDir). By default it will refer to the pom.xml file in the root of the target folder.

NB: Even though Capabilities have access to the same methods as Generators they really should never copy or change files by themselves, nor should they directly change the list of k8s/OpenShift resources. They should always delegate that to a Generator instead.

IMPORTANT: One thing to take into account when creating Generators is that they can be executed more than once on the same project. For example when you apply several Capabilities it's almost certain that all of them will apply a Platform Generator to set up basic Runtime support. But if doing so would create duplicates or undo previously made changes then it's the responsibility of the Generator to make sure that this will not happen!

At the same time it's perfectly possible for a Generator to be applied multiple times with different properties (perhaps differently named services are created for example). So the code will need to check if Resources with the same name already exists or if files have already been copied/created and skip doing its work if true. These situations should be silently handled, no exceptions should be thrown or (log) messages written!

Post apply

Capabilities can have an optional extra method that gets called at the end of the apply process. It will actually be called on all Capabilities that have been added so far to the project, now or at a previous time. This means that the post processing can take into account the full combination of applied Capabilities to act upon.

This is what the method looks like:

public async postApply(resources: Resources, props?: any, deployment?: any): Promise<Resources>

It's arguments and their usage:

  • resources - An object of type Resources that makes it really easy to deal with K8s/OpenShift resources.
  • props - An free object containing the properties that can be used to tell the Capability to change its behavior. It's very recommended to not have any required properties. Make sure to create usable defaults in all cases.
  • deployment - This is the final "Deployment Descriptor" which is basically a record of all Capabilities that have been applied to the project so far. With this information it is possible to re-create a project from scratch.