Skip to content

Commit 0041a83

Browse files
committed
test/extension: add first version of server lib & topology mutation
extension
1 parent c79c8f5 commit 0041a83

File tree

13 files changed

+671
-109
lines changed

13 files changed

+671
-109
lines changed

exp/runtime/hooks/api/v1alpha1/topologymutation_types.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ type HolderReference struct {
171171
}
172172

173173
// ValidateTopology validates the Cluster topology after all patches have been applied.
174-
func ValidateTopology(*GeneratePatchesRequest, *GeneratePatchesResponse) {}
174+
func ValidateTopology(*ValidateTopologyRequest, *ValidateTopologyResponse) {}
175175

176176
func init() {
177177
catalogBuilder.RegisterHook(GeneratePatches, &runtimecatalog.HookMeta{

internal/controllers/topology/cluster/patches/inline/json_patch_generator.go

Lines changed: 3 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,14 @@ func New(patch *clusterv1.ClusterClassPatch) api.Generator {
5454
func (j *jsonPatchGenerator) Generate(_ context.Context, req *runtimehooksv1.GeneratePatchesRequest) *runtimehooksv1.GeneratePatchesResponse {
5555
resp := &runtimehooksv1.GeneratePatchesResponse{}
5656

57-
globalVariables := toMap(req.Variables)
57+
globalVariables := patchvariables.ToMap(req.Variables)
5858

5959
// Loop over all templates.
6060
errs := []error{}
6161
for i := range req.Items {
6262
item := &req.Items[i]
6363

64-
templateVariables := toMap(item.Variables)
64+
templateVariables := patchvariables.ToMap(item.Variables)
6565

6666
// Calculate the list of patches which match the current template.
6767
matchingPatches := []clusterv1.PatchDefinition{}
@@ -78,7 +78,7 @@ func (j *jsonPatchGenerator) Generate(_ context.Context, req *runtimehooksv1.Gen
7878
}
7979

8080
// Merge template-specific and global variables.
81-
variables, err := mergeVariableMaps(globalVariables, templateVariables)
81+
variables, err := patchvariables.MergeVariableMaps(globalVariables, templateVariables)
8282
if err != nil {
8383
errs = append(errs, errors.Wrapf(err, "failed to merge global and template-specific variables for item with uid %q", item.UID))
8484
continue
@@ -124,15 +124,6 @@ func (j *jsonPatchGenerator) Generate(_ context.Context, req *runtimehooksv1.Gen
124124
return resp
125125
}
126126

127-
// toMap converts a list of Variables to a map of JSON (name is the map key).
128-
func toMap(variables []runtimehooksv1.Variable) map[string]apiextensionsv1.JSON {
129-
variablesMap := map[string]apiextensionsv1.JSON{}
130-
for i := range variables {
131-
variablesMap[variables[i].Name] = variables[i].Value
132-
}
133-
return variablesMap
134-
}
135-
136127
// matchesSelector returns true if the GeneratePatchesRequestItem matches the selector.
137128
func matchesSelector(req *runtimehooksv1.GeneratePatchesRequestItem, templateVariables map[string]apiextensionsv1.JSON, selector clusterv1.PatchSelector) bool {
138129
gvk := req.Object.Object.GetObjectKind().GroupVersionKind()
@@ -357,53 +348,3 @@ func calculateTemplateData(variables map[string]apiextensionsv1.JSON) (map[strin
357348

358349
return res, nil
359350
}
360-
361-
// mergeVariableMaps merges variables.
362-
// NOTE: In case a variable exists in multiple maps, the variable from the latter map is preserved.
363-
// NOTE: The builtin variable object is merged instead of simply overwritten.
364-
func mergeVariableMaps(variableMaps ...map[string]apiextensionsv1.JSON) (map[string]apiextensionsv1.JSON, error) {
365-
res := make(map[string]apiextensionsv1.JSON)
366-
367-
for _, variableMap := range variableMaps {
368-
for variableName, variableValue := range variableMap {
369-
// If the variable already exits and is the builtin variable, merge it.
370-
if _, ok := res[variableName]; ok && variableName == patchvariables.BuiltinsName {
371-
mergedV, err := mergeBuiltinVariables(res[variableName], variableValue)
372-
if err != nil {
373-
return nil, errors.Wrapf(err, "failed to merge builtin variables")
374-
}
375-
res[variableName] = *mergedV
376-
continue
377-
}
378-
res[variableName] = variableValue
379-
}
380-
}
381-
382-
return res, nil
383-
}
384-
385-
// mergeBuiltinVariables merges builtin variable objects.
386-
// NOTE: In case a variable exists in multiple builtin variables, the variable from the latter map is preserved.
387-
func mergeBuiltinVariables(variableList ...apiextensionsv1.JSON) (*apiextensionsv1.JSON, error) {
388-
builtins := &patchvariables.Builtins{}
389-
390-
// Unmarshal all variables into builtins.
391-
// NOTE: This accumulates the fields on the builtins.
392-
// Fields will be overwritten by later Unmarshals if fields are
393-
// set on multiple variables.
394-
for _, variable := range variableList {
395-
if err := json.Unmarshal(variable.Raw, builtins); err != nil {
396-
return nil, errors.Wrapf(err, "failed to unmarshal builtin variable")
397-
}
398-
}
399-
400-
// Marshal builtins to JSON.
401-
builtinVariableJSON, err := json.Marshal(builtins)
402-
if err != nil {
403-
return nil, errors.Wrapf(err, "failed to marshal builtin variable")
404-
}
405-
406-
return &apiextensionsv1.JSON{
407-
Raw: builtinVariableJSON,
408-
}, nil
409-
}

internal/controllers/topology/cluster/patches/inline/json_patch_generator_test.go

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1759,33 +1759,6 @@ func TestCalculateTemplateData(t *testing.T) {
17591759
}
17601760
}
17611761

1762-
func TestMergeVariables(t *testing.T) {
1763-
t.Run("Merge variables", func(t *testing.T) {
1764-
g := NewWithT(t)
1765-
1766-
m, err := mergeVariableMaps(
1767-
map[string]apiextensionsv1.JSON{
1768-
patchvariables.BuiltinsName: {Raw: []byte(`{"cluster":{"name":"cluster-name","namespace":"default","topology":{"class":"clusterClass1","version":"v1.21.1"}}}`)},
1769-
"a": {Raw: []byte("a-different")},
1770-
"c": {Raw: []byte("c")},
1771-
},
1772-
map[string]apiextensionsv1.JSON{
1773-
// Verify that builtin variables are merged correctly and
1774-
// the latter variables take precedent ("cluster-name-overwrite").
1775-
patchvariables.BuiltinsName: {Raw: []byte(`{"controlPlane":{"replicas":3},"cluster":{"name":"cluster-name-overwrite"}}`)},
1776-
"a": {Raw: []byte("a")},
1777-
"b": {Raw: []byte("b")},
1778-
},
1779-
)
1780-
g.Expect(err).To(BeNil())
1781-
1782-
g.Expect(m).To(HaveKeyWithValue(patchvariables.BuiltinsName, apiextensionsv1.JSON{Raw: []byte(`{"cluster":{"name":"cluster-name-overwrite","namespace":"default","topology":{"version":"v1.21.1","class":"clusterClass1"}},"controlPlane":{"replicas":3}}`)}))
1783-
g.Expect(m).To(HaveKeyWithValue("a", apiextensionsv1.JSON{Raw: []byte("a")}))
1784-
g.Expect(m).To(HaveKeyWithValue("b", apiextensionsv1.JSON{Raw: []byte("b")}))
1785-
g.Expect(m).To(HaveKeyWithValue("c", apiextensionsv1.JSON{Raw: []byte("c")}))
1786-
})
1787-
}
1788-
17891762
// toJSONCompact is used to be able to write JSON values in a readable manner.
17901763
func toJSONCompact(value string) []byte {
17911764
var compactValue bytes.Buffer
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
Copyright 2021 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package variables
18+
19+
import (
20+
"encoding/json"
21+
22+
"github.com/pkg/errors"
23+
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
24+
)
25+
26+
// MergeVariableMaps merges variables.
27+
// This func is useful when merging global and template-specific variables.
28+
// NOTE: In case a variable exists in multiple maps, the variable from the latter map is preserved.
29+
// NOTE: The builtin variable object is merged instead of simply overwritten.
30+
func MergeVariableMaps(variableMaps ...map[string]apiextensionsv1.JSON) (map[string]apiextensionsv1.JSON, error) {
31+
res := make(map[string]apiextensionsv1.JSON)
32+
33+
for _, variableMap := range variableMaps {
34+
for variableName, variableValue := range variableMap {
35+
// If the variable already exits and is the builtin variable, merge it.
36+
if _, ok := res[variableName]; ok && variableName == BuiltinsName {
37+
mergedV, err := mergeBuiltinVariables(res[variableName], variableValue)
38+
if err != nil {
39+
return nil, errors.Wrapf(err, "failed to merge builtin variables")
40+
}
41+
res[variableName] = *mergedV
42+
continue
43+
}
44+
res[variableName] = variableValue
45+
}
46+
}
47+
48+
return res, nil
49+
}
50+
51+
// mergeBuiltinVariables merges builtin variable objects.
52+
// NOTE: In case a variable exists in multiple builtin variables, the variable from the latter map is preserved.
53+
func mergeBuiltinVariables(variableList ...apiextensionsv1.JSON) (*apiextensionsv1.JSON, error) {
54+
builtins := &Builtins{}
55+
56+
// Unmarshal all variables into builtins.
57+
// NOTE: This accumulates the fields on the builtins.
58+
// Fields will be overwritten by later Unmarshals if fields are
59+
// set on multiple variables.
60+
for _, variable := range variableList {
61+
if err := json.Unmarshal(variable.Raw, builtins); err != nil {
62+
return nil, errors.Wrapf(err, "failed to unmarshal builtin variable")
63+
}
64+
}
65+
66+
// Marshal builtins to JSON.
67+
builtinVariableJSON, err := json.Marshal(builtins)
68+
if err != nil {
69+
return nil, errors.Wrapf(err, "failed to marshal builtin variable")
70+
}
71+
72+
return &apiextensionsv1.JSON{
73+
Raw: builtinVariableJSON,
74+
}, nil
75+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
Copyright 2021 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package variables
18+
19+
import (
20+
"testing"
21+
22+
. "github.com/onsi/gomega"
23+
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
24+
)
25+
26+
func TestMergeVariables(t *testing.T) {
27+
t.Run("Merge variables", func(t *testing.T) {
28+
g := NewWithT(t)
29+
30+
m, err := MergeVariableMaps(
31+
map[string]apiextensionsv1.JSON{
32+
BuiltinsName: {Raw: []byte(`{"cluster":{"name":"cluster-name","namespace":"default","topology":{"class":"clusterClass1","version":"v1.21.1"}}}`)},
33+
"a": {Raw: []byte("a-different")},
34+
"c": {Raw: []byte("c")},
35+
},
36+
map[string]apiextensionsv1.JSON{
37+
// Verify that builtin variables are merged correctly and
38+
// the latter variables take precedent ("cluster-name-overwrite").
39+
BuiltinsName: {Raw: []byte(`{"controlPlane":{"replicas":3},"cluster":{"name":"cluster-name-overwrite"}}`)},
40+
"a": {Raw: []byte("a")},
41+
"b": {Raw: []byte("b")},
42+
},
43+
)
44+
g.Expect(err).To(BeNil())
45+
46+
g.Expect(m).To(HaveKeyWithValue(BuiltinsName, apiextensionsv1.JSON{Raw: []byte(`{"cluster":{"name":"cluster-name-overwrite","namespace":"default","topology":{"version":"v1.21.1","class":"clusterClass1"}},"controlPlane":{"replicas":3}}`)}))
47+
g.Expect(m).To(HaveKeyWithValue("a", apiextensionsv1.JSON{Raw: []byte("a")}))
48+
g.Expect(m).To(HaveKeyWithValue("b", apiextensionsv1.JSON{Raw: []byte("b")}))
49+
g.Expect(m).To(HaveKeyWithValue("c", apiextensionsv1.JSON{Raw: []byte("c")}))
50+
})
51+
}

internal/controllers/topology/cluster/patches/variables/variables.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,3 +346,12 @@ func ipFamilyToString(ipFamily clusterv1.ClusterIPFamily) string {
346346
return "Invalid"
347347
}
348348
}
349+
350+
// ToMap converts a list of Variables to a map of JSON (name is the map key).
351+
func ToMap(variables []runtimehooksv1.Variable) map[string]apiextensionsv1.JSON {
352+
variablesMap := map[string]apiextensionsv1.JSON{}
353+
for i := range variables {
354+
variablesMap[variables[i].Name] = variables[i].Value
355+
}
356+
return variablesMap
357+
}

test/e2e/cluster_upgrade_runtimesdk.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,10 @@ func clusterUpgradeWithRuntimeSDKSpec(ctx context.Context, inputGetter func() cl
214214
func extensionConfig(specName string, namespace *corev1.Namespace) *runtimev1.ExtensionConfig {
215215
return &runtimev1.ExtensionConfig{
216216
ObjectMeta: metav1.ObjectMeta{
217-
Name: fmt.Sprintf("%s-%s", specName, util.RandomString(6)),
217+
// FIXME(sbueringer): use constant name for now as we have to be able to reference it in the ClusterClass.
218+
// Random generate later on again when Yuvaraj's PR has split up the cluster lifecycle.
219+
//Name: fmt.Sprintf("%s-%s", specName, util.RandomString(6)),
220+
Name: specName,
218221
Annotations: map[string]string{
219222
runtimev1.InjectCAFromSecretAnnotation: fmt.Sprintf("%s/webhook-service-cert", namespace.Name),
220223
},

test/e2e/data/infrastructure-docker/v1beta1/clusterclass-quick-start-runtimesdk.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,8 @@ spec:
7575
# TODO: enable external patches once topology mutation is implemented
7676
# - name: lbImageRepository
7777
# external:
78-
# generateExtension: generate-patches.test-extension-config
79-
# validateExtension: validate-topology.test-extension-config
78+
# generateExtension: generate-patches.k8s-upgrade-with-runtimesdk
79+
# validateExtension: validate-topology.k8s-upgrade-with-runtimesdk
8080
- name: imageRepository
8181
description: "Sets the imageRepository used for the KubeadmControlPlane."
8282
definitions:
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
Copyright 2022 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
// Package topologymutation contains the handlers for the topologymutation webhook.
18+
package topologymutation
19+
20+
import (
21+
"context"
22+
"strconv"
23+
24+
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
25+
"k8s.io/apimachinery/pkg/runtime"
26+
"k8s.io/apimachinery/pkg/runtime/serializer"
27+
ctrl "sigs.k8s.io/controller-runtime"
28+
29+
runtimehooksv1 "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1"
30+
patchvariables "sigs.k8s.io/cluster-api/internal/controllers/topology/cluster/patches/variables"
31+
infrav1 "sigs.k8s.io/cluster-api/test/infrastructure/docker/api/v1beta1"
32+
)
33+
34+
// NewHandler returns a new topology mutation Handler.
35+
func NewHandler(scheme *runtime.Scheme) *Handler {
36+
return &Handler{
37+
decoder: serializer.NewCodecFactory(scheme).UniversalDecoder(
38+
infrav1.GroupVersion,
39+
),
40+
}
41+
}
42+
43+
// Handler is a topology mutation handler.
44+
type Handler struct {
45+
decoder runtime.Decoder
46+
}
47+
48+
// GeneratePatches returns a function that generates patches for the given request.
49+
func (h *Handler) GeneratePatches(ctx context.Context, req *runtimehooksv1.GeneratePatchesRequest, resp *runtimehooksv1.GeneratePatchesResponse) {
50+
log := ctrl.LoggerFrom(ctx)
51+
log.Info("GeneratePatches called")
52+
53+
walkTemplates(h.decoder, req, resp, func(obj runtime.Object, variables map[string]apiextensionsv1.JSON) error {
54+
if dockerClusterTemplate, ok := obj.(*infrav1.DockerClusterTemplate); ok {
55+
if err := patchDockerClusterTemplate(dockerClusterTemplate, variables); err != nil {
56+
return err
57+
}
58+
}
59+
60+
return nil
61+
})
62+
}
63+
64+
// patchDockerClusterTemplate patches the DockerClusterTepmlate.
65+
func patchDockerClusterTemplate(dockerClusterTemplate *infrav1.DockerClusterTemplate, templateVariables map[string]apiextensionsv1.JSON) error {
66+
// Get the variable value as JSON string.
67+
value, err := patchvariables.GetVariableValue(templateVariables, "lbImageRepository")
68+
if err != nil {
69+
return err
70+
}
71+
72+
// Unquote the JSON string.
73+
stringValue, err := strconv.Unquote(string(value.Raw))
74+
if err != nil {
75+
return err
76+
}
77+
78+
dockerClusterTemplate.Spec.Template.Spec.LoadBalancer.ImageRepository = stringValue
79+
return nil
80+
}
81+
82+
// ValidateTopology returns a function that validates the given request.
83+
func (h *Handler) ValidateTopology(ctx context.Context, _ *runtimehooksv1.ValidateTopologyRequest, resp *runtimehooksv1.ValidateTopologyResponse) {
84+
log := ctrl.LoggerFrom(ctx)
85+
log.Info("ValidateTopology called")
86+
87+
resp.Status = runtimehooksv1.ResponseStatusSuccess
88+
}

0 commit comments

Comments
 (0)