This topic tells you how to Author new app accelerators for Tanzu Platform.
An accelerator contains your conforming code and configurations that developers can use to create new Projects that, by default, follow the standards defined in your accelerators.
Before you begin
-
Install the Tanzu CLI
app-developer
plug-in group, if it is not present, by running:tanzu plugin install --group vmware-tanzu/app-developer
-
Log in from the Tanzu CLI by running:
tanzu login
-
Set the Project to use by running:
tanzu project use
-
Install the Tanzu Platform Developer Tools plug-in for IntelliJ.
Getting started
You can use any Git repository to create an accelerator. You need the URL of the repository to create an accelerator.
For this example, the Git repository is public and contains a README.md
file. These are options available when you create repositories on GitHub.
Create a simple accelerator Git repository
To create an accelerator based on this Git repository:
- Clone your Git repository.
- Create a file named
accelerator.yaml
in the root directory of this Git repository. -
Add the following content to the
accelerator.yaml
file:accelerator: options: []
-
Create a file named
accelerator.axl
in the root directory of this Git repository. -
Add the following content to the
accelerator.axl
file:engine { Include({"**"}) }
-
Add the new
accelerator.yaml
andaccelerator.acl
files, commit this change, and then push to your Git repository.
Publish this new accelerator to your Tanzu Project
To publish this new accelerator to your Tanzu Project:
- Create a manifest file. In this example it is named
simple-manifest.yaml
. You can create this file anywhere and you do not have to commit the file to your Git repository. -
Add the following content to the
simple-manifest.yaml
file:apiVersion: accelerator.tanzu.vmware.com/v2 kind: Accelerator metadata: name: simple-accelerator spec: displayName: Simple Accelerator description: Contains just a README iconURL: https://images.freecreatives.com/wp-content/uploads/2015/05/smiley-559124_640.jpg tags: - simple - getting-started git: ref: branch: main url: GIT_REPOSITORY_URL
- You can use a different icon if it uses a reachable URL.
- Replace
GIT_REPOSITORY_URL
with the URL for your Git repository.
-
Publish this accelerator by running:
tanzu accelerator apply -f simple-manifest.yaml
Verify the accelerator was published successfully
To verify that the accelerator was published successfully, list accelerators by running:
tanzu accelerator list --from-context
Create an accelerator.yaml file for a new App Accelerator
This section tells you how to create an accelerator.yaml
file when using App Accelerators for Tanzu Platform.
By including an accelerator.yaml
file in your Accelerator repository, you can declare input options that users fill in by using a form in the UI. These option values control processing by the template engine before it returns the zipped output files. For more information, see the later Sample accelerator section.
When there is no accelerator.yaml
, the repository still works as an accelerator, but the files are passed unmodified to users.
accelerator.yaml
has one top-level section (accelerator
) and two sub-sections (options
and imports
).
Accelerator options
This section documents how an accelerator is presented to users in the IDE UI.
For example:
accelerator:
options:
- name: deploymentType
inputType: select
choices:
- value: none
text: Skip deployment configuration
- value: cf-manifest
text: Cloud Foundry manifest
- value: tp-k8s
text: Tanzu Platform ContainerApp
defaultValue: tp-k8s
required: true
The list of options is passed to the UI to create input text boxes for each option.
The following option properties are used by both the UI and the engine.
-
name:
Each option must have a unique camelCase name. The option value entered by a user is made available as a SPeL variable name. For example,
#deploymentType
.You can specify your own default name by including:
options: - name: projectName label: Name inputType: text defaultValue: myname required: true
-
dataType:
Data types that work with the UI are:
string
boolean
number
- Custom types defined in the accelerator
types
section - Arrays of these, such as
[string]
,[number]
, and so on
Most input types return a string, which is the default. Use Boolean values with
checkbox
. -
defaultValue:
This literal value pre-populates the option. Ensure that its type matches the
dataType
. For example, use["text 1", "text 2"]
for thedataType
[string]
. Options without adefaultValue
can trigger a processing error if the user does not provide a value for that option. -
validationRegex:
When present, a regular expression validates the string representation of the option value when set. The regular expression does not apply when the value is blank. As a consequence, do not use the regular expression to enforce prerequisites. For that purpose, see the required option property described later in this section.
This regular expression is used in several layers in Application Accelerator, built using several technologies, such as JavaScript and Java. So, refrain from using unusual regex features.
The regular expression applies to portions of the value by default. That is,
[a-z ]+
matchesHello world
despite the capitalH
. To apply the regular expression to the whole value (or just the start or end), anchor it by using^
and$
.Finally, backslashes in a YAML string using double quotation marks must be escaped. Therefore, to match a number write
validationRegex: "\\d+"
or use another string style.
The following option properties are for UI purposes only:
-
label: A human-readable version of the
name
identifying the option. -
description: A tooltip to accompany the input.
-
inputType:
text
: The default input typetextarea
: Single text value with larger input allowing line breakscheckbox
: Ideal for Boolean values or multi-value selection from choicesselect
: Single-value selection from choices using a drop-down menuradio
: Alternative single-value selection from choices using buttons
-
choices: This is a list of predefined choices. Users can select from the list in the UI. Choices are supported by
checkbox
,select
, andradio
.Each choice must have a
text
property for the displayed text and avalue
property for the value that the form returns for that choice. The list is presented in the UI in the same order as it is declared inaccelerator.yaml
. -
required:
true
forces users to enter a value in the UI. -
dependsOn: This is a way to control visibility by specifying the
name
and optionalvalue
of another input option. When the other option has a value exactly equal tovalue
, ortrue
if novalue
is specified, then the option withdependsOn
is visible.Otherwise, it is hidden. Ensure that the value matches the
dataType
of thedependsOn
option. For example, a multi-value option (dataType = [string]
), such as acheckbox
, uses[matched-value]
to trigger another option whenmatched-value
(and onlymatched-value
) is selected. See the following section for more information aboutdependsOn
.
DependsOn and multi-value dataType
dependsOn
tests for strict equality, even for multi-valued options. This means that a multi-valued option must not be used to trigger several other options unfolding, one for each value. Instead, use several single-valued options:
For example, do not use this:
options:
- name: toppings
dataType: [string]
inputType: checkbox
choices:
- value: vegetables
text: Vegetables
- value: meat
text: Meat
...
- name: vegType
dependsOn:
name: toppings
value: [vegetables] # or vegetables, this won't do what you want either
- name: meatType
dependsOn:
name: toppings
value: [meat]
...
Instead, for example, use this:
options:
- name: useVeggies
dataType: boolean
inputType: checkbox
label: Vegetables
- name: useMeat
dataType: boolean
inputType: checkbox
label: Meat
- name: vegType
dependsOn:
name: useVeggies
value: true
- name: meatType
dependsOn:
name: useMeat
value: true
...
Examples
The following screen capture, and the accelerator.yaml
file snippet that follows, demonstrates each inputType
. You can also see the GitHub sample demo-input-types.
accelerator:
displayName: Demo Input Types
description: "Accelerator with options for each inputType"
iconUrl: https://raw.githubusercontent.com/vmware-tanzu/application-accelerator-samples/main/icons/icon-cloud.png
tags: ["demo", "options"]
options:
- name: text
display: true
defaultValue: Text value
- name: toggle
display: true
dataType: boolean
defaultValue: true
- name: dependsOnToggle
label: 'depends on toggle'
description: Visibility depends on the value of the toggle option being true.
dependsOn:
name: toggle
defaultValue: text value
- name: textarea
inputType: textarea
display: true
defaultValue: |
Text line 1
Text line 2
- name: checkbox
inputType: checkbox
display: true
dataType: [string]
defaultValue:
- value-2
choices:
- text: Checkbox choice 1
value: value-1
- text: Checkbox choice 2
value: value-2
- text: Checkbox choice 3
value: value-3
- name: dependsOnCheckbox
label: 'depends on checkbox'
description: Visibility depends on the checkbox option containing exactly value value-2.
dependsOn:
name: checkbox
value: [value-2]
defaultValue: text value
- name: select
inputType: select
display: true
defaultValue: value-2
choices:
- text: Select choice 1
value: value-1
- text: Select choice 2
value: value-2
- text: Select choice 3
value: value-3
- name: radio
inputType: radio
display: true
defaultValue: value-2
choices:
- text: Radio choice 1
value: value-1
- text: Radio choice 2
value: value-2
- text: Radio choice 3
value: value-3
Create an accelerator.axl file for a new App Accelerator
The accelerator.axl
file describes how to take the files from the accelerator repository root directory and transform them into the contents of a generated Project. The transformation operates on the files as a set and can do things such as:
- Filter the set of files: Remove or keep only files that match specific criteria.
- Change the contents of a file: For example, replace strings within the file.
- Rename or move files: Change the paths of the files.
accelerator.axl example
The following snippet shows an example accelerator.axl
file:
engine {
let includePoms = "#buildType == 'Maven'",
includeGradle = "#buildType == 'Gradle'" in {
Include({"**/*.md" , "**/*.xml" , "**/*.gradle" , "**/*.java"})
Exclude({"**/secret/**"})
if (includeGradle) {
Include({"*.gradle"})
}
+ if (includePoms) {
Include({"pom.xml"})
}
+ Include({"**/*.java", "README.md"}).ReplaceText(substitutions: {{text: "Hello World!", with: #greeting}})
UniquePath(Fail)
RewritePath("(.*)simpleboot(.*)", #g1 + #packageName + #g2)
ReplaceText({{text: "simpleboot", with: #packageName}})
}
}
accelerator.axl description
This section explains the transforms used in the preceding example:
-
engine
is the global transform.engine
produces the final set of files to be zipped and returned from the accelerator. As input, it receives all the files from the accelerator repository root. The properties in this node dictate how this set of files is transformed into a final set of files zipped as the accelerator result. -
Include
filters the set of files, retaining only those files that match a list of path patterns. This ensures that the accelerator only detects files in the repository that match the list of patterns. -
Exclude
further restricts which files are detected. The example ensures files in any directory calledsecret
are never detected. -
let
defines additional variables and assigns them values. These derived symbols function like options, but instead of being supplied from a UI widget they are computed by the accelerator itself. -
+
executes each of its children in parallel. Each child receives a copy of the current set of input files. These are the files that remain after applying theinclude
andexclude
filters.Therefore, each child produces a set of files. All the files from all the children are then combined, as if overlaid on top of each other in the same directory. If more than one child produces a file with the same path, the transform resolves the conflict by dropping the file content from the earlier child and keeping the content from the later child.
-
UniquePath
specifies how a conflict is handled when an operation, such as merging, produces multiple files at the same path:Fail
raises an error when there is a conflict.UseFirst
keeps the content of the first file.UseLast
keeps the content of the last file.Append
keeps both by usingcat FIRST-FILE SECOND-FILE
.
Advanced accelerator use
There are advanced features that you can use when writing an accelerator.yaml
. For more information see, Use custom types in Application Accelerator.
Sample accelerator options and engine processing files
This section provides you with sample accelerator files to get you started writing your own accelerators by using App Accelerators for Tanzu Platform.
Sample accelerator.yaml
Sample accelerator.yaml
file:
accelerator:
# The `accelerator` section serves to document how an accelerator is presented to the
# user in the accelerator web UI.
# displayName: a descriptive human-readable name. Make this short so as to look nice
# in a list of many accelerators shown to a user.
displayName: Hello Spring Boot
# description: a more detailed description that a user can see if they took a closer
# look at a particular accelerator.
description: Simple Hello World Rest Service based on Spring Boot
# iconUrl: Optional, a nice colorful, icon for your accelerator to make it stand out visually.
iconUrl: https://raw.githubusercontent.com/vmware-tanzu/application-accelerator-samples/main/icons/icon-cloud.png
# tags: A list of classification tags. The UI allows users to search for accelerators based on tags
tags:
- Java
- Spring
- Function
# options are parameters that can affect how the accelerator behaves.
# The purpose of the options section is
# - to list all applicable options
# - describe each option in enough detail so that the UI can create
# a suitable input widget for it.
options: # a list of options
# a first option
- name:
greeting
# name: each option must have a name.
# This must be
# - camelCase
# - unique (i.e. no two options can have the same name)
# This is like a variable used by the accelerator to refer to
# and use the value during its execution.
# This name is internal to your accelerator and is not shown to
# the user.
label:
Greeting Message
# A human readable version of the `name`. This is used to identify an
# option to the user in the UI.
# This should be short (so as not to look ugly in a ui with limited
# space available for labeling the input widgets).
# There are no limits on what characters can be used in the label (so spaces
# are allowed).
description:
Greeting message displayed by the Hello World app.
# An optional more detailed description / explanation that can be shown to
# to the user in the UI when the short label alone might not be enough to understand
# its purpose.
dataType:
string
# type of data the accelerator expects during execution (this is
# like the type of the 'variable'.
# possible dataTypes are string, boolean, number or [string] (the latter meaning a
# list of strings
inputType:
text
# Related to the dataType but somewhat independent, this identifies the type
# of widget shown in the ui. Available types are:
# - text - the default
# - textarea (single text value with larger input that allows linebreaks)
# - checkbox - multivalue selection from choices
# - select - single value selection from choices
# - radio - alternative single value selection from choices
# - tag - multivalue input ui for entering single-word tags
required: true
defaultValue: Hello Accelerator
# second option:
- name: packageName
label: "Package Name"
description: Name of Java package
dataType: string
inputType: text
defaultValue: somepackage
# another option:
- name: buildType
label: Build Type
description: Choose whether to use Maven or Gradle to build the project.
dataType: string
inputType: select
choices:
- value: Maven
text: Maven (pom.xml)
- value: Gradle
text: Gradle (build.gradle)
Annotated sample accelerator.axl
An annotated sample accelerator.axl
file:
// this is the 'global' transform. It produces the final set of
// files to be zipped and returned from the accelerator.
// As input it receives all the files from the accelerator repo root.
engine {
// 'let' defines additional variables and assign them values
// These 'derived symbols' function much like options, but instead of
// being supplied from a UI widget, they are computed by the accelerator itself.
let includePoms = "#buildType == 'Maven'",
includeGradle = "#buildType == 'Gradle'" in {
// This defined `include` filters the set of files
// retaining only those matching a given list of path patterns.
// This can ensure that only files in the repo matching the list of
// patterns will be seen / considered by the accelerator.
Include({"**/*.md" , "**/*.xml" , "**/*.gradle" , "**/*.java"})
// This defined `exclude` further restricts what files are considered.
// This example ensures files in any directory called `secret` are never considered.
Exclude({"**/secret/**"})
// This merge section executes each of its children 'in parallel'.
// Each child receives a copy of the current set of input files.
// (i.e. the files that are remaining after considering the `include` and `exclude`.
// Each of the children thus produces a set of files.
// Merge then combines all the files from all the children, as if by overlaying them on top of each other
// in the same directory. If more than one child produces a file with the same path,
// this 'conflict' is resolved by dropping the file contents from the earlier child
// and keeping only the later one.
// merge child 1: this child node wants to contribute 'gradle' files to the final result
if (includeGradle) {
Include({"*.gradle"})
}
// merge child 2: this child wants to contribute 'pom' files to the final result
+ if (includePoms) {
Include({"pom.xml"})
}
// merge child 3: this child wants to contribute Java code and README.md to the final result
// Using the dot operator it ensures that the substitutions are done first before merging the file set
+ Include({"**/*.java", "README.md"}).ReplaceText(substitutions: {{text: "Hello World!", with: #greeting}})
// other values are `UseFirst`, `UseLast`, or `Append`
// when merging (or really any operation) produces multiple files at the same path
// this defines how that conflict is handled.
// Fail: raise an error when conflict happens
// UseFirst: keep the contents of the first file
// UseLast: keep the contents of the last file
// Append: keep both as by using `cat <first-file> > <second-file>`).
UniquePath(Fail)
RewritePath("(.*)simpleboot(.*)", #g1 + #packageName + #g2)
ReplaceText({{text: "simpleboot", with: #packageName}})
}
}
Use transforms in Application Accelerator
This section tells you about using transforms with App Accelerators for Tanzu Platform.
When the accelerator engine executes the accelerator, it produces a set of files. The purpose of the accelerator.axl
file is to describe precisely how that set of files is created.
Example accelerator.yaml
:
accelerator:
# Describes options, custom types and imports
...
Example accelerator.axl
:
engine {
// Describes what happens to the files of the accelerator
}
Purpose of transforms
When you run an accelerator, the content of the accelerator produces the result. The content consists of subsets of the files taken from the accelerator <root>
directory and its subdirectories. You can copy the files as they are or transform them in several ways before adding them to the result.
The Domain Specific Language (DSL) notation in the engine
section defines a transformation that takes a set of files (in the <root>
directory of the accelerator) as input. It then produces another set of files as output, which ultimately constitute the result.
Every transform has a type
. Different types of transform have different behaviors and different properties that control precisely what they do.
In addition to this, the accelerator DSL uses some keyword-style syntax to make some particular transforms first-class citizens (such as if()
or merge with T1 + T2
). But at its core, the behavior of the engine is functional: Something comes in, a transformation is applied, and then something comes out.
In the following example, a transform of type Include
is a filter. The transform takes a set of files as input and produces a subset of those files as output, retaining only the files whose path matches any one of a list of patterns
.
If the accelerator has something like this:
engine {
Include(patterns: {'**/*.java'})
}
then this accelerator produces a result containing all the .java
files from the accelerator <root>
or its subdirectories, but nothing else.
Transforms can also operate on the content of a file, instead of merely selecting it for inclusion.
For example:
engine {
ReplaceText(substitutions: {{text: "hello-fun", with: #artifactId }})
}
This transform searches for all instances of a string (hello-fun
) in all its input files and then replaces them with an artifactId
, which is the result of evaluating a Spring Expression Language (SpEL) expression.
The general syntax for using a transform of type MyTransform
is to write MyTransform()
. This is referred to as using the transform constructor.
If the transform can be parameterized with configuration properties, you can pass those typed properties in the constructor call. For example:
Include(patterns: {'**/*.java'})
You can pass properties by name like in the preceding example, or in order like in this example:
Include( {'**/*.java'} )
To learn more about the existing transforms and their configuration properties, see the Transforms reference.
Combining transforms
From the preceding examples, you can see that transforms such as ReplaceText
and Include
are too primitive to be useful by themselves. They are meant to be the building blocks of more complex accelerators.
To combine transforms, Application Accelerators rely on two operators called Chain
and Merge
. These operators are recursive in the sense that they compose several child transforms to create a more complex transform. This allows building arbitrarily deep and complex trees of nested transform definitions.
The following example shows what each of these two operators does and how they are used together.
Chain
Because transforms are functions whose input and output are of the same type (a set of files), you can take the output of one function and feed it as input to another. This is what Chain
does. In mathematical terms, Chain
is function composition.
You might, for example, want to do this with the ReplaceText
transform. Used by itself, it replaces text strings in all the accelerator input files. If you want to apply this replacement to only a subset of the files, you can use an Include
filter to select only a subset of files of interest and chain that subset into ReplaceText
.
To use chains in the DSL syntax, write transform A
followed by transform B
:
engine {
Include(patterns: {'**/*.java'})
ReplaceText(substitutions: {{text: "hello-fun", with: #artifactId }})
}
Merge
Chaining Include
into ReplaceText
limits the scope of ReplaceText
to a subset of the input files. This action also eliminates all other files from the result.
For example:
engine {
Include(patterns: {'**/pom.xml'})
ReplaceText(substitutions: {{text: "hello-fun", with: #artifactId }})
}
The preceding accelerator produces a Project that only contains pom.xml
files and nothing else.
If you want other files in that result, such as some Java files, but do not want to apply the same text replacement to them, you might expect this to work:
engine {
Include(patterns: {'**/pom.xml'})
ReplaceText(substitutions: {{text: "hello-fun", with: #artifactId }})
Include(patterns: {'**/*.java'})
}
However, that does not work. If you chain non-overlapping include
s together like this, the result is an empty result set. The cause is the first include
retaining only pom.xml
files. These files are fed to the next transform in the chain. The second include
only retains .java
files, but because there are only pom.xml
files retained in the input, the result is an empty set.
This is where Merge
comes in. A Merge
takes the outputs of several transforms executed independently on the same input sourceset
and combines or merges them together into a single sourceset
. The way to write merges in the DSL syntax is to use the +
operator, symbolizing the union of the result sets.
For example:
engine {
{
Include(patterns: {'**/pom.xml'})
ReplaceText(substitutions: {{text: "hello-fun", with: #artifactId }})
}
+ Include(patterns: {'**/*.java'})
}
The preceding accelerator produces a result that includes both:
- The
pom.xml
files with some text replacements applied to them. - Verbatim copies of all the
.java
files.
Thinking “Chain First” with the applyTo operator
Combining effects on some files, such as all pom.xml
files, and then doing something else with other files, such as all *.java
files, might be cumbersome if you only use chains and merges.
You would need to create an N+1 merge construct, where each of the N transformations apply specifically to the files of interest, and the last one brings the rest of files that were not in any of the N first inclusions.
To avoid this, you can use the applyTo()
operator sequentially. For example:
engine {
applyTo('**/pom.xml') {
ReplaceText(substitutions: {{text: "hello-fun", with: #artifactId }})
}
applyTo('**/*.java') {
OpenRewriteRecipe('org.openrewrite.java.ChangePackage',
{ oldPackageName: 'com.acme',
newPackageName: #companyPkg }
)
}
}
applyTo
constructs the complex Merge, Include, Exclude
equivalent setup for you. The inner transform (ReplaceText
in the first example) is only applied to the files selected (**/pom.xml
in the example), while the other files (everything that is not a pom.xml
in the example) carry through unchanged to the next transform down the line.
Conditional transforms
You can wrap transforms, or sequences of transforms, inside an if()
construct. For example:
engine {
if (#k8sConfig == 'k8s-resource-simple') {
Include({"kubernetes/app/*.yaml"})
ReplaceText({{text: 'hello-fun', with: #artifactId}})
}
}
When an if
condition is false
, that transform is deactivated. This means it is replaced by a transform that does nothing. However, doing nothing can have different meanings depending on the context:
-
When in the context of a
Merge
, a deactivated transform behaves like something that returns an empty set. AMerge
adds things together using a kind of union. Adding an empty set to a union does nothing. -
When in the context of a
Chain
, a deactivated transform behaves like theidentity
function instead, that is,lambda (x) => x
. When you chain functions together, a value is passed through all functions in succession.Therefore each function in the chain can do something by returning a different modified value. If you use a function in a chain, to do nothing means to return the input unchanged as the output.
Merge conflict
The representation of the set of files that transforms operate on is richer than what you can physically store on a file system. A key difference is that in this case, the set of files allows for multiple files with the same path to exist at the same time.
When files are initially read from the accelerator repository, this situation does not arise. However, as transforms are applied to this input, it can produce results that have more than one file with the same path and yet different content.
For example, when using a merge:
engine {
Include({"**/*}) // Transform A
+ { // Transform B
Include({**/pom.xml})
ReplaceText(...)
}
}
The result of the preceding merge
is two files with path pom.xml
, assuming there was a pom.xml
file in the input. Transform A produces a pom.xml
that is a verbatim copy of the input file. Transform B produces a modified copy with some text replaced in it.
It is impossible to have two files on a disk with the same path. Therefore, this conflict must be resolved before you can write the result to disk or pack it into a zip file.
As the example shows, merges are likely to give rise to these conflicts, so you might call this a merge conflict. However, such conflicts can also arise from other operations. For example, RewritePath
:
RewritePath(regex: '.*\.md', rewriteTo: 'docs/README.md')
This example renames any .md
file as docs/README.md
. If the input contains more than one .md
file, the output now contains multiple files with the path docs/README.md
. Again, this is a conflict, because there can only be one such file in a physical file system or zip file.
Resolving merge conflicts
By default, when a conflict arises, the engine does not do anything with it. The internal representation for a set of files allows for multiple files with the same path. The engine resumes manipulating the files as is. This is not a problem until the files must be written to disk or a zip file. If a conflict is still present at that time, an error is raised.
If your accelerator produces such conflicts, they must be resolved before writing files to disk. VMware provides the UniquePath transform. This transform enables you to specify what to do when more than one file has the same path. For example:
engine {
RewritePath(regex: '.*\.md', rewriteTo: 'docs/README.md')
UniquePath(strategy: Append)
}
The result of this transform is that all .md
files are gathered up and concatenated into a single file at path docs/README.md
. Another possible resolution strategy is to keep only the content of one of the files. For more information, see Conflict Resolution.
File ordering
As mentioned earlier, the set of files representation is richer than the files on a typical file system in that it allows for multiple files with the same path. Another way in which it is richer is that the files in the set are ordered. That is, a FileSet
is more like an ordered list than an unordered set.
In most situations, the order of files in a FileSet
does not matter. However, in conflict resolution it is significant. If you look at the preceding RewritePath
example again, you might wonder about the order in which the various .md
files are appended to each other. This ordering is determined by the order of the files in the input set.
In general, when files are read from disk to create a FileSet
, you cannot assume a specific order. The files are read and processed in a sequential order, but the actual order is not well defined. It depends on implementation details of the underlying file system.
The accelerator engine therefore does not ensure a specific order in this case. The accelerator engine only ensures that it preserves whatever ordering it receives from the file system, and processes files in accordance with that order.
If you do not want the file order produced from reading directly from a file system, and want to control the order of the sections in the README.md
file, change the order of the merge children. Merge
processes its children in order and reflects this order in the resulting output.
For example:
engine {
Include({'README.md'})
+ {
Include({'DEPLOYMENT.md'})
RewritePath(rewriteTo: 'README.md')
}
UniquePath(Append)
}
In this example, README.md
from the first child of merge
comes before DEPLOYMENT.md
from the second child of merge
.
Next steps
This introduction focused on an intuitive understanding of the <transform-definition>
notation. This notation defines precisely how the accelerator engine generates new Project content from the files in the accelerator root.
For more information, see:
- The reference topic for all built-in transform types.
- A sample commented accelerator to learn from a concrete example.
Use fragments in Application Accelerator
This section tells you how to use fragments when using App Accelerators for the Tanzu Platform.
Introduction
Despite their benefits, writing and maintaining accelerators can become repetitive and verbose as new accelerators are added. Some create a Project different from the next with similar aspects, requiring some form of copying and pasting.
To alleviate this concern, Application Accelerators support a feature named Composition that allows the re-use of parts of an accelerator called fragments.
Introducing fragments
A fragment appears exactly the same as an accelerator:
- A fragment is made of a set of files.
- A fragment contains an
accelerator.yaml
descriptor with options, declarations, and anaccelerator.axl
file that describes its root transform.
There are differences, however:
- Fragments are declared to the system differently. They are filed as fragment custom resources.
- They deal with files differently. Because fragments deal with their own files and files from the accelerator using them, they use dedicated conflict resolution strategies.
Fragments are similar to functions in programming languages in that after being defined and referenced, they are called at various points in the main accelerator. The composition feature is designed with ease of use and common-use-first in mind, so these functions are typically called with as little noise as possible. You can also think of them as complex or different values.
Composition relies on two building blocks that work hand in hand:
- The
imports
section at the top of an accelerator manifest. - The
InvokeFragment
transform that is used alongside any other transform.
The imports section explained
To be usable in composition, a fragment must be imported to the dedicated section of an accelerator manifest. For example:
accelerator:
name: my-awesome-accelerator
options:
- name: flavor
dataType: string
defaultValue: Strawberry
imports:
- name: my-first-fragment
- name: another-fragment
The effect of importing a fragment this way is twofold:
- It makes the files available to the engine (therefore importing a fragment is required).
- It exposes all of the options as options of the accelerator, as if they were defined by the accelerator itself.
So, in the earlier example, if the my-first-fragment
fragment has the following accelerator.yaml
file then the YAML looks like this:
accelerator
name: my-first-fragment
options:
- name: optionFromFragment
dataType: boolean
description: this option comes from the fragment
...
Then it is as if the my-awesome-accelerator
accelerator defined it:
accelerator:
name: my-awesome-accelerator
options:
- name: flavor
dataType: string
defaultValue: Strawberry
- name: optionFromFragment
dataType: boolean
description: this option comes from the fragment
imports:
- name: my-first-fragment
- name: another-fragment
All the metadata about options (type, default value, description, choices if applicable, and so on) come along when imported.
Because of this, users are prompted for a value for those options that come from fragments, as if they were options of the accelerator.
Using the InvokeFragment transform
The second part at play in the composition is the InvokeFragment
transform.
As with any other transform, it can be used anywhere in the engine
tree and receives files that are visible at that point. Those files, alongside those that constitute the fragment, are made available to the fragment logic.
If the fragment wants to choose between two versions of a file for a path, two new conflict resolution strategies are available (FavorForeign
and FavorOwn
).
The behavior of the InvokeFragment
transform is straightforward. After having validated options that the fragment expects (and maybe after having set default values for options that support one), the transform executes the whole transform of the fragment as if it were written in place of InvokeFragment
.
For explanations, examples, and configuration options, see the InvokeFragment
reference. This topic now focuses on additional features of the imports
section that are seldom used but still available to cover more complex use cases.
Back to the imports section
The complete definition of the imports
section is as follows, with features in increasing order of complexity:
accelerator:
name: ...
options:
- name: ...
...
imports:
- name: some-fragment
- name: another-fragment
expose:
- name: "*"
exposeTypes:
- name: "*"
- name: yet-another-fragment
expose:
- name: someOption
- name: someOtherOption
as: aDifferentName
exposeType:
- name: SomeType
- name: SomeOtherType
as: ADifferentName
As shown earlier, the imports
section calls a list of fragments to import. By default, all their options and types become options or types of the accelerator. Those options appear after the options defined by the accelerator, in the order the fragments are imported in.
A fragment can even import another fragment. The semantics are the same as when an accelerator imports a fragment. This is a way to break a fragment apart even further if needed.
When importing a fragment, you can select which options of the fragment to make available as options of the accelerator.
Only use this feature when a name clash arises in option names.
The semantics of the expose
block are as follows:
-
For every
name
-as
pair, do not use the original (name
) of the option. Instead, use the alias (as
). Other metadata about the option is left unchanged. -
If the special
name: "*"
appears (which is not a valid option name usually), all imported option names that are not remapped might be exposed with their original name. The index at which the*
appears in the YAML list is irrelevant. -
The default value for
expose
is[{name: "*"}]
. In other words, by default it exposes all options with their original name. -
As soon as a single remap rule appears, the default is overridden. For example, to override some names AND expose the others unchanged, the
*
must be explicitly re-added. -
To explicitly un-expose all options from an imported fragment, an empty array can be used and override the default:
expose: []
.
Similarly, you can also select which custom types of the fragment to make available as types of the accelerator.
Only use this feature when a name clash arises in types names.
The semantics of the exposeTypes
block are as follows:
-
For every
name
-as
pair, do not use the original (name
) of the type. Instead, use the alias (as
). Options that used the original name are automatically rewritten to use the new name. -
If the special
name: "*"
appears, which is not usually a valid type name, all imported other type names that are not remapped are exposed with their original name. The index at which the*
appears in the YAML list is irrelevant. -
The default value for
exposeTypes
is[{name: "*"}]
. That is, by default it exposes all types with their original name. -
As soon as a single remap rule appears, the default is overridden. For example, to override some names and expose the others unchanged, the
*
must be explicitly re-added. -
To explicitly un-expose all types from an imported fragment, you can use an empty array that overrides the default:
exposeTypes: []
.
Using dependsOn in the imports section
Lastly, as a convenience for the conditional use of fragments, you can make an exposed imported option depend on another option. For example:
imports:
- name: tap-initialize
expose:
- name: gitRepository
as: gitRepository
dependsOn:
name: deploymentType
value: workload
- name: gitBranch
as: gitBranch
dependsOn:
name: deploymentType
value: workload
This works well with the use of if
, as in this example:
engine {
...
if (#deploymentType == 'workload') {
InvokeFragment('tap-initialize')
}
...
Discovering fragments by using the Tanzu CLI accelerator plug-in
By using the accelerator plug-in for the Tanzu CLI, view a list of available fragments by running:
tanzu accelerator fragment list
Example output:
NAME READY REPOSITORY
app-sso-client true source-image: registry.example.com/app-accelerator/fragments/app-sso-client@sha256:ed5cf5544477d52d4c7baf3a76f71a112987856e77558697112e46947ada9241
java-version true source-image: registry.example.com/app-accelerator/fragments/java-version@sha256:df99a5ace9513dc8d083fb5547e2a24770dfb08ec111b6591e98497a329b969d
live-update true source-image: registry.example.com/app-accelerator/fragments/live-update@sha256:c2eda015b0f811b0eeaa587b6f2c5410ac87d40701906a357cca0decb3f226a4
spring-boot-app-sso-auth-code-flow true source-image: registry.example.com/app-accelerator/fragments/spring-boot-app-sso-auth-code-flow@sha256:78604c96dd52697ea0397d3933b42f5f5c3659cbcdc0616ff2f57be558650499
tap-initialize true source-image: registry.example.com/app-accelerator/fragments/tap-initialize@sha256:7a3ae8f9277ef633200622dbf9d0f5a07dea25351ac3dbf803ea2fa759e3baac
tap-workload true source-image: registry.example.com/app-accelerator/fragments/tap-workload@sha256:8056ad9f05388883327d9bbe457e6a0122dc452709d179f683eceb6d848338d0
See all the options defined for the fragment, and also any accelerators or other fragments that import this fragment, by running:
tanzu accelerator fragment get <fragment-name>
For example:
$ tanzu accelerator fragment get java-version
name: java-version
namespace: accelerator-system
displayName: Select Java Version
ready: true
options:
- choices:
- text: Java 8
value: "1.8"
- text: Java 11
value: "11"
- text: Java 17
value: "17"
defaultValue: "11"
inputType: select
label: Java version to use
name: javaVersion
required: true
artifact:
message: Resolved revision: registry.example.com/app-accelerator/fragments/java-version@sha256:df99a5ace9513dc8d083fb5547e2a24770dfb08ec111b6591e98497a329b969d
ready: true
url: http://source-controller-manager-artifact-service.source-system.svc.cluster.local./imagerepository/accelerator-system/java-version-frag-97nwp/df99a5ace9513dc8d083fb5547e2a24770dfb08ec111b6591e98497a329b969d.tar.gz
imports:
None
importedBy:
accelerator/java-rest-service
accelerator/java-server-side-ui
accelerator/spring-cloud-serverless
This shows the options
and importedBy
with a list of three accelerators that import this fragment. Correspondingly, see the fragments that an accelerator imports by running:
tanzu accelerator get <accelerator-name>
For example:
$ tanzu accelerator get java-rest-service
name: java-rest-service
namespace: accelerator-system
description: A Spring Boot Restful web application including OpenAPI v3 document generation and database persistence, based on a three-layer architecture.
displayName: Tanzu Java Restful Web App
iconUrl: data:image/png;base64,...abbreviated...
source:
image: registry.example.com/app-accelerator/samples/java-rest-service@sha256:c098bb38b50d8bbead0a1b1e9be5118c4fdce3e260758533c38487b39ae0922d
secret-ref: [{reg-creds}]
tags:
- java
- spring
- web
- jpa
- postgresql
- tanzu
ready: true
options:
- defaultValue: customer-profile
inputType: text
label: Module artifact name
name: artifactId
required: true
- defaultValue: com.example
inputType: text
label: Module group name
name: groupId
required: true
- defaultValue: com.example.customerprofile
inputType: text
label: Module root package
name: packageName
required: true
- defaultValue: customer-profile-database
inputType: text
label: Database Instance Name this Application will use (can be existing one in
the cluster)
name: databaseName
required: true
- choices:
- text: Maven (https://maven.apache.org/)
value: maven
- text: Gradle (https://gradle.org/)
value: gradle
defaultValue: maven
inputType: select
name: buildTool
required: true
- choices:
- text: Flyway (https://flywaydb.org/)
value: flyway
- text: Liquibase (https://docs.liquibase.com/)
value: liquibase
defaultValue: flyway
inputType: select
name: databaseMigrationTool
required: true
- dataType: boolean
defaultValue: false
label: Expose OpenAPI endpoint?
name: exposeOpenAPIEndpoint
- defaultValue: ""
dependsOn:
name: exposeOpenAPIEndpoint
inputType: text
label: System API Belongs To
name: apiSystem
- defaultValue: ""
dependsOn:
name: exposeOpenAPIEndpoint
inputType: text
label: Owner of API
name: apiOwner
- defaultValue: ""
dependsOn:
name: exposeOpenAPIEndpoint
inputType: text
label: API Description
name: apiDescription
- choices:
- text: Java 8
value: "1.8"
- text: Java 11
value: "11"
- text: Java 17
value: "17"
defaultValue: "11"
inputType: select
label: Java version to use
name: javaVersion
required: true
- dataType: boolean
defaultValue: true
dependsOn:
name: buildTool
value: maven
inputType: checkbox
label: Include TAP IDE Support for Java Workloads
name: liveUpdateIDESupport
- defaultValue: dev.local
dependsOn:
name: liveUpdateIDESupport
description: The prefix for the source image repository where source can be stored
during development
inputType: text
label: The source image repository prefix to use when pushing the source
name: sourceRepositoryPrefix
artifact:
message: Resolved revision: registry.example.com/app-accelerator/samples/java-rest-service@sha256:c098bb38b50d8bbead0a1b1e9be5118c4fdce3e260758533c38487b39ae0922d
ready: true
url: http://source-controller-manager-artifact-service.source-system.svc.cluster.local./imagerepository/accelerator-system/java-rest-service-acc-wcn8x/c098bb38b50d8bbead0a1b1e9be5118c4fdce3e260758533c38487b39ae0922d.tar.gz
imports:
java-version
live-update
tap-workload
The imports
section at the end shows the fragments that this accelerator imports. The options
section shows all options defined for this accelerator. This includes all options defined in the imported fragments, such as the options for the Java version imported from the java-version
fragment.
Use SpEL with Application Accelerator
This section tells you about some common SpEL use cases in App Accelerators for Tanzu Platform.
For more information, see the Spring Expression Language documentation.
Variables
You can reference all the values added as options in the accelerator
section from the YAML file as variables in the engine
section of accelerator.axl
. You can access the value using the syntax #OPTION-NAME
.
Example accelerator.yaml
:
options:
- name: foo
dataType: string
inputType: text
...
Example accelerator.axl
:
engine {
Include({"some/file.txt"})
ReplaceText(substitutions: {{text: 'bar', with: #foo}})
}
This sample replaces every occurrence of the text bar
in the file some/file.txt
with the content of the foo
option.
Implicit variables
Some variables are made available to the model by the engine, including:
-
artifactId
is a built-in value derived from theprojectName
passed in from the UI with spaces replaced by_
. If that value is empty, it is set toapp
. -
files
is a helper object that currently exposes thecontentsOf(<path>)
method. For more information, see ReplaceText. -
camel2Kebab
, like other variations of the formxxx2Yyyy
, is a series of helper functions for dealing with changing the case of words. For more information, see ReplaceText.
Conditionals
You can use Boolean options for conditionals in your transformations.
Example accelerator.yaml
:
options:
- name: numbers
inputType: select
choices:
- text: First Option
value: first
- text: Seconf Option
value: second
defaultValue: first
Example accelerator.axl
:
engine {
if (#numbers == 'first') {
Include({"some/file.txt"})
ReplaceText({{text: "bar", with: #foo}})
}
}
This replaces the text only if the selected option is the first one.
Rewrite path concatenation
String templates are available in Application Accelerator by using backticks. These are useful, for example, when using RewritePath
.
Example accelerator.yaml
:
options:
- name: renameTo
dataType: string
inputType: text
...
Example accelerator.axl
:
engine {
Include({"some/file.txt"})
RewritePath(rewriteTo: `somewhere/#{#renameTo}.txt`)
}
Regular expressions
Regular expressions allow you to use patterns as a matcher for strings.
Example accelerator.yaml
:
options:
- name: foo
dataType: string
inputType: text
defaultValue: abcZ123
...
Example accelerator.axl
:
engine {
if (#foo.matches('[a-z]+Z\d+')) {
Include({"some/file.txt"})
ReplaceText({{text: "bar", with: #foo}})
}
}
This example uses regex to match a string of letters that ends with an uppercase Z
and any number of digits. If this condition is fulfilled, the text is replaced in the file file.txt
.
Dealing with string arrays
Options with a dataType
of [string]
come out as an array of strings.
You can use the Java static String.join()
method to use them and, for example, format the result as a bulleted list.
Example accelerator.yaml
:
accelerator:
options:
- name: meals
dataType: [string]
inputType: checkbox
choices:
- value: fish
- value: chips
- value: BLT
...
Example accelerator.axl
:
engine {
ReplaceText({{text: recipe, with: ' * ' + T(java.lang.String).join('\n * ', #meals) }})
}
Content feedback and comments