Templating 101

As Jinja2 is used as the templating engine it is highly recommended to familiarize yourself with Jinja2.

Here are several helpful articles:

And finally the Jinja2 Template Designer Documentation, a must read for Jinja2 template authors.

Template Configuration

The Template Configuration provides data, influences logic and control structures or points to further resources.

All data of the Template Configuration is available to the Declaration Template as python data structures and can be accessed through the ninja namespace.

Template Configuration Files

The Template Configuration is generated from Template Configuration Files.

Two data formats are supported as Template Configuration Files:

  • YAML

  • JSON

Combining Multiple Template Configuration Files is supported by AS3 Ninja, we discuss the details later.

Note

There are many Pros and Cons about JSON vs. YAML. While it is out of scope to discuss this in detail, YAML is often easier to start with for simple use-cases. Two good articles about the challenges YAML and JSON introduce for use as configuration files:

An example:

1services:
2  My Web Service:
3    type: http
4    address: 198.18.0.1
5    irules:
6      - ./files/irules/myws_redirects.iRule
7    backends:
8      - 192.0.2.1
9      - 192.0.2.2

The highlighted lines provide data which will be used in the Declaration Template to fill out specific fields, like the desired name for the service (line 2), its IP address and backend servers.

1services:
2  My Web Service:
3    type: http
4    address: 198.18.0.1
5    irules:
6      - ./files/irules/myws_redirects.iRule
7    backends:
8      - 192.0.2.1
9      - 192.0.2.2

On line 3 type: http is used to indicate the service type. This information is used in the Declaration Template logic to distinguish between types of services and apply type specific settings.

1services:
2  My Web Service:
3    type: http
4    address: 198.18.0.1
5    irules:
6      - ./files/irules/myws_redirects.iRule
7    backends:
8      - 192.0.2.1
9      - 192.0.2.2

irules on line 5 references to a list of iRule files. The logic within the Declaration Template can use this list to load the iRule files dynamically and add them to the service.

as3ninja namespace

The namespace as3ninja within the Template Configuration is reserved for AS3 Ninja specific directives and configuration values.

Here is an overview of the current as3ninja namespace configuration values.

1as3ninja:
2  declaration_template: /path/to/declaration_template_file.j2

The declaration_template points to the Declaration Template File on the filesystem. It is optional and ignored when a Declaration Template is referenced explicitly, for example through a CLI parameter.

The as3ninja namespace is accessible under the ninja namespace, as with any other data from Template Configurations.

Caution

The as3ninja namespace is reserved and might be used by additional integrations, therefore it should not be used for custom configurations.

Back to our service example:

 1as3ninja:
 2  declaration_template: ./files/templates/main.j2
 3services:
 4  My Web Service:
 5    type: http
 6    address: 198.18.0.1
 7    irules:
 8      - ./files/irules/myws_redirects.iRule
 9    backends:
10      - 192.0.2.1
11      - 192.0.2.2

We extended our Template Configuration with the declaration_template directive to point to the Declaration Template ./files/templates/main.j2. AS3 Ninja will use this Declaration Template unless instructed otherwise (eg. through a CLI parameter).

Git and the as3ninja namespace

In addition as3ninja.git is updated during runtime when using AS3 Ninja’s Git integration. It holds the below information which can be used in the Declaration Template.

 1as3ninja:
 2  git:
 3    commit:
 4      id:       commit id (long)
 5      id_short: abbreviated commit id
 6      epoch:    unix epoch of commit
 7      date:     human readable date of commit
 8      subject:  subject of commit message
 9    author:
10      name:     author's name of commit message
11      email:    author's email
12      epoch:    epoch commit was authored
13      date:     human readable format of epoch
14    branch:     name of the branch

To use the short git commit id within the Declaration Template you would reference it as ninja.as3ninja.git.commit.id_short.

Note

Git Authentication is not explicitly supported by AS3 Ninja.

However there are several options:

  1. AS3 Ninja invokes the git command with privileges of the executing user, hence the same authentication facilities apply.

  2. Implicitly providing credentials through the URL should work: https://<username>:<password>@gitsite.domain/repository

    When using Github: Personal Access Tokens can be used instead of the user password.

  3. .netrc, which can be placed in the docker container at /as3ninja/.netrc, see confluence.atlassian.com : Using the .netrc file for an example.

Merging multiple Template Configuration Files

AS3 Ninja supports multiple Template Configuration Files. This provides great flexibility to override and extend Template Configurations.

Template Configuration Files are loaded, de-serialized and merged in the order specified. Starting from the first configuration every following configuration is merged into the Template Configuration. As the de-serialization takes place before merging, JSON and YAML can be combined.

Let’s use our previous example, and add two additional Template Configuration Files. as3ninja is removed for conciseness.

 1# main.yaml
 2services:
 3  My Web Service:
 4    type: http
 5    address: 198.18.0.1
 6    irules:
 7      - ./files/irules/myws_redirects.iRule
 8    backends:
 9      - 10.0.2.1
10      - 10.0.2.2
1# internal_service.yaml
2services:
3  My Web Service:
4    address: 172.16.0.1
5    backends:
6      - 172.16.2.1
7      - 172.16.2.2
1# backends_dev.yaml
2services:
3  My Web Service:
4    backends:
5      - 192.168.200.1
6      - 192.168.200.2

main.yaml is our original example. internal_service.yaml specifies the same My Web Service and contains two keys: address and backends. backends_dev.yaml again contains our My Web Service but only lists different backends.

When AS3 Ninja is instructed to use the Template Configurations Files in the order:

  1. main.yaml

  2. internal_service.yaml

AS3 Ninja loads, de-serializes and then merges the configuration. This results in the below python dict.

 1# merged: main.yaml, internal_service.yaml
 2{
 3  'services': {
 4    'My Web Service': {
 5      'address': '172.16.0.1',
 6      'backends': ['172.16.2.1', '172.16.2.2'],
 7      'irules': ['./files/irules/myws_redirects.iRule'],
 8      'type': 'http',
 9    }
10  }
11}

'address' and 'backends' was overridden by the data in internal_service.yaml.

When AS3 Ninja is instructed to use all three Template Configurations Files in the order:

  1. main.yaml

  2. internal_service.yaml

  3. backends_dev.yaml

The resulting python dict looks as below.

 1# merged: main.yaml, internal_service.yaml, backends_dev.yaml
 2{
 3  'services': {
 4    'My Web Service': {
 5      'address': '172.16.0.1',
 6      'backends': ['192.168.200.1', '192.168.200.2'],
 7      'irules': ['./files/irules/myws_redirects.iRule'],
 8      'type': 'http',
 9    }
10  }
11}

The 'address' and 'backends' definition was first overridden by the data in internal_service.yaml and 'backends' was then again overridden by backends_dev.yaml.

Important

Please note that sequences (lists, arrays) are not merged, they are replaced entirely.

Including further Template Configurations using as3ninja.include namespace

Further Template Configuration files can be included using include within the as3ninja namespace.

Combined with the ability to merge multiple Template Configuration files, this becomes a powerful feature which can raise complexity. So use with care.

Important rules for using as3ninja.include:

  1. Files included via as3ninja.include cannot include further Template Configuration files.

  2. All Template Configuration files supplied to as3ninja can use as3ninja.include.

  3. Every file included via as3ninja.include will only be included once, even if multiple configuration files reference this file.

  4. Files will be included in the order specified.

  5. Files are included just after the current configuration file (containing the include statement).

  6. When filename and/or path globbing is used, all matching files will be included alphabetically.

  7. Finally when all includes have been identified as3ninja.include will be updated with the full list of all includes in the order loaded.

The following example illustrates the behavior. Suppose we have the below tree structure and three Template Configuration files.

 1./configs
 2├── one.yaml
 3├── second
 4   ├── 2a.yaml
 5   ├── 2b.yaml
 6   └── 2c.yaml
 7└── third
 8    ├── 3rd.yaml
 9    ├── a
10       ├── 3a.yaml
11       └── a2
12           └── 3a2.yaml
13    ├── b
14       ├── 3b1.yaml
15       └── 3b2.yaml
16    └── c
17        └── 3c.yaml
1# first.yaml
2as3ninja:
3  include: ./configs/one.yaml  # a single file include can use key:value
1# second.yaml
2as3ninja:
3  include:  # multiple file includes require a list
4    - ./configs/second/2c.yaml  # explicitly include 2c.yaml first
5    - ./configs/second/*.yaml  # include all other files
6    # The above order ensures that 2c.yaml is merged first and the
7    # remaining files are merged afterwards.
8    # 2c.yaml will not be imported twice, hence this allows to
9    # control merge order with wildcard includes.
1# third.yaml
2as3ninja:
3  include:
4    - ./configs/third/**/*.yaml  # recursively include all .yaml files
5    - ./configs/one.yaml  # try including one.yaml again

This will result in the following list of files, which will be merged to one configuration in the order listed:

 1first.yaml
 2configs/one.yaml
 3second.yaml
 4configs/second/2c.yaml  # notice 2c.yaml is included first
 5configs/second/2a.yaml
 6configs/second/2b.yaml
 7third.yaml
 8configs/third/3rd.yaml
 9configs/third/a/3a.yaml
10configs/third/a/a2/3a2.yaml
11configs/third/b/3b1.yaml
12configs/third/b/3b2.yaml
13configs/third/c/3c.yam
14# notice that configs/one.yaml is not included by third.yaml

Assume every YAML file has an data: <filename> entry and you have a template.jinja2 with {{ ninja | jsonify }}.

1as3ninja transform --no-validate -t template.jinja2 \
2  -c first.yaml \
3  -c second.yaml \
4  -c third.yaml \
5  | jq .

would produce:

 1{
 2  "as3ninja": {
 3    "include": [
 4      "configs/one.yaml",
 5      "configs/second/2c.yaml",
 6      "configs/second/2a.yaml",
 7      "configs/second/2b.yaml",
 8      "configs/third/3rd.yaml",
 9      "configs/third/a/3a.yaml",
10      "configs/third/a/a2/3a2.yaml",
11      "configs/third/b/3b1.yaml",
12      "configs/third/b/3b2.yaml",
13      "configs/third/c/3c.yaml"
14    ]
15  },
16  "data": "configs/third/c/3c.yaml"
17}

Note

The above example is intended to demonstrate the behavior but could be seen as an example for bad practice due to the include complexity.

Including further YAML files using !include

AS3 Ninja uses a custom yaml !include tag which provides additional functionality to include further YAML files.

!include is followed by a filename (including the path from the current working directory) or a python list of filenames. The filename(s) can include a globbing pattern following the rules of python3’s pathlib Path.glob.

Note

Nesting !include is possible, e.g. a.yaml includes b.yaml which includes c.yaml but should be avoided in favor of a cleaner and more understandable design.

Suppose we have the below tree structure:

 1.
 2├── main.yaml
 3└── services
 4    ├── A
 5       ├── serviceA1.yaml
 6       ├── serviceA2.yaml
 7       └── serviceA3.yaml
 8    └── B
 9        ├── serviceB1.yaml
10        └── serviceB2.yaml

Each serviceXY.yaml file contains definitions for its service, for example:

1ServiceXY:
2  address: 198.18.x.y

In main.yaml we use !include to include the serviceXY.yaml files as follows:

 1# Use globbing to traverse all subdirectories in `./services/`
 2# and include all `.yaml` files:
 3all_services: !include ./services/**/*.yaml
 4
 5# simply include a single yaml file:
 6service_a1: !include ./services/A/serviceA1.yaml
 7
 8# include a single yaml file but make sure it is included as a list element:
 9service_b1_list: !include [./services/B/serviceB1.yaml]
10
11# include two yaml files explicitly:
12service_a2_b2: !include [./services/A/serviceA2.yaml, ./services/B/serviceB2.yaml]
13
14# include all files matching serviceB*.yaml in the directory ./services/B/
15services_b: !include ./services/B/serviceB*.yaml

The above yaml describes all syntaxes of !include and is equivalent to the below yaml.

Please specifically note the behavior for the following examples:

  • all_services contains a list of all the yaml files the globbing pattern matched.

  • service_a1 only contains the one yaml file, because only one file was specified, it is included as an object not a list.

  • service_a2_b2 contain a list with the entries of serviceA2.yaml and serviceB2.yaml

  • service_b1_list includes only serviceB1.yaml but as a list entry due to the explicit use of []

Note

Also note that the above paths are relative to the CWD where as3ninja is executed. That means if ls ./services/A/serviceA2.yaml is successful running as3ninja from the current directory will work as well.

 1all_services:
 2  - ServiceA2:
 3      address: 198.18.1.2
 4  - ServiceA3:
 5      address: 198.18.1.3
 6  - ServiceA1:
 7      address: 198.18.1.1
 8  - ServiceB2:
 9      address: 198.18.2.2
10  - ServiceB1:
11      address: 198.18.2.1
12
13service_a1:
14  ServiceA1:
15    address: 198.18.1.1
16
17service_b1_list:
18  - ServiceB1:
19      address: 198.18.2.1
20
21service_a2_b2:
22  - ServiceA2:
23      address: 198.18.1.2
24  - ServiceB2:
25      address: 198.18.2.2
26
27services_b:
28  - ServiceB2:
29      address: 198.18.2.2
30  - ServiceB1:
31      address: 198.18.2.1

It is important to note that !include does not create a “new yaml file” similar to the above example, instead it de-serializes the main.yaml file and treats !include as an “instruction”, which then de-serializes the files found based on the !include statement.

So de-serializing the main.yaml actually results in the below python data structure (dict):

 1{
 2  "all_services": [
 3    { "ServiceA2": { "address": "198.18.1.2" } },
 4    { "ServiceA3": { "address": "198.18.1.3" } },
 5    { "ServiceA1": { "address": "198.18.1.1" } },
 6    { "ServiceB2": { "address": "198.18.2.2" } },
 7    { "ServiceB1": { "address": "198.18.2.1" } }
 8  ],
 9  "service_a1": { "ServiceA1": { "address": "198.18.1.1" } },
10  "service_b1_list": [
11    { "ServiceB1": { "address": "198.18.2.1" } }
12  ],
13  "service_a2_b2": [
14    { "ServiceA2": { "address": "198.18.1.2" } },
15    { "ServiceB2": { "address": "198.18.2.2" } }
16  ],
17  "services_b": [
18    { "ServiceB2": { "address": "198.18.2.2" } },
19    { "ServiceB1": { "address": "198.18.2.1" } }
20  ]
21}

Caution

!include does not prevent against circular inclusion loops, which would end in a RecursionError exception.

Default Template Configuration File

If no Template Configuration File is specified, AS3 Ninja will try to use the first of the following files.

  1. ./ninja.json

  2. ./ninja.yaml

  3. ./ninja.yml

This is useful if you do not need multiple Template Configuration Files or only occasionally need them.

Declaration Template

The Declaration Template defines how the configuration is used to render an AS3 Declaration.

Declaration Templates use the Template Configuration, which is available in the Jinja2 Context.

A question of paradigms: Declarative or Imperative

If you thought you already choose the declarative paradigm with AS3 you are mostly correct. The AS3 Declaration is declarative.

But how do you produce the AS3 Declaration?

This is where AS3 Ninja and specifically Jinja2 comes into play. Jinja2 provides a wide spectrum between declarative and imperative to fit your specific needs.

A quick overview of Imperative vs. Declarative Programming, which can help understand the topic better: Imperative vs Declarative Programming

AS3 Ninja the declarative way

Let’s look at a declarative way to render an AS3 Declaration.

 1{# Declaration Template #}
 2{
 3  "class": "AS3",
 4  "declaration": {
 5    "class": "ADC",
 6    "schemaVersion": "3.11.0",
 7    "id": "urn:uuid:{{ ninja.uuid }}",
 8    "{{ ninja.tenant }}": {
 9      "class": "Tenant",
10      "{{ ninja.app.name }}": {
11        "class": "Application",
12        "template": "http",
13        "backends": {
14          "class": "Pool",
15          "monitors": ["http"],
16          "members": [
17            {
18              "servicePort": 80,
19              "serverAddresses": [ {{ ninja.app.backends }} ]
20            }
21          ]
22        },
23        "serviceMain": {
24          "class": "Service_HTTP",
25          "virtualAddresses": ["{{ ninja.app.address }}"],
26          "pool": "backends"
27        }
28      }
29    }
30  }
31}

The above Declaration Template uses Jinja2 to fill specific values using variables. No logic, no control structures nor commands are used.

1# Template Configuration
2tenant: MyTenant
3uuid: 2819307c-d8c3-4d1e-911e-40889e1df6c7
4app:
5  name: MyApp
6  address: 198.18.0.1
7  backends: "\"192.168.0.1\", \"192.168.0.2\""

Above is an example Template Configuration for our Declaration Template. As our backends are expected to be a JSON array, the value of backends isn’t very pretty.

Adding additional services, tenants or service specific configurations will require changes in the Template Configuration as well as the Declaration Template.

AS3 Ninja the imperative way

Now let’s find an imperative way to render a similar AS3 Declaration.

 1{# Declaration Template #}
 2{
 3  "class": "AS3",
 4  "declaration": {
 5    "class": "ADC",
 6    "schemaVersion": "3.11.0",
 7    "id": "urn:uuid:{{ uuid() }}",
 8    {% for tenant in ninja.tenants %}
 9    "{{ tenant.name }}": {
10      "class": "Tenant",
11      {% for app in tenant.apps %}
12      "{{ app.name }}": {
13        "class": "Application",
14        "template": "{{ app.type }}",
15        "backends": {
16          "class": "Pool",
17            "monitors":
18            {% if app.monitors is defined %}
19                {{ app.monitors | jsonify }},
20            {% else %}
21                {{ ninja.mappings.monitor[app.type] | jsonify }},
22            {% endif %}
23            "members": {{ app.backends | jsonify }}
24        },
25        "serviceMain": {
26          "class": "{{ ninja.mappings.service[app.type] }}",
27          "virtualAddresses": {{ app.address | jsonify }},
28          "pool": "backends"
29        }
30      }
31    {% if not loop.last %},{% endif %}
32    {% endfor %}
33    }
34  {% if not loop.last %},{% endif %}
35  {% endfor %}
36  }
37}

This Declaration Template not only uses Jinja2 to fill specific values using variables but also uses control structures, mainly loops and conditions (highlighted), to render the AS3 Declaration.

You can already see that this Declaration Template iterates over a list of tenants and a list of apps for each tenant. This clearly shows this example is probably easy to extend with additional tenants and apps.

As this Declaration Template contains a lot more details we will take a closer look at each step, but first let’s have a look at the Template Configuration:

 1# Template Configuration
 2tenants:
 3- name: MyTenant
 4  apps:
 5  - name: MyApp
 6    type: http
 7    address:
 8    - 198.18.0.1
 9    backends:
10    - servicePort: 80
11      serverAddresses:
12      - 192.168.0.1
13      - 192.168.0.2
14mappings:
15  service:
16    http: Service_HTTP
17  monitor:
18    http:
19    - http

The Template Configuration is longer than the previous declarative example, but it is also more flexible. The non-pretty representation of the backends has been replaced with a more flexible backends definition (highlighted).

As this Configuration Template works hand in hand with the Declaration Template we will take a closer look at both in the next section.

Building a Declaration Template

A declarative Declaration Template and the corresponding Template Configuration is pretty straightforward as you saw earlier.

So instead we will look at the imperative example above and walk through each step. For conciseness we will remove parts from the Declaration Template and Template Configuration and focus on the subject.

Looping Tenants and their Apps

1# Template Configuration
2tenants:
3- name: MyTenant
4  # ... tenant specific configuration
5  apps:
6  - name: MyApp
7    type: http
8    # ... app specific configuration

The above Template Configuration excerpt contains a list of Tenants (line 2) with the first list entry having name key with value MyTenant (line 3). Within this Tenant a list of Applications (Apps) is defined (line 5), with the first list entry having a name key with value MyApp (line 6).

 1{# Declaration Template #}
 2{
 3  "class": "AS3",
 4  {# ... more code ... #}
 5    {% for tenant in ninja.tenants %}
 6    "{{ tenant.name }}": {
 7      "class": "Tenant",
 8      {% for app in tenant.apps %}
 9      "{{ app.name }}": {
10      {# ... app specific code ... #}
11      }
12    {% if not loop.last %},{% endif %}
13    {% endfor %}
14    }
15  {% if not loop.last %},{% endif %}
16  {% endfor %}
17  }
18}

The Declaration Template is built to iterate over a list of Tenants (line 5). The Template Configuration list of Tenants is accessible via ninja.tenants and each Tenant is assigned to tenant, which is now available within the for loop. On line 6 the Tenant name is read from tenant.name.

Furthermore on line 8 the Declaration Template will iterate the list of Applications defined for this Tenant. The list of Applications for this particular Tenant is available via tenant.apps. apps refers to the definition in the Template Configuration (on line 5). The Application specific configuration starts on line 9, where app.name is used to declarative the Application class of the AS3 Declaration.

Line 12 is checking for the last iteration of the inner “Application loop” and makes sure the comma (,) is included when there are further elements in the Application list. This is important as JSON does not tolerate a trailing comma. Line 13 defines the end of the loop.

The same is done on line 15 and 16 for the outer “Tenants loop”.

Note

More details on control structures in Jinja2 can be found at List of Control Structures in the Jinja2 Template Designer Documentation.

Application specific settings

Now let’s look at the Application specific settings.

 1# Template Configuration
 2tenants:
 3- name: Tenant1
 4  apps:
 5  - name: MyApp
 6    type: http
 7    address:
 8    - 198.18.0.1
 9    backends:
10    - servicePort: 80
11      serverAddresses:
12      - 192.168.0.1
13      - 192.168.0.2
14mappings:
15  service:
16    http: Service_HTTP
17  monitor:
18    http:
19    - http

The YAML is more structured to not only fit the Declaration Template but also the AS3 data structures. A mappings data structure was added to assist with default values / mappings to Application types.

 1{# Declaration Template #}
 2  {# ... more code ... #}
 3  "{{ app.name }}": {
 4    "class": "Application",
 5    "template": "{{ app.type }}",
 6    "backends": {
 7      "class": "Pool",
 8        "monitors":
 9        {% if app.monitors is defined %}
10            {{ app.monitors | jsonify }},
11        {% else %}
12            {{ ninja.mappings.monitor[app.type] | jsonify }},
13        {% endif %}
14        "members": {{ app.backends | jsonify }}
15    },
16    "serviceMain": {
17      "class": "{{ ninja.mappings.service[app.type] }}",
18      "virtualAddresses": {{ app.address | jsonify }},
19      "pool": "backends"
20    {# ... more code ... #}

The app.type is used on line 5 to map to the http AS3 template, on line 12 app.type is used again as a key for mappings.service. This allows us to create multiple App type to Service_<type> mappings. In this case http maps to the AS3 service class Service_HTTP.

Line 9-13 deals with monitors, if app.monitors is defined it is used, otherwise app.type is used again to lookup the default monitor to use, based on the Template Configuration (line 17-19). Note that "monitors" is expected to be a JSON array of monitors, this is why the Template Configuration YAML uses a list for monitor.http. jsonify is an AS3 Ninja Filter (see as3ninja.jinja2.filterfunctions.jsonify()) which will convert any “piped” data to a valid JSON format. A python list (which the YAML de-serializes to) is converted to a JSON array.

The "members" key for a AS3 Pool class is expected to be a list, each list entry is an object with several key:value pairs. serverAddresses are again expected to be a list of IP addresses.

Looking at the backends part of the Template Configuration again:

1    backends:
2    - servicePort: 80
3      serverAddresses:
4      - 192.168.0.1
5      - 192.168.0.2

app.backends and it’s YAML exactly represents this structure, making it easy for the Declaration Template to just convert it to JSON (using the jsonify filter). Sometimes it is easier to look at the resulting JSON, as it is used by AS3 as well. Here is how the above YAML for backends looks like:

1{
2  "backends": [
3    {
4      "servicePort": 80,
5      "serverAddresses": ["192.168.0.1", "192.168.0.2"]
6    }
7  ]
8}

"virtualAddresses", on line 18 Declaration Template, is also expected to be a JSON array, which is what the Template Configuration perfectly represents and jsonify converts to.

Adding more Tenants

Based on the above imperative example, it is easy to add further Tenants.

Here is an example adding one more Tenant:

 1# Template Configuration
 2tenants:
 3- name: Tenant1
 4  apps:
 5  - name: MyApp
 6    type: http
 7    address:
 8    - 198.18.0.1
 9    backends:
10    - servicePort: 80
11      serverAddresses:
12      - 192.168.0.1
13      - 192.168.0.2
14- name: Tenant2
15  apps:
16  - name: TheirApp
17    type: http
18    address:
19    - 198.18.100.1
20    monitors:
21    - http
22    - icmp
23    backends:
24    - servicePort: 80
25      serverAddresses:
26      - 192.168.100.1
27mappings:
28  service:
29    http: Service_HTTP
30  monitor:
31    http:
32    - http

Adding an additional App type

What if we want to add an additional type of Application? Let’s assume we want to add a SSH server, using AS3’s Service_TCP.

As this service class doesn’t come with a default value for virtualPort we will need to modify our Declaration Template.

 1{# Declaration Template #}
 2{
 3  "class": "AS3",
 4  "declaration": {
 5    "class": "ADC",
 6    "schemaVersion": "3.11.0",
 7    "id": "urn:uuid:{{ uuid() }}",
 8    {% for tenant in ninja.tenants %}
 9    "{{ tenant.name }}": {
10      "class": "Tenant",
11      {% for app in tenant.apps %}
12      "{{ app.name }}": {
13        "class": "Application",
14        "template": "{{ app.type }}",
15        "backends": {
16          "class": "Pool",
17            "monitors":
18            {% if app.monitors is defined %}
19                {{ app.monitors | jsonify }},
20            {% else %}
21                {{ ninja.mappings.monitor[app.type] | jsonify }},
22            {% endif %}
23            "members": {{ app.backends | jsonify }}
24        },
25        "serviceMain": {
26          {% if app.port is defined %}
27          "virtualPort": {{ app.port }},
28          {% endif %}
29          "class": "{{ ninja.mappings.service[app.type] }}",
30          "virtualAddresses": {{ app.address | jsonify }},
31          "pool": "backends"
32        }
33      }
34    {% if not loop.last %},{% endif %}
35    {% endfor %}
36    }
37  {% if not loop.last %},{% endif %}
38  {% endfor %}
39  }
40}

We added a conditional check for app.port (line 26-28). If it is set, "virtualPort" will be added to the AS3 Declaration with the value of app.port. Of course this app.port can be used by other service types as well.

 1# Template Configuration
 2tenants:
 3- name: Tenant1
 4  apps:
 5  - name: MyApp
 6    type: http
 7    address:
 8    - 198.18.0.1
 9    backends:
10    - servicePort: 80
11      serverAddresses:
12      - 192.168.0.1
13      - 192.168.0.2
14- name: Tenant2
15  apps:
16  - name: TheirApp
17    type: http
18    address:
19    - 198.18.100.1
20    monitors:
21    - http
22    - icmp
23    backends:
24    - servicePort: 80
25      serverAddresses:
26      - 192.168.100.1
27  - name: TcpApp
28    type: tcp
29    port: 22
30    address:
31    - 198.18.100.1
32    backends:
33    - servicePort: 22
34      serverAddresses:
35      - 192.168.100.1
36mappings:
37  service:
38    http: Service_HTTP
39    tcp: Service_TCP
40  monitor:
41    http:
42    - http
43    tcp:
44    - tcp

Line 29 has the new port key, which is used in the Declaration Template. Along with the TCP based service we also updated the mappings.

Hint

If you use Visual Studio Code, the jinja-json-syntax Syntax Highlighter is very helpful.