- Naming Conventions
-
Common structure for
values.yaml
- Preferences on function use
- Developing template helpers
- Templating Configuration Files
- Templating chart notes (NOTES.txt)
Style guide
This document describes various guidelines and best practices for GitLab Helm chart development.
Naming Conventions
We are using camelCase for our function names, and properties where they are used in values.yaml
.
Example: gitlab.assembleHost
Template functions are placed into namespaces according to the chart they are associated with, and named to match the affected populated value in the target file. Note that chart global functions generally fall under the gitlab.*
namespace.
Examples:
-
gitlab.redis.host
: provides the host name of the Redis server, as a part of thegitlab
chart. -
registry.minio.url
: provides the URL to the MinIO host as part of theregistry
chart.
Common structure for values.yaml
Many charts need to be provided with the same information, for example we need to provide the Redis and PostgreSQL connection settings to multiple charts. Here we outline our standard naming and structure for those settings.
Connecting to other services
redis:
host: redis.example.com
serviceName: redis
port: 8080
sentinels:
- host: sentinel1.example.com
port: 26379
password:
secret: gitlab-redis
key: redis-password
-
redis
- the name for what the current chart needs to connect to -
host
- overrides the use of serviceName, comment out by default use0.0.0.0
as the example. If using Redis Sentinels, thehost
attribute needs to be set to the cluster name as specified in thesentinel.conf
. -
serviceName
- intended to be used by default instead of the host, connect using the Kubernetes Service name -
port
- the port to connect on. Comment out by default, and use the default port as the example. -
password
- defines settings for the Kubernetes Secret containing the password. -
sentinels.[].host
- defines the hostname of Redis Sentinel server for a Redis HA setup. -
sentinels.[].port
- defines the port on which to connect to the Redis Sentinel server. Defaults to26379
.
Note: The current Redis Sentinel support only supports Sentinels that have
been deployed separately from the GitLab chart. As a result, the Redis
deployment through the GitLab chart should be disabled with redis.install=false
.
The Secret containing the Redis password must be manually created
before deploying the GitLab chart.
Sharing secrets
We use secrets to store sensitive information like passwords and share them among the different charts/pods.
The common fields we use them in are:
- TLS/SSL Certificates - Sharing TLS/SSL certificates
- Passwords - Sharing the Redis password.
- Auth Tokens - Sharing the inter-service auth tokens
- Other Secrets - Sharing other secrets like JWT certificates and signing keys
TLS/SSL Certificates
A TLS/SSL certificate is expected to be a valid Kubernetes TLS Secret.
For example, to set up the registry:
registry:
tls:
secretName: <TLS secret name>
When a TLS certificate is shared between charts, it should be defined as a global value.
global:
ingress:
tls:
secretName: <TLS secret name>
Passwords
For example, where redis
was the owning chart, and the other charts need to reference the redis
password.
The owning chart should define its password secret like the following:
password:
secret: <secret name>
key: <key name inside the secret to fetch>
Other charts should share the same password secret like the following:
redis:
password:
secret: <secret name>
key: <key name inside the secret to fetch>
Auth Tokens
The owning chart should define its authToken secret like the following:
authToken:
secret: <secret name>
key: <key name inside the secret to fetch>
Other charts should share the same password secret like the following:
gitaly:
authToken:
secret: <secret name>
key: <key name inside the secret to fetch>
For example, where gitaly
was the owning chart, and the other charts need to reference the gitaly
authToken.
Other Secrets
Other secrets, such as the JWT signing certificate for the registry
or the gitaly
GPG signing
key, use the same format as authToken
and password
secrets.
To share such secrets from one chart to other charts, provide a configuration similar to
the example below in which the registry
JWT signing certificate is shared with other charts.
The owning chart should define its secret like the following:
certificate:
secret: <secret name>
key: <key name inside the secret to fetch>
Other charts should share the same secret like the following:
registry:
certificate:
secret: <secret name>
key: <key name inside the secret to fetch>
Preferences on function use
We have evolved a set of preferences for developing these charts, regarding the various functions available to use in gotmpl, Sprig, and Helm. The following sections explain some of these, and reasoning behind them
Use nindent over indent
When possible, make use of the nindent
function instead of the indent
function.
This preference is based on readability, and especially for Helm charts as complex
as ours can be. The preferred use of nindent
has become community wide, and is also
now the default within templates generated by the helm create
command.
Let’s look at two snippet examples, which easily exemplify the reasoning:
Easy to read
gitlab.yml.erb: |
production: &base
gitlab:
host: {{ template "gitlab.gitlab.hostname" . }}
https: {{ hasPrefix "https://" (include "gitlab.gitlab.url" .) }}
{{- with .Values.global.hosts.ssh }}
ssh_host: {{ . | quote }}
{{- end }}
{{- with .Values.global.appConfig }}
max_request_duration_seconds: {{ default (include "gitlab.appConfig.maxRequestDurationSeconds" $) .maxRequestDurationSeconds }}
impersonation_enabled: {{ .enableImpersonation }}
application_settings_cache_seconds: {{ .applicationSettingsCacheSeconds | int }}
usage_ping_enabled: {{ eq .enableUsagePing true }}
username_changing_enabled: {{ eq .usernameChangingEnabled true }}
issue_closing_pattern: {{ .issueClosingPattern | quote }}
default_theme: {{ .defaultTheme }}
{{- include "gitlab.appConfig.defaultProjectsFeatures.configuration" $ | nindent 8 }}
webhook_timeout: {{ .webhookTimeout }}
{{- end }}
trusted_proxies:
{{- if .Values.trusted_proxies }}
{{- toYaml .Values.trusted_proxies | nindent 10 }}
{{- end }}
time_zone: {{ .Values.global.time_zone | quote }}
{{- include "gitlab.outgoing_email_settings" . | nindent 8 }}
{{- with .Values.global.appConfig }}
{{- if .incomingEmail.enabled }}
{{- include "gitlab.appConfig.incoming_email" . | nindent 6 }}
{{- end }}
{{- include "gitlab.appConfig.cronJobs" . | nindent 6 }}
gravatar:
Hard to read
gitlab.yml.erb: |
production: &base
gitlab:
host: {{ template "gitlab.gitlab.hostname" . }}
https: {{ hasPrefix "https://" (include "gitlab.gitlab.url" .) }}
{{- with .Values.global.hosts.ssh }}
ssh_host: {{ . | quote }}
{{- end }}
{{- with .Values.global.appConfig }}
max_request_duration_seconds: {{ default (include "gitlab.appConfig.maxRequestDurationSeconds" $) .maxRequestDurationSeconds }}
impersonation_enabled: {{ .enableImpersonation }}
usage_ping_enabled: {{ eq .enableUsagePing true }}
username_changing_enabled: {{ eq .usernameChangingEnabled true }}
issue_closing_pattern: {{ .issueClosingPattern | quote }}
default_theme: {{ .defaultTheme }}
{{- include "gitlab.appConfig.defaultProjectsFeatures.configuration" $ | indent 8 }}
webhook_timeout: {{ .webhookTimeout }}
{{- end }}
trusted_proxies:
{{- if .Values.trusted_proxies }}
{{- toYaml .Values.trusted_proxies | indent 10 }}
{{- end }}
time_zone: {{ .Values.global.time_zone | quote }}
{{- include "gitlab.outgoing_email_settings" . | indent 8 }}
{{- with .Values.global.appConfig }}
{{- if .incomingEmail.enabled }}
{{- include "gitlab.appConfig.incoming_email" . | indent 6 }}
{{- end }}
{{- include "gitlab.appConfig.cronJobs" . | indent 6 }}
gravatar:
Related issue: #729 Refactoring: Helm templates
When to utilize toYaml
in templates
It is frowned upon to default to utilizing a toYaml
in the template files as
this will put undue burden on supporting all functionalities of both Kubernetes
and desired community configurations. We primary focus on providing a
reasonable default using the bare minimum configuration. Our secondary focus
would be to provide the ability to override the defaults for more advanced users
of Kubernetes. This should be done on a case-by-case basis as there are
certainly scenarios where either option may be too cumbersome to support, or
provides an unnecessarily complex template to maintain.
An good example of a reasonable default with the ability to override can be
found in the Horizontal Pod Autoscaler configuration for the registry subchart.
We default to providing the bare minimum that can easily be supported, by
exposing a specific configuration of controlling the HPA via the CPU Utilization
and exposing only one configuration option to the community, the
targetAverageUtilization
. Being that an HPA can provide much more
flexibility, more advanced users may want to target different metrics and as
such, is a perfect example of where we can utilize and if statement allowing the
end user to provide a more complex HPA configuration in place.
metrics:
{{- if not .Values.hpa.customMetrics }}
- type: Resource
resource:
name: cpu
target:
type: Utilization
targetAverageUtilization: {{ .Values.hpa.cpu.targetAverageUtilization }}
{{- else -}}
{{- toYaml .Values.hpa.customMetrics | nindent 4 -}}
{{- end -}}
In the above example, the minimum configuration will be a simple change in the
values.yaml
to update the targetAverageUtilization
.
Advanced users who have identified a better metric can override this overly
simplistic HPA configuration by setting .customMetrics
to an array containing
precisely the Kubernetes API compatible configuration for the HPA metrics array.
It is important that we maintain ease of use for the more advanced users to minimize their own configuration files without it being cumbersome.
Developing template helpers
A charts template helpers are located in templates/_helpers.tpl
. These contain the named templates
used within the chart.
When using these templates, there a few things to keep in mind regarding the Go templating syntax.
Trapping non-printed values from actions
In the go templating syntax, all actions (indicated by {{ }}
) are expected
to print a string, with the exception of control structures (define, if, with, range) and variable assignment.
This means you will sometimes need to use variable assignment to trap output that is not meant to be printed.
For example:
{{- $details := .Values.details -}}
{{- $_ := set $details "serviceName" "example" -}}
{{ template "serviceHost" $details }}
In the above example, we want to add some additional data to a Map before passing it to a template function for output.
We trapped the output of the set
function by assigning it to the $_
variable. Without this assignment, the
template would try to output the result of set
(which returns the Map it modified) as a string.
Passing variables between control structures
The go templating syntax strongly differentiates between initialization (:=
) and assignment (=
), and this is impacted by scope.
As a result you can re-initialize a variable that existed outside your control structure (if/with/range), but know that variables declared within your control structure are not available outside.
For example:
{{- define "exampleTemplate" -}}
{{- $someVar := "default" -}}
{{- if true -}}
{{- $someVar := "desired" -}}
{{- end -}}
{{- $someVar -}}
{{- end -}}
In the above example, calling exampleTemplate
will always return default
because the variable that contained desired
was
only accessible within the if
control structure.
To work around this issue, we attempt to avoid the problem by using a Dictionary to hold the values we want to change in multiple scopes,
or explicitly use the assignment operator (=
vs :=
).
Example of avoiding the issue:
{{- define "exampleTemplate" -}}
{{- if true -}}
{{- "desired" -}}
{{- else -}}
{{- "default" -}}
{{- end -}}
Example of using a Dictionary:
{{- define "exampleTemplate" -}}
{{- $result := dict "value" "default" -}}
{{- if true -}}
{{- $_ := set $result "value" "desired" -}}
{{- end -}}
{{- $result.value -}}
{{- end -}}
Example of assignment versus initialization (look close!)
{{- define "exampleTemplate" -}}
{{- $someVar := "default" -}}
{{- if true -}}
{{- $someVar = "desired" -}}
{{- end -}}
{{- $someVar -}}
{{- end -}}
Example of using a template:
{{- define "exampleTemplate" -}}
foo:
bar:
baz: bat
{{- end -}}
And then pulling the above into a variable and configuration:
{{- $fooVar := include "exampleTemplate" . | fromYaml -}}
{{- $barVar := merge $.Values.global.some.config $fooVar -}}
config:
{{ $barVar }}
Templating Configuration Files
These charts make use of cloud-native GitLab containers. Those containers support the use of either ERB or gomplate.
Guidelines:
- Use template files within ConfigMaps (example:
gitlab.yml.erb
,config.toml.tpl
)- Entries must use the expected extensions in order to be handled as templates.
- Use templates to populate Secret contents from mounted file locations. (example: GitLab Pages
config
) - ERB (
.erb
) can be used for any container using Ruby during run-time execution - gomplate (
.tpl
) can be used for any container.
ERB usage:
We make use of standard ERB, and you can expect json
and yaml
modules to have been pre-loaded.
gomplate usage:
We make use of gomplate in order to remove the size and surface of Ruby within
containers. We configure gomplate syntax with alternate delimiters of {% %}
, so not
to collide with Helm’s use of {{ }}
.
Templating sensitive content
Secrets have the potential contain characters that could result invalid YAML if not properly encoded or quoted. Especially for complex passwords, we must be careful how these strings are added into various configuration formats.
Guidelines:
- Quote in the ERB / Gomplate output, not surrounding it.
- Use a format-native encoder whenever possible.
- For rendered YAML, use JSON strings because YAML is a superset of JSON.
- For rendered TOML, use JSON strings because TOML strings escape similarly.
- Be wary of complexity, such as quoted strings inside quoted stings such as database connection strings.
Example of encoding passwords
Using Gitaly’s client secret token as an example. This value is, gitaly_token
,
is templated into both YAML and TOML.
Let’s use my"$pec!@l"p#assword%'
as an example:
# YAML
gitaly:
token: "<%= File.read('gitaly_token').strip =>"
# TOML
[auth]
token = "<%= File.read('gitaly_token').strip %>"
Renders to be invalid YAML, and invalid TOML.
# YAML
gitaly:
token: "my"$pec!@l"p#assword%'"
(<unknown>): did not find expected key while parsing a block mapping at line 3 column 3
[auth]
token = "my"$pec!@l"p#assword%'"
Error on line 2: Expected Comment, Newline, Whitespace, or end of input but "$" found.
This changed to <%= File.read('gitaly_token').strip.to_json %>
results valid
content format for YAML and TOML. Note the removal of "
from outside of <% %>
.
gitaly:
token: "my\"$pec!@l\"p#assword%'"
This same can be done with gomplate: {% file.Read "gitaly_token" | strings.TrimSpace | data.ToJSON %}
gitaly:
# gomplate
token: {% file.Read "./token" | strings.TrimSpace | data.ToJSON %}
# ERB
token: <%= File.read('gitaly_token').strip.to_json %>
Templating chart notes (NOTES.txt)
Helm’s chart notes feature provides helpful information and follow-up instructions after chart installations and upgrades.
These notes are placed in templates/NOTES.txt.
When working with these notes, there are a few things to keep in mind regarding style to ensure that the output is legible and actionable.
Choosing a note category
Two categories, WARNING
and NOTICE
, signify each type of entry in the note output.
-
WARNING
signifies that further action is required to optimize the installation -
NOTICE
highlights important reminders that do not necessarily require further action
Each entry in NOTES.txt
should start with one of these two categories. For example:
{{- if eq true .Values.some.setting }}
{{ $WARNING }}
This message is a warning.
{{- end }}
{{- if eq true .Values.some.other.setting }}
{{ $NOTICE }}
This message is a notice.
{{- end }}
These examples use one of two predefined variables included at the top of the NOTES.txt
file that ensure consistent titles and spacing between each entry in the output.