Skip to content

Markers

Operator Builder uses commented markers as the basis for defining a new API. The fields for a custom resource kind are created when it finds a +operator-builder marker in a source manifest.

A workload marker is commented out so the manifest is still valid and can be used if needed. The marker must begin with +operator-builder followed by some colon-separated fields:

These markers should always be provided as an in-line comment or as a head comment. The marker always begins with +operator-builder:field: or +operator-builder:collection:field: (more on this later).

That is followed by arguments separated by ,. Arguments can be given in any order.

Arguments

Arguments come after the actual marker and are separated from the marker name with a :. They are given in the format of argument=value and separated by the ,. Additionally, if the argument name is given by itself with no value, it is assumed to have an implict =true on the end and is treated as a flag.

Below you will find the supported markers and their supported arguments.

Field Markers

Defined as +operator-builder:field this marker can be used to define a CRD field for your workload.

Field Type Required
name string true
type string{string, int, bool, stringArray, stringMap} true
default type false
replace string false
merge bool false
arbitrary bool false
description string false

Name (required if Parent is unspecified)

The name you want to use for the field in the custom resource that Operator Builder will create. If you're not sure what that means, it will become clear shortly.

Example:

1
+operator-builder:field:name=myName,type=string

Parent (required if Name is unspecified)

The parent field in which you wish to substitute. Currently, only metadata.name and metadata.namespace are supported. This will allow you to use the parent name or namespace as a value in the child resource.

Example:

1
+operator-builder:field:parent=metadata.name,type=string

The metadata.name and metadata.namespace fields from the collection workload is also supported:

1
+operator-builder:collection:field:parent=metadata.name,type=string

Type (required)

The other required field is the type field which specifies the data type for the value.

The supported Go data types are:

  • bool
  • string
  • int
  • stringArray (an []string Go data type)
  • stringMap (a map[string]string Go data type)

ex. +operator-builder:field:name=myName,type=string

stringArray fields

Use type=stringArray to define a CRD field that holds a list of strings. Place the marker as a head comment above a YAML sequence:

1
2
3
4
# +operator-builder:field:name=allowedHosts,type=stringArray
allowedHosts:
  - host1.example.com
  - host2.example.com

The generated Go spec field will be []string and can carry an optional default using semicolon-separated syntax (aligning with kubebuilder's enum convention):

1
2
3
4
# +operator-builder:field:name=dnsServers,default=8.8.8.8;1.1.1.1,type=stringArray
dnsServers:
  - 8.8.8.8
  - 1.1.1.1

Note: The replace= argument is not supported for stringArray fields.

Note: Default values that start with - or contain =, ,, or : must be wrapped in double quotes so the marker parser treats them as string literals: default="--log-level;info". Unquoted values starting with - are tokenised as numeric literals, which causes a parse error.

Note: To include a literal semicolon inside one element, escape it with a backslash: default=foo\;bar;baz produces ["foo;bar", "baz"].

stringMap fields

Use type=stringMap to define a CRD field that holds a map[string]string. Place the marker as a head comment above a YAML mapping:

1
2
3
4
# +operator-builder:field:name=configData,type=stringMap
data:
  APP_ENV: production
  LOG_LEVEL: info

The generated Go spec field will be map[string]string.

stringMap fields are always optional. An absent or empty map is equivalent to map[string]string{} — no +kubebuilder:validation:Required is ever emitted for this type regardless of whether a default= is provided.

A default can be specified using semicolon-separated key=value pairs:

1
2
3
4
# +operator-builder:field:name=configData,default="APP_ENV=production;LOG_LEVEL=info",type=stringMap
data:
  APP_ENV: production
  LOG_LEVEL: info

Important: The default= value for a stringMap field must always be wrapped in double quotes. Each pair contains an = sign, and the marker parser uses = to separate argument names from values. Without quotes the parser would misread APP_ENV=production as two separate arguments. Always write: default="KEY1=value1;KEY2=value2".

Note: Values may themselves contain = signs — only the first = in each pair is treated as the key/value separator: default="JDBC_URL=jdbc:postgresql://host/db?ssl=true" produces {"JDBC_URL": "jdbc:postgresql://host/db?ssl=true"}.

Note: The replace= argument is not supported for stringMap fields.

Note: To include a literal semicolon inside a value, escape it with a backslash: default="A=foo\;bar" produces {"A": "foo;bar"}.

merge flag for stringMap fields

The merge flag merges the static key-value pairs already in the YAML manifest with the user-supplied map from the custom resource spec. User-supplied values always win — keys present in both the manifest and the spec take their value from the spec.

1
2
3
4
# +operator-builder:field:name=configData,type=stringMap,merge
data:
  LOG_LEVEL: info
  APP_ENV: production

Given the above marker, operator-builder emits code equivalent to:

1
2
3
4
5
6
// start with the static pairs from the manifest
m := map[string]string{"APP_ENV": "production", "LOG_LEVEL": "info"}
// overlay whatever the user provided — user values win
for k, v := range parent.Spec.ConfigData {
    m[k] = v
}

You can combine merge with a default= to seed the spec field as well:

1
2
3
# +operator-builder:field:name=configData,type=stringMap,merge,default="DEBUG=false"
data:
  LOG_LEVEL: info

This makes the configData spec field default to {"DEBUG": "false"}, which is then merged on top of the static {"LOG_LEVEL": "info"} at runtime.

Note: merge is only valid for stringMap fields. Using it with any other type returns a parse error.

Note: merge and replace= are mutually exclusive; use one or the other.

Default (optional)

This will make configuration optional for your operator's end user. the supplied value will be used for the default value. If a field has no default, it will be a required field in the custom resource. For example:

1
+operator-builder:field:name=myName,type=string,default=test

Replace (optional)

There may be some instances where you only want a specific portion of a value to be configurable (such as config maps). In these scenarios you can use the replace argument to specify a search string (or regex) to target for configuration.

Consider the following example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
apiVersion: v1
kind: ConfigMap
metadata:
  labels:
    # +operator-builder:field:name=environment,default=dev,type=string,replace="dev"
    app: myapp-dev
  name: contour-configmap
  namespace: ingress-system
data:
  # +operator-builder:field:name=configOption,default=myoption,type=string,replace="configuration2"
  # +operator-builder:field:name=yamlType,default=myoption,type=string,replace="multi.*yaml"
  config.yaml: |
    ---
    someoption: configuration2
    anotheroption: configuration1
    justtesting: multistringyaml

In this scenario three custom resource fields will be generated. The value from the environment field will replace the dev portion of myapp-dev. For example, if prod is provided as a value for the environment field, the resulting config map will get the label app: myapp-prod. Values from the configOption and yamlType fields will replace corresponding strings in the content of config.yaml. The resulting configmap will look as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
apiVersion: v1
kind: ConfigMap
metadata:
  labels:
    app: myapp-prod
  name: contour-configmap
  namespace: ingress-system
data:
  config.yaml: |-
    ---
    someoption: myoption
    anotheroption: configuration1
    justtesting: myoption

Merge (optional)

The merge flag is only valid for stringMap fields. When set, the static key-value pairs in the YAML manifest are used as a base and the user-supplied map from the custom resource spec is merged on top — user values always win.

1
2
3
# +operator-builder:field:name=configData,type=stringMap,merge
data:
  LOG_LEVEL: info

See stringMap merge for full details and examples.

Arbitrary (optional)

If you wish to create a field for a custom resource that does not directly map to a value in a child resource, mark a field as arbitrary.

Here is an example of how to mark a field as arbitrary:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
---
# +operator-builder:field:name=nginx.installType,arbitrary,default="deployment",type=string,description=`
# +kubebuilder:validation:Enum=deployment;daemonset
# Method of install nginx ingress controller.  One of: deployment | daemonset.`
apiVersion: v1
kind: Namespace
metadata:
  # +operator-builder:field:name=namespace,default="nukleros-ingress-system",type=string,description=`
  # Namespace to use for ingress support services.`
  name: nukleros-ingress-system

On the first line you can see the nginx.installType custom resource field is marked as arbitrary with the arbitrary marker field. Where you place this marker is unimportant but it is recommended you put all arbitrary fields at the beginning of one chosen manifest for ease of maintenance.

This will result in a custom resource sample that looks as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
apiVersion: platform.addons.nukleros.io/v1alpha1
kind: IngressComponent
metadata:
  name: ingresscomponent-sample
spec:
  #collection:
    #name: "supportservices-sample"
    #namespace: ""
  nginx:
    installType: "deployment"            # <---- arbitary field
    image: "nginx/nginx-ingress"
    version: "2.3.0"
    replicas: 2
  namespace: "nukleros-ingress-system"

This arbitrary field will not map to any child resource value. However it can be leveraged by some custom mutation code or by a resource marker such as this:

1
2
3
4
5
6
7
8
---
# +operator-builder:resource:field=nginx.installType,value="deployment",include
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-ingress
  namespace: nukleros-ingress-system # +operator-builder:field:name=namespace,default="nukleros-ingress-system",type=string
...

The marker on line one indicates the deployment resource only be created if nginx.installType has a value of deployment (as shown in the custom resource sample above). In this example, we are providing an option to install the Nginx Ingress Controller as a deployment or a daemonset.

Description (optional)

An optional description can be provided which will be used in the source code as a Doc String, backticks ` may be used to capture multiline strings (head comments only).

By injecting documentation to the CRD, the consumer of the custom resource gets the added benefit by being able to run kubectl explain against their resource and having documentation right at their fingertips without having to navigate to API documentation in order to see the usage of the API. For example:

1
+operator-builder:field:name=myName,type=string,default=test,description="Hello World"

Note: that you can use a single custom resource field name to configure multiple fields in the resource.

Consider the following Deployment:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion: apps/v1
kind: Deployment
metadata:
  name: webapp-deploy
  labels:
    production: false  # +operator-builder:field:name=production,default=false,type=bool
spec:
  replicas: 2  # +operator-builder:field:name=webAppReplicas,default=2,type=int
  selector:
    matchLabels:
      app: webapp
  template:
    metadata:
      labels:
        app: webapp
    spec:
      containers:
      - name: webapp-container
        image: nginx:1.17  # +operator-builder:field:name=webAppImage,type=string
        ports:
        - containerPort: 8080

In this case, operator-builder will create and add three fields to the custom resource:

  • A production field that is a boolean. It will have a default of false and will inform the value of the label when the deployment is configured.
  • A webAppReplicas field that will default to 2 and allow the user to specify the number of replicas for the deployment in the custom resource manifest.
  • A webAppImage field that will set the value for the images used in the pods.

Now the end-user of the operator will be able to define a custom resource similar to the following to configure the deployment created:

1
2
3
4
5
6
7
8
apiVersion: product.apps.acme.com/v1alpha1
kind: WebApp
metadata:
  name: dev-webapp
spec:
  production: false
  webAppReplicas: 2
  webAppImage: acmerepo/webapp:3.5.3

Struct Markers

Defined as +operator-builder:struct this marker annotates a generated Go struct type with a description. Unlike a field marker, it does not create a new API field — it attaches documentation to a struct that is already created by the field markers nested within it.

Field Type Required
name string true
description string true

Struct Name (required)

The dot-separated path of the struct to annotate, matching the prefix used in the nested field marker names. For example, if you have a field marker name=subscriptionManager.image, the corresponding struct name is subscriptionManager.

Use the special value "." to target the root Spec struct itself.

1
2
3
4
# +operator-builder:struct:name=subscriptionManager,description="Configures the subscription manager"
subscriptionManager:
  # +operator-builder:field:name=subscriptionManager.image,type=string,default="nginx"
  image: nginx
1
2
3
4
# +operator-builder:struct:name=".",description="Root configuration for the WebStore operator"
apiVersion: apps/v1
kind: Deployment
...

Struct Description (required)

The description to emit as a comment above both the field declaration in the parent struct and the type declaration. kubebuilder reads both locations to populate description in the generated CRD schema, so this text will appear in kubectl explain output.

1
2
3
4
5
6
$ kubectl explain myapp.spec.subscriptionManager
KIND:     MyApp
VERSION:  apps.acme.com/v1alpha1

FIELD:    subscriptionManager <Object>
    Configures the subscription manager

Struct marker constraints

A struct marker must have at least one field marker nested within the named struct. If the named path does not correspond to any existing struct node (because no field markers were defined for it), processing fails with an error.

The marker can be placed as a head comment above any YAML node in the resource manifest — it does not need to be directly above the YAML key that corresponds to the struct. Placing it above the first node of the manifest document or above the relevant map key are both valid and common patterns.

Struct markers in collection components

Struct markers work the same way in collection components. A struct marker placed in a component resource file annotates that component's spec; a struct marker placed in a collection resource file annotates the collection's spec.

1
2
3
4
5
# in a collection resource file
# +operator-builder:struct:name=".",description="Cloud Native Platform collection configuration"
apiVersion: storage.k8s.io/v1
kind: StorageClass
...
1
2
3
4
5
6
7
# in a component resource file
# +operator-builder:struct:name=nsOperator,description="Configures the Namespace Operator component"
apiVersion: apps/v1
kind: Deployment
...
  # +operator-builder:field:name=nsOperator.image,type=string
  image: nginx:1.17

Generated output

Given a struct marker and matching field markers, operator-builder emits the description in two places in the generated _types.go file:

1
2
3
4
5
6
7
8
9
// +kubebuilder:validation:Optional
//
// Configures the subscription manager
SubscriptionManager WebAppSpecSubscriptionManagerSpec `json:"subscriptionManager,omitempty"`

// Configures the subscription manager
type WebAppSpecSubscriptionManagerSpec struct {
    // ...
}

Collection Markers

A second marker type +operator-builder:collection:field can be used with the same arguments as a Field Marker. These markers are used to define global fields for your Collection and can be used in any of its associated components.

If you include any marker on a collection resource it will be treated as a collection marker and will configure a field in the collection's custom resource.

Resource Markers

Defined as +operator-builder:resource this marker can be used to control a specific resource with arguments in the marker.

Note: a resource marker must reference a field defined by a field marker. If you include a resource marker with a unique field name that is not also defined by a field marker you will get an error. You may use an arbitrary field on a field marker if you don't wish to associate the field with a value in a child resource.

Field Type Required
field string true
collectionField string{string, int, bool} true
value type true
include bool true

Field / CollectionField (required)

The conditional field to associate with an action (currently only include). One of field or collectionField must be provided depending upon if you are checking a condition against a collection, or a component/standalone workload spec. The field input relates directly to a given workload marker such as +operator-builder:field:name=provider would produce a field of provider to be used in a resource marker with argument field=provider.

ex. +operator-builder:resource:collectionField=provider,value="aws",include ex. +operator-builder:resource:field=provider,value="aws",include=false

Value (required)

The conditional value to associate with an action (currently only include - see above). The value input relates directly to the value of field as it exists in the API spec requested by the user.

Examples:

1
2
+operator-builder:resource:collectionField=provider,value="aws",include
+operator-builder:resource:field=provider,value="aws",include=false

Include (required)

The action to perform on the resource. Include will include the resource for deployment during a control loop given a field or collectionField and a value. Using this means that the resource will only be included if this condition is met. If the condition is not met, the resource will not be deployed.

Here are some sample marker examples:

1
2
3
4
+operator-builder:resource:field=provider,value="aws",include
+operator-builder:resource:field=provider,value="aws",include=true
+operator-builder:resource:collectionField=provider,value="aws",include
+operator-builder:resource:collectionField=provider,value="aws",include=true

With include set to false, the opposite is true and the resource is excluded from being deployed during a control loop if a condition is met:

Examples:

1
2
+operator-builder:resource:field=provider,value="aws",include=false
+operator-builder:resource:collectionField=provider,value="aws",include=false

At this time, the include argument with field and value can be simply thought of as (pseudo-code):

1
2
3
4
5
if field == value {
  if include {
    includeResource()
  }
}

IMPORTANT: A resource marker is not required and should only be used when there is a desire to act upon a resource. If no resource marker is provided, a resource is always deployed during a control loop.

Include Resource On Condition

Below is a sample of how to include a resource only if a condition is met. If the condition is not met, the resource is not deployed during the control loop:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# +operator-builder:resource:field=provider,value="aws",include
---
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: aws-storage-class
  annotations:
    storageclass.kubernetes.io/is-default-class: true
  labels:
    provider: "aws" # +operator-builder:field:name=provider,type=string,default="aws"
allowVolumeExpansion: false
provisioner: "kubernetes.io/aws-ebs"
reclaimPolicy: "Delete"
volumeBindingMode: WaitForFirstConsumer
parameters:
  type: "gp2"
  iopsPerGB: 10
  fsType: "ext4"
  encrypted: false

Given the below CRD, the resource would be included:

1
2
3
4
5
6
7
apiVersion: apps.acme.com/v1alpha1
kind: Sample
metadata:
  name: sample
  namespace: default
spec:
  provider: "aws"

Given the below CRD, the resource would NOT be included:

1
2
3
4
5
6
7
apiVersion: apps.acme.com/v1alpha1
kind: Sample
metadata:
  name: sample
  namespace: default
spec:
  provider: "azure"

Exclude Resource On Condition

Below is a sample of how to exclude a resource only if a condition is met. If the condition is not met, the resource is not deployed during the control loop:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# +operator-builder:resource:field=provider,value="azure",include=false
---
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: aws-storage-class
  annotations:
    storageclass.kubernetes.io/is-default-class: true
  labels:
    provider: "aws" # +operator-builder:field:name=provider,type=string,default="aws"
allowVolumeExpansion: false
provisioner: "kubernetes.io/aws-ebs"
reclaimPolicy: "Delete"
volumeBindingMode: WaitForFirstConsumer
parameters:
  type: "gp2"
  iopsPerGB: 10
  fsType: "ext4"
  encrypted: false

Given the below CRD, the resource would be included:

1
2
3
4
5
6
7
apiVersion: apps.acme.com/v1alpha1
kind: Sample
metadata:
  name: sample
  namespace: default
spec:
  provider: "aws"

Given the below CRD, the resource would NOT be included:

1
2
3
4
5
6
7
apiVersion: apps.acme.com/v1alpha1
kind: Sample
metadata:
  name: sample
  namespace: default
spec:
  provider: "azure"

Stacking Resource Markers

You can include multiple resource markers on a particular resource. For example:

1
2
3
4
5
6
7
8
---
# +operator-builder:resource:field=nginx.include,value=true,include
# +operator-builder:resource:field=nginx.installType,value="deployment",include
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-ingress
...

The purpose of the first marker is to include all nginx ingress contoller resources when spec.nginx.include: true. The second gives users a choice to install nginx ingress controller as a deployment or daemonset. When spec.nginx.installType: deployment the deployment resource is included. Therefore the custom resource will need to look as follows for this deployment resource to be created:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
apiVersion: platform.addons.nukleros.io/v1alpha1
kind: IngressComponent
metadata:
  name: ingresscomponent-sample
spec:
  nginx:
    installType: "deployment"  # if not "deployment" deployment resource excluded
    include: true  # if false, no nginx resources are created
    image: "nginx/nginx-ingress"
    version: "2.3.0"
    replicas: 2

The resulting source code looks as follows. If either if-statement is evaluated as true, the function will return without any object - hence the deployment will not be included.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// CreateDeploymentNamespaceNginxIngress creates the Deployment resource with name nginx-ingress.
func CreateDeploymentNamespaceNginxIngress(
    parent *platformv1alpha1.IngressComponent,
    collection *setupv1alpha1.SupportServices,
    reconciler workload.Reconciler,
    req *workload.Request,
) ([]client.Object, error) {

    if parent.Spec.Nginx.Include != true {
        return []client.Object{}, nil
    }

    if parent.Spec.Nginx.InstallType != "deployment" {
        return []client.Object{}, nil
    }

    var resourceObj = &unstructured.Unstructured{
        Object: map[string]interface{}{
            // +operator-builder:resource:field=nginx.include,value=true,include
            // +operator-builder:resource:field=nginx.installType,value="deployment",include
            "apiVersion": "apps/v1",
            "kind":       "Deployment",
      ...
            }

    return mutate.MutateDeploymentNamespaceNginxIngress(resourceObj, parent, collection, reconciler, req)
}