Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,27 +1,31 @@

# Writing a ClusterClass
# Writing a ClusterClass <!-- omit in toc -->

A ClusterClass becomes more useful and valuable when it can be used to create many Cluster of a similar
shape. The goal of this document is to explain how ClusterClasses can be written in a way that they are
flexible enough to be used in as many Clusters as possible by supporting variants of the same base Cluster shape.

**Table of Contents**

* [Basic ClusterClass](#basic-clusterclass)
* [ClusterClass with MachineHealthChecks](#clusterclass-with-machinehealthchecks)
* [ClusterClass with patches](#clusterclass-with-patches)
* [ClusterClass with custom naming strategies](#clusterclass-with-custom-naming-strategies)
* [Defining a custom naming strategy for ControlPlane objects](#defining-a-custom-naming-strategy-for-controlplane-objects)
* [Defining a custom naming strategy for MachineDeployment objects](#defining-a-custom-naming-strategy-for-machinedeployment-objects)
* [Defining a custom naming strategy for MachinePool objects](#defining-a-custom-naming-strategy-for-machinepool-objects)
* [Advanced features of ClusterClass with patches](#advanced-features-of-clusterclass-with-patches)
* [MachineDeployment variable overrides](#machinedeployment-and-machinepool-variable-overrides)
* [Builtin variables](#builtin-variables)
* [Complex variable types](#complex-variable-types)
* [Using variable values in JSON patches](#using-variable-values-in-json-patches)
* [Optional patches](#optional-patches)
* [Version-aware patches](#version-aware-patches)
* [JSON patches tips &amp; tricks](#json-patches-tips--tricks)
- [Basic ClusterClass](#basic-clusterclass)
- [ClusterClass with MachinePools](#clusterclass-with-machinepools)
- [ClusterClass with MachineHealthChecks](#clusterclass-with-machinehealthchecks)
- [ClusterClass with patches](#clusterclass-with-patches)
- [ClusterClass with custom naming strategies](#clusterclass-with-custom-naming-strategies)
- [Defining a custom naming strategy for ControlPlane objects](#defining-a-custom-naming-strategy-for-controlplane-objects)
- [Defining a custom naming strategy for MachineDeployment objects](#defining-a-custom-naming-strategy-for-machinedeployment-objects)
- [Defining a custom naming strategy for MachinePool objects](#defining-a-custom-naming-strategy-for-machinepool-objects)
- [Defining a custom namespace for ClusterClass object](#defining-a-custom-namespace-for-clusterclass-object)
- [Securing cross-namespace reference to the ClusterClass](#securing-cross-namespace-reference-to-the-clusterclass)
- [Advanced features of ClusterClass with patches](#advanced-features-of-clusterclass-with-patches)
- [MachineDeployment and MachinePool variable overrides](#machinedeployment-and-machinepool-variable-overrides)
- [Builtin variables](#builtin-variables)
- [Complex variable types](#complex-variable-types)
- [Using variable values in JSON patches](#using-variable-values-in-json-patches)
- [Optional patches](#optional-patches)
- [Version-aware patches](#version-aware-patches)
- [tpl and include functions](#tpl-and-include-functions)
- [JSON patches tips \& tricks](#json-patches-tips--tricks)

## Basic ClusterClass

Expand Down Expand Up @@ -972,6 +976,87 @@ A simple approach to solve this problem is to define a map of version-aware vari
being the Kubernetes version. Patch could then use the proper builtin variables as a lookup entry to fetch
the corresponding values for the Kubernetes version in use by each object.

### tpl and include functions

The `tpl` function allows to evaluate strings as templates inside a template. This is useful to pass a template string as a variable.

First argument is the template string, second argument is the context.

```yaml
apiVersion: cluster.x-k8s.io/v1beta2
kind: ClusterClass
metadata:
name: docker-clusterclass-v0.1.0
spec:
...
variables:
- name: customImage
required: true
schema:
openAPIV3Schema:
type: string
description: Custom Image is the container registry to pull images from.
default: kindest/node:{{ .builtin.machineDeployment.version }}
example: kindest/node:{{ .builtin.machineDeployment.version }}
...
patches:
- name: customImage
description: "Sets the container image that is used for running dockerMachines."
definitions:
- selector:
apiVersion: infrastructure.cluster.x-k8s.io/v1beta2
kind: DockerMachineTemplate
matchResources:
machineDeploymentClass:
names:
- default-worker
jsonPatches:
- op: add
path: /spec/template/spec/customImage
valueFrom:
template: |
{{ tpl .customImage . }}
```

For complex use-cases, you can use the `include` with template definitions:

```yaml
apiVersion: cluster.x-k8s.io/v1beta2
kind: ClusterClass
metadata:
name: docker-clusterclass-v0.1.0
spec:
...
variables:
- name: customImage
required: true
schema:
openAPIV3Schema:
type: string
description: Custom Image is the container registry to pull images from.
default: kindest/node:{{ include "default-version". }}
example: kindest/node:{{ include "default-version". }}
...
patches:
- name: customImage
description: "Sets the container image that is used for running dockerMachines."
definitions:
- selector:
apiVersion: infrastructure.cluster.x-k8s.io/v1beta2
kind: DockerMachineTemplate
matchResources:
machineDeploymentClass:
names:
- default-worker
jsonPatches:
- op: add
path: /spec/template/spec/customImage
valueFrom:
template: |
{{- define "default-version" }}{{ .builtin.machineDeployment.version }}{{- end -}}
{{ tpl .customImage . }}
```

## JSON patches tips & tricks

JSON patches specification [RFC6902] requires that the target of
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import (
"strings"
"text/template"

"github.com/Masterminds/sprig/v3"
"github.com/pkg/errors"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
kerrors "k8s.io/apimachinery/pkg/util/errors"
Expand All @@ -39,6 +38,7 @@ import (
"sigs.k8s.io/cluster-api/internal/contract"
"sigs.k8s.io/cluster-api/internal/controllers/topology/cluster/patches/api"
patchvariables "sigs.k8s.io/cluster-api/internal/controllers/topology/cluster/patches/variables"
"sigs.k8s.io/cluster-api/internal/topology/templates"
)

// jsonPatchGenerator generates JSON patches for a GeneratePatchesRequest based on a ClusterClassPatch.
Expand Down Expand Up @@ -319,7 +319,9 @@ func calculateValue(patch clusterv1.JSONPatch, variables map[string]apiextension
// renderValueTemplate renders a template with the given variables as data.
func renderValueTemplate(valueTemplate string, variables map[string]apiextensionsv1.JSON) (*apiextensionsv1.JSON, error) {
// Parse the template.
tpl, err := template.New("tpl").Funcs(sprig.HermeticTxtFuncMap()).Parse(valueTemplate)
tpl := template.New("tpl")
tpl.Funcs(templates.TemplateFunctions(tpl))
_, err := tpl.Parse(valueTemplate)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse template: %q", valueTemplate)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,14 @@ func TestGenerate(t *testing.T) {
Template: `[{"contentFrom":{"secret":{"key":"control-plane-azure.json","name":"{{ .builtin.controlPlane.machineTemplate.infrastructureRef.name }}-azure-json"}}}]`,
},
},
// tpl function is available.
{
Op: "replace",
Path: "/spec/tplFunction",
ValueFrom: &clusterv1.JSONPatchValue{
Template: `{{ tpl .variableABC . }}`,
},
},
},
},
},
Expand All @@ -133,6 +141,10 @@ func TestGenerate(t *testing.T) {
Name: "variableC",
Value: apiextensionsv1.JSON{Raw: []byte(`"C"`)},
},
{
Name: "variableABC",
Value: apiextensionsv1.JSON{Raw: []byte(`"{{ .variableA }}-{{ .variableB }}-{{ .variableC }}"`)},
},
},
Items: []runtimehooksv1.GeneratePatchesRequestItem{
{
Expand Down Expand Up @@ -183,7 +195,9 @@ func TestGenerate(t *testing.T) {
"name":"controlPlaneInfrastructureMachineTemplate1-azure-json"
}
}
}]}]`),
}]},
{"op":"replace","path":"/spec/tplFunction","value":"A-B-C-template"}
]`),
PatchType: runtimehooksv1.JSONPatchType,
},
},
Expand Down Expand Up @@ -417,8 +431,8 @@ func TestGenerate(t *testing.T) {

got, err := NewGenerator(tt.patch).Generate(context.Background(), &clusterv1.Cluster{ObjectMeta: metav1.ObjectMeta{Namespace: "default"}}, tt.req)

g.Expect(got).To(BeComparableTo(tt.want))
g.Expect(err).ToNot(HaveOccurred())
g.Expect(got).To(BeComparableTo(tt.want))
})
}
}
Expand Down
107 changes: 107 additions & 0 deletions internal/topology/templates/template_functions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*
Copyright 2021 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package templates

import (
"errors"
"fmt"
"strings"
"text/template"

"github.com/Masterminds/sprig/v3"
)

const recursionMaxNums = 1000

// Copied from Helm
// https://github.com/helm/helm/blob/f05c21b6b6380d923696a7684c80a5e0847fbd7b/pkg/engine/engine.go#L132
// 'include' needs to be defined in the scope of a 'tpl' template as
// well as regular file-loaded templates.
func includeFun(t *template.Template, includedNames map[string]int) func(string, interface{}) (string, error) {
return func(name string, data interface{}) (string, error) {
var buf strings.Builder
if v, ok := includedNames[name]; ok {
if v > recursionMaxNums {
return "", fmt.Errorf(
"rendering template has a nested reference name: %s: %w",
name, errors.New("unable to execute template"))
}
includedNames[name]++
} else {
includedNames[name] = 1
}
err := t.ExecuteTemplate(&buf, name, data)
includedNames[name]--
return buf.String(), err
}
}

// Copied from Helm
// https://github.com/helm/helm/blob/f05c21b6b6380d923696a7684c80a5e0847fbd7b/pkg/engine/engine.go#L153
// As does 'tpl', so that nested calls to 'tpl' see the templates
// defined by their enclosing contexts.
func tplFun(parent *template.Template, includedNames map[string]int, strict bool) func(string, interface{}) (string, error) {
return func(tpl string, vals interface{}) (string, error) {
t, err := parent.Clone()
if err != nil {
return "", fmt.Errorf("cannot clone template: %w", err)
}

// Re-inject the missingkey option, see text/template issue https://github.com/golang/go/issues/43022
// We have to go by strict from our engine configuration, as the option fields are private in Template.
// TODO: Remove workaround (and the strict parameter) once we build only with golang versions with a fix.
if strict {
t.Option("missingkey=error")
} else {
t.Option("missingkey=zero")
}

// Re-inject 'include' so that it can close over our clone of t;
// this lets any 'define's inside tpl be 'include'd.
t.Funcs(template.FuncMap{
"include": includeFun(t, includedNames),
"tpl": tplFun(t, includedNames, strict),
})

// We need a .New template, as template text which is just blanks
// or comments after parsing out defines just adds new named
// template definitions without changing the main template.
// https://pkg.go.dev/text/template#Template.Parse
// Use the parent's name for lack of a better way to identify the tpl
// text string. (Maybe we could use a hash appended to the name?)
t, err = t.New(parent.Name()).Parse(tpl)
if err != nil {
return "", fmt.Errorf("cannot parse template %q: %w", tpl, err)
}

var buf strings.Builder
if err := t.Execute(&buf, vals); err != nil {
return "", fmt.Errorf("error during tpl function execution for %q: %w", tpl, err)
}

// See comment in renderWithReferences explaining the <no value> hack.
return strings.ReplaceAll(buf.String(), "<no value>", ""), nil
}
}

func TemplateFunctions(t *template.Template) template.FuncMap {
funcs := sprig.HermeticTxtFuncMap()
includedNames := make(map[string]int)
funcs["include"] = includeFun(t, includedNames)
funcs["tpl"] = tplFun(t, includedNames, true)
return funcs
}
Loading