- Configuration
- Top-level extension
- Template merging
- Service-level extension
- Using YAML anchors
- Template variables and functions
- Usage
Podlike comes with a built-in template processor, to help transforming services and their tasks in Docker Swarm stacks into a "pod", a set of co-located and tightly coupled containers. Similar stacks, or similar types of applications in a stack, could often benefit from decorating the tasks with the same components, with only slightly different configuration. For example, sidecars or service meshes usually need the same component consistently deployed with the applications themselves, and we'd probably want changes to these components done in a single place, and applied to all services (or a set of them) at the same time.
Using templates gives you a flexible way to define these components, and allows you to reuse the components accross stacks and services. Templates generate parts of a YAML Compose file using Go's text/template package. The templates to use are defined directly in the stack YAML files with extension fields, so the configuration lives within them, and can be versioned/changed/rolled out with the same workflows as the original stack.
The example YAML snippet below demonstrates the use of the Podlike-related extension fields the template generator will look for.
version: '3.5'
x-anchors:
- &tracing-component
templates/components/tracing.yml
- &proxy-component
templates/components/proxy.yml
x-podlike:
example:
pod:
- templates/pod.yml
- http: https://templates.store.local/pods/example.yml
- inline:
pod:
labels:
swarm.service.label: templated-for-{{ .Service.Name }}
transformer:
- templates/transformer.yml
# or http or inline
templates:
- templates/components/sidecar.yml
- templates/components/logger.yml
- *tracing-component
- *proxy-component
# or http or inline
copy:
- inline:
sidecar: /var/conf/sidecar.conf:/etc/sidecar/conf.d/default.conf
# or http or from file
args:
ExposedHttpPort: 8080
ExtraEnvVars:
- DEBUG=false
# any other arguments you'd need available for the templates
args:
GlobalVariable: example
# any other arguments you'd need available for the templates
services:
example:
image: example/app:0.1.2
volumes:
- log-data:/var/logs/example
x-podlike:
pod:
# same as above
transformer:
# same as above
templates:
# same as above
copy:
# same as above
args:
# same as above
volumes:
log-data:
name: log-data-for-{{.Task.ID}}
labels:
com.github.rycus86.podlike.volume-ref: shared-log-folder
Let's unpack the example above, and look at the different extension places.
Extension fields at the root level of a stack YAML are supported since Compose schema version 3.4
, and are simply ignored by a docker stack deploy
. Podlike can use the x-podlike
top-level extension field to define templates per service, matching the service name, plus any additional args
to make available globally to the templates used within the stack.
For each service, we can define pod
templates, transformer
templates, templates
for the additional components, copy
configurations, and additional args
available for templates used with this service. The additional arguments are merged with any global args
.
# top-level extension
x-podlike:
svc1:
pod:
init:
transformer:
templates:
copy:
args:
svc2:
templates:
args:
args:
Every field is optional to use, and you can use a single template or a list of them for pod
, init
, transformer
, templates
or copy
. If multiple templates are given for a single type, they will be merged together (except for init
), in order - see more details below at Template merging.
The example above would define templates to use on the svc1
and svc2
services, plus specific arguments for each service, as well as additional global arguments. See which template is used for what below, but first, let's have a quick overview of what types of parameters they accept.
All 5 types of definitions accept either a single item, or a list of items. An item can be:
- A simple string
This points to a template file or an HTTP(S) address.
x-podlike:
example:
pod:
- templates/pod.yml
transformer:
- file:
path: local.template.yml
fallback:
inline: InlineFallbackIfFileNotAvailable
templates:
- https://template.srv.local/addon.yml
File templates can be given with a simple string pointing to a file, or as a mapping with a mandatory path
field, and an optional fallback
property if loading the file fails.
- An inline template mapping
This uses the given string as the template text, or the YAML string marshalled from the given mapping.
x-podlike:
example:
pod:
- inline: |
image: sample/{{ .Service.Name }}:{{ .Args.ImageTag }}
labels:
format: string
- inline:
labels:
given: as.mapping
- An HTTP(S) URL to the template
This fetches the template from the given URL, and uses the response content as the template text.
x-podlike:
example:
pod:
- http: https://my.templates.local/pods/sample.yml
transformer:
- http:
url: https://maybe.insecure.local/transformer/sample.yml
insecure: true
- http:
url: http://template.cache.local/addons/cache.yml
fallback:
file:
path: cached/local.copy.yml
fallback:
inline: |
main:
image: sensible/defaults
As shown above, the value of the http
property can be a simple URL string, or a mapping with a mandatory url
field, plus an optional insecure
property to disable SSL certificate validation, and a fallback
field to specify the template to try if loading from HTTP fails.
The templates listed under the pod
key are used to construct the new Swarm service definition for the controller. This is allowed to produce a Swarm compatible service mapping, e.g. deploy
, configs
, secrets
, etc. are OK.
If omitted, a default template is used to generate the image
property pointing to rycus86/podlike
with the same version as the template generator. The default also adds a volume mapping for the Docker engine socket at /var/run/docker.sock
for convenience, and enables streaming logs from the components using the -logs
Podlike flag.
If there is at least one template given, the template engine only makes sure there is an image
defined, with the same rules as above, plus it adds volume for the Docker engine socket.
x-podlike:
example:
pod:
inline:
pod:
image: forked/podlike:{{ .Args.Version }}
deploy:
replicas: 3
args:
Version: 0.1.2
The name of the root property in the generated string doesn't matter, it will be replaced by the actual name of the service as given in the stack YAML. The template engine also copies over most of the properties from the original service definition, unless they are added by the templates, see these in the mergedPodKeys
in the merge.go source file.
Each of the templates given here must output a YAML compatible string with a single root property.
The init
templates generate component definitions for containers that run to completion sequentially before the main component and any any additional ones are started, similarly to init containers in Kubernetes. When using Podlike directly with labels, you can define them as a YAML string containing a list of components, however with the templated approach you can define them as a list of maps.
x-podlike:
example:
init:
- inline:
example:
image: init/files
command: >
--dir /etc/shared/files
- inline:
component:
image: init/config
environment:
TARGET: /var/conf
The name of component doesn't matter in the definition, the target string won't contain them. As mentioned above, these containers will run one-by-one, and each of them need to finish successfully, with exit code 0
.
The transformer
templates generate the Compose-compatible component definition for the main component, that is the original image defined in the stack YAML in most cases, with its selected properties.
x-podlike:
example:
transformer:
inline: |
main:
environment:
- EXTRA_VARS={{ .Args.ExtraEnv }}
{{ if .Service.ReadOnly }}
read_only: true
{{ end }}
args:
ExtraEnv: some-env-var
It no templates given, a default one will copy over the image
property from the original service definition, plus a fair bit of other properties are added automatically, defined by mergedTransformerKeys
in the merge.go source file.
Each of the templates given here must output a YAML compatible string with a single root property. Most of the v2 Compose file properties are allowed, with the exceptions listed on the main project README. The name of the result component will be the root key of the first template. Root keys defined by any other templates will be ignored, and converted automatically to the one defined by the first. The example above would use main
, the default is app
.
The templates listed under templates
can define any number of components. These are meant to generate the Compose-compatible definitions of the containers to couple the main component with.
x-podlike:
example:
templates:
- templates/sidecar.yml
- templates/service-discovery.yml
- templates/tracing.yml
- inline:
tracing:
mem_limit: 64m
- inline:
tracing:
environment:
HTTP_PORT: '{{ .Args.Tracing.Http.Port }}'
args:
Tracing:
Http:
Port: 12345
As with the other types, the templates are processed in the same order as they are defined in the YAML, and any common properties are merged in together. In the example above, the templates/tracing.yml
template could define a component with the tracing
name, then the last two templates would add in the mem_limit
property, if not defined by the previous template already, plus the environment
variables would also contain HTTP_PORT
.
The names of the components come from root properties of the result YAML, after merging all the template outputs together.
Podlike allows copying files from the controller container into the component containers before they start, and the copy
templates can define the mappings for these.
x-podlike:
example:
copy:
- inline:
proxy: '/shared/proxy.conf:/var/conf/proxy/default.conf'
- inline:
logging:
- /shared/logging.conf:/var/conf/logger/settings.properties
- /shared/proxy.logging:/var/conf/logger/conf.d/proxy.conf
Each template needs to output a mapping of service name to copy configurations. The copy items will be converted into a string
slice of <source>:<target>
paths, but accepts a <source>: <target>
mapping, or a single string as well. The lists generated by all the copy
templates will be then merged into a single list, and put on the controller definition.
As mentioned above, each type of templates can use multiple source to generate the final markup, and they can output the same properties for the same component with different settings. Single-valued properties are going to be ignored if redefined, but slices and maps are merged together. A prime example of these would be environment
variables or labels
.
x-podlike:
example:
transformer:
- inline:
environment:
- HTTP_PROXY=my.local.proxy:8091
labels:
inline.label: sample
- inline:
environment:
ADDED: 'new key, and is added'
HTTP_PROXY: 'ignored as already defined'
# note that `- HTTP_PROXY=override` would have been added
# because at this point the template engine wouldn't assume it's
# a key-value pair as a string, only when it sees that it can be a mapping
labels:
inline.label: ignored
additional.label: added
The merging logic works on a best-effort basis to merge items of the same property together, even if they are of different types. It can:
- Merge items of a map into another map
- Merge items of a slice of
key=value
pairs into a map - Merge items of a slice into another slice
- Merge items of a map into a slice after converting it to a map as
key=value
pairs - Add a string into a slice
See the implementation in the merge.go file, and also the tests for these cases in the merge_test.go file.
Besides top-level extension fields, the template engine also supports per-service extensions with the same x-podlike
name. This currently works by removing the property and its children from the YAML after reading the configuration from them.
With Compose schema version
3.7
, the service-level extension fields are going to be supported as well, but until then having these makes the YAML invalid for a plaindocker stack deploy
command.
The configuration is the same as it is for the top-level field, with the exception that the service name does not have to be defined as it is inferred from the service name.
version: '3.5'
services:
example:
image: sample/application
x-podlike:
pod:
transformer:
templates:
copy:
args:
If the same service has any configuration in the top-level x-podlike
field as well, then those are merged into the service-level configuration following the rules above. For example:
version: '3.5'
services:
example:
image: sample/application
x-podlike:
templates:
- templates/first.yml
- templates/second.yml
x-podlike:
example:
templates:
- templates/third.yml
The args
are also merged the same way, in the order of:
- Service-level arguments
- Per-service arguments from the top-level extension
- Global arguments from the top-level extension
This allows you to define default values for arguments globally, then override then per service.
If multiple services in a single stack share similar templating configuration, YAML anchors could help reduce some of the duplication. For example:
version: '3.5'
x-podlike-templates: # the name of this does not matter
- &default-pod
pod:
inline:
image: forked/podlike
command: -logs -pids=false
- &sidecar-template
inline:
sidecar:
image: sample/sidecar
- &logging-template
inline:
logging:
image: sample/logger
command: -input {{ .Args.LogFile }}
services:
service-one:
image: sample/svc1
x-podlike:
<<: *default-pod
templates:
- <<: *sidecar-template
- <<: *logging-template
args:
LogFile: /var/logs/service.one.log
service-two:
image: sample/svc2
x-podlike:
<<: *default-pod
templates:
- <<: *logging-template
args:
LogFile: /var/logs/service.two.log
This is particularly useful when using inline templates. A better approach would be sharing the templates through files, or serving them up on HTTP.
When rendering the templates, the following variables are available to them:
Service
: the Swarm service definition as the Docker cli package has itArgs
: the merged map of the service arguments, with the globalargs
added in as described above
There are also additional template functions available, on top of the built-in ones:
yaml <obj>
: returns the YAML string representation of an objectindent <num> <str>
: indents every line of<str>
by<num>
spacesempty <obj>
: returns true if the array/slice/map is emptynotEmpty <obj>
: returns true if the array/slice/map is not emptycontains <s> <t>
: returns true if<t>
contains<s>
startsWith <s> <t>
: return true if<t>
starts with<s>
replace <old> <new> <n> <s>
: replaces<old>
with<new>
<n>
times in<s>
An example template using them could look like this:
sidecar:
image: sidecars/{{ .Args.Sidecar.Current.Image }}:{{ .Args.Sidecar.Current.Version }}
{{ if notEmpty .Service.Ports }}
{{ with $port := index .Service.Ports 0 }}
command: --listen {{ $port.Target }}
{{ end }}
{{ else }}
command: --listen 8080
{{ end }}
labels:
{{ range $key, $value := .Service.Labels }}
{{ if $key | startsWith "sidecar." }}
{{ with $label := $key | replace "sidecar." "" -1 }}
{{ printf "%s: %s" $label $value | indent 4 }}
{{ end }}
{{ end }}
{{ end }}
If given an input like this:
version: '3.5'
services:
app:
image: sample/app:1.1.0
ports:
- 9090:3000
- 15000:8080
labels:
com.xyz.label: value
sidecar.metrics.port: 15000
sidecar.ping.endpoint: /v2/ping
x-podlike:
pod:
inline:
pod:
labels: # avoid copying labels from the service
transformer:
inline:
app:
labels:
com.xyz.system: sample-app
templates:
- templates/sidecar/v1.yml
args:
Sidecar:
Current:
Image: scapp
Version: 3.4.2
The generated result would look like this:
version: "3.5"
services:
app:
image: rycus86/podlike:latest
labels:
pod.component.app: |
image: sample/app:1.1.0
labels:
com.xyz.system: sample-app
pod.component.sidecar: |
command:
- --listen
- "3000"
image: sidecars/scapp:3.4.2
labels:
metrics.port: "15000"
ping.endpoint: /v2/ping
ports:
- mode: ingress
target: 3000
published: 9090
protocol: tcp
- mode: ingress
target: 8080
published: 15000
protocol: tcp
volumes:
- type: bind
source: /var/run/docker.sock
target: /var/run/docker.sock
read_only: true
networks: {}
volumes: {}
secrets: {}
configs: {}
The template engine is part of the main application, so it can easily be invoked through Docker:
$ docker run --rm -it \
-v $PWD:/workspace:ro \
-w /workspace \
rycus86/podlike \
template <file> [<file>...]
This shares the current directory (and its sub-directories) and generates the final YAML using the given input stack YAML files. Alternatively, you can pipe the source stack to the standard input of the container, if you only use inline templates:
$ cat source.yml | docker run --rm -i rycus86/podlike template -
# notice the missing --tty option
To make these easier, there is a script to do this for you, called podtemplate
. Have a look into its documentation for installation and usage information.