Tanzu Platform SaaS

Author new accelerators

Last Updated February 19, 2025

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

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:

  1. Clone your Git repository.
  2. Create a file named accelerator.yaml in the root directory of this Git repository.
  3. Add the following content to the accelerator.yaml file:

    accelerator:
      options: []
    
  4. Create a file named accelerator.axl in the root directory of this Git repository.

  5. Add the following content to the accelerator.axl file:

    engine {
     Include({"**"})
    }
    
  6. Add the new accelerator.yaml and accelerator.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:

  1. 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.
  2. 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.
  3. 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 the dataType [string]. Options without a defaultValue 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 ]+ matches Hello world despite the capital H. 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 type
    • textarea: Single text value with larger input allowing line breaks
    • checkbox: Ideal for Boolean values or multi-value selection from choices
    • select: Single-value selection from choices using a drop-down menu
    • radio: 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, and radio.

    Each choice must have a text property for the displayed text and a value 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 in accelerator.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 optional value of another input option. When the other option has a value exactly equal to value, or true if no value is specified, then the option with dependsOn is visible.

    Otherwise, it is hidden. Ensure that the value matches the dataType of the dependsOn option. For example, a multi-value option (dataType = [string]), such as a checkbox, uses [matched-value] to trigger another option when matched-value (and only matched-value) is selected. See the following section for more information about dependsOn.

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.

A screen capture of option inputs for sample accelerator 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 called secret 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 the include and exclude 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 using cat 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.

Diagram of a chain transform.

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 includes 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. A Merge 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 the identity 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 an accelerator.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 the projectName passed in from the UI with spaces replaced by _. If that value is empty, it is set to app.

  • files is a helper object that currently exposes the contentsOf(<path>) method. For more information, see ReplaceText.

  • camel2Kebab, like other variations of the form xxx2Yyyy, 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)  }})
}