Skip to content

Commit

Permalink
Merge pull request #6635 from sbueringer/pr-runtime-sdk-test-extensio…
Browse files Browse the repository at this point in the history
…n-impl

🌱 test/extension: add first version of server lib & topology mutation
  • Loading branch information
k8s-ci-robot authored Jun 15, 2022
2 parents c79c8f5 + 0041a83 commit 72b68bf
Show file tree
Hide file tree
Showing 13 changed files with 671 additions and 109 deletions.
2 changes: 1 addition & 1 deletion exp/runtime/hooks/api/v1alpha1/topologymutation_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ type HolderReference struct {
}

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

func init() {
catalogBuilder.RegisterHook(GeneratePatches, &runtimecatalog.HookMeta{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,14 @@ func New(patch *clusterv1.ClusterClassPatch) api.Generator {
func (j *jsonPatchGenerator) Generate(_ context.Context, req *runtimehooksv1.GeneratePatchesRequest) *runtimehooksv1.GeneratePatchesResponse {
resp := &runtimehooksv1.GeneratePatchesResponse{}

globalVariables := toMap(req.Variables)
globalVariables := patchvariables.ToMap(req.Variables)

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

templateVariables := toMap(item.Variables)
templateVariables := patchvariables.ToMap(item.Variables)

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

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

// toMap converts a list of Variables to a map of JSON (name is the map key).
func toMap(variables []runtimehooksv1.Variable) map[string]apiextensionsv1.JSON {
variablesMap := map[string]apiextensionsv1.JSON{}
for i := range variables {
variablesMap[variables[i].Name] = variables[i].Value
}
return variablesMap
}

// matchesSelector returns true if the GeneratePatchesRequestItem matches the selector.
func matchesSelector(req *runtimehooksv1.GeneratePatchesRequestItem, templateVariables map[string]apiextensionsv1.JSON, selector clusterv1.PatchSelector) bool {
gvk := req.Object.Object.GetObjectKind().GroupVersionKind()
Expand Down Expand Up @@ -357,53 +348,3 @@ func calculateTemplateData(variables map[string]apiextensionsv1.JSON) (map[strin

return res, nil
}

// mergeVariableMaps merges variables.
// NOTE: In case a variable exists in multiple maps, the variable from the latter map is preserved.
// NOTE: The builtin variable object is merged instead of simply overwritten.
func mergeVariableMaps(variableMaps ...map[string]apiextensionsv1.JSON) (map[string]apiextensionsv1.JSON, error) {
res := make(map[string]apiextensionsv1.JSON)

for _, variableMap := range variableMaps {
for variableName, variableValue := range variableMap {
// If the variable already exits and is the builtin variable, merge it.
if _, ok := res[variableName]; ok && variableName == patchvariables.BuiltinsName {
mergedV, err := mergeBuiltinVariables(res[variableName], variableValue)
if err != nil {
return nil, errors.Wrapf(err, "failed to merge builtin variables")
}
res[variableName] = *mergedV
continue
}
res[variableName] = variableValue
}
}

return res, nil
}

// mergeBuiltinVariables merges builtin variable objects.
// NOTE: In case a variable exists in multiple builtin variables, the variable from the latter map is preserved.
func mergeBuiltinVariables(variableList ...apiextensionsv1.JSON) (*apiextensionsv1.JSON, error) {
builtins := &patchvariables.Builtins{}

// Unmarshal all variables into builtins.
// NOTE: This accumulates the fields on the builtins.
// Fields will be overwritten by later Unmarshals if fields are
// set on multiple variables.
for _, variable := range variableList {
if err := json.Unmarshal(variable.Raw, builtins); err != nil {
return nil, errors.Wrapf(err, "failed to unmarshal builtin variable")
}
}

// Marshal builtins to JSON.
builtinVariableJSON, err := json.Marshal(builtins)
if err != nil {
return nil, errors.Wrapf(err, "failed to marshal builtin variable")
}

return &apiextensionsv1.JSON{
Raw: builtinVariableJSON,
}, nil
}
Original file line number Diff line number Diff line change
Expand Up @@ -1759,33 +1759,6 @@ func TestCalculateTemplateData(t *testing.T) {
}
}

func TestMergeVariables(t *testing.T) {
t.Run("Merge variables", func(t *testing.T) {
g := NewWithT(t)

m, err := mergeVariableMaps(
map[string]apiextensionsv1.JSON{
patchvariables.BuiltinsName: {Raw: []byte(`{"cluster":{"name":"cluster-name","namespace":"default","topology":{"class":"clusterClass1","version":"v1.21.1"}}}`)},
"a": {Raw: []byte("a-different")},
"c": {Raw: []byte("c")},
},
map[string]apiextensionsv1.JSON{
// Verify that builtin variables are merged correctly and
// the latter variables take precedent ("cluster-name-overwrite").
patchvariables.BuiltinsName: {Raw: []byte(`{"controlPlane":{"replicas":3},"cluster":{"name":"cluster-name-overwrite"}}`)},
"a": {Raw: []byte("a")},
"b": {Raw: []byte("b")},
},
)
g.Expect(err).To(BeNil())

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}}`)}))
g.Expect(m).To(HaveKeyWithValue("a", apiextensionsv1.JSON{Raw: []byte("a")}))
g.Expect(m).To(HaveKeyWithValue("b", apiextensionsv1.JSON{Raw: []byte("b")}))
g.Expect(m).To(HaveKeyWithValue("c", apiextensionsv1.JSON{Raw: []byte("c")}))
})
}

// toJSONCompact is used to be able to write JSON values in a readable manner.
func toJSONCompact(value string) []byte {
var compactValue bytes.Buffer
Expand Down
75 changes: 75 additions & 0 deletions internal/controllers/topology/cluster/patches/variables/merge.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
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 variables

import (
"encoding/json"

"github.com/pkg/errors"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
)

// MergeVariableMaps merges variables.
// This func is useful when merging global and template-specific variables.
// NOTE: In case a variable exists in multiple maps, the variable from the latter map is preserved.
// NOTE: The builtin variable object is merged instead of simply overwritten.
func MergeVariableMaps(variableMaps ...map[string]apiextensionsv1.JSON) (map[string]apiextensionsv1.JSON, error) {
res := make(map[string]apiextensionsv1.JSON)

for _, variableMap := range variableMaps {
for variableName, variableValue := range variableMap {
// If the variable already exits and is the builtin variable, merge it.
if _, ok := res[variableName]; ok && variableName == BuiltinsName {
mergedV, err := mergeBuiltinVariables(res[variableName], variableValue)
if err != nil {
return nil, errors.Wrapf(err, "failed to merge builtin variables")
}
res[variableName] = *mergedV
continue
}
res[variableName] = variableValue
}
}

return res, nil
}

// mergeBuiltinVariables merges builtin variable objects.
// NOTE: In case a variable exists in multiple builtin variables, the variable from the latter map is preserved.
func mergeBuiltinVariables(variableList ...apiextensionsv1.JSON) (*apiextensionsv1.JSON, error) {
builtins := &Builtins{}

// Unmarshal all variables into builtins.
// NOTE: This accumulates the fields on the builtins.
// Fields will be overwritten by later Unmarshals if fields are
// set on multiple variables.
for _, variable := range variableList {
if err := json.Unmarshal(variable.Raw, builtins); err != nil {
return nil, errors.Wrapf(err, "failed to unmarshal builtin variable")
}
}

// Marshal builtins to JSON.
builtinVariableJSON, err := json.Marshal(builtins)
if err != nil {
return nil, errors.Wrapf(err, "failed to marshal builtin variable")
}

return &apiextensionsv1.JSON{
Raw: builtinVariableJSON,
}, nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
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 variables

import (
"testing"

. "github.com/onsi/gomega"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
)

func TestMergeVariables(t *testing.T) {
t.Run("Merge variables", func(t *testing.T) {
g := NewWithT(t)

m, err := MergeVariableMaps(
map[string]apiextensionsv1.JSON{
BuiltinsName: {Raw: []byte(`{"cluster":{"name":"cluster-name","namespace":"default","topology":{"class":"clusterClass1","version":"v1.21.1"}}}`)},
"a": {Raw: []byte("a-different")},
"c": {Raw: []byte("c")},
},
map[string]apiextensionsv1.JSON{
// Verify that builtin variables are merged correctly and
// the latter variables take precedent ("cluster-name-overwrite").
BuiltinsName: {Raw: []byte(`{"controlPlane":{"replicas":3},"cluster":{"name":"cluster-name-overwrite"}}`)},
"a": {Raw: []byte("a")},
"b": {Raw: []byte("b")},
},
)
g.Expect(err).To(BeNil())

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}}`)}))
g.Expect(m).To(HaveKeyWithValue("a", apiextensionsv1.JSON{Raw: []byte("a")}))
g.Expect(m).To(HaveKeyWithValue("b", apiextensionsv1.JSON{Raw: []byte("b")}))
g.Expect(m).To(HaveKeyWithValue("c", apiextensionsv1.JSON{Raw: []byte("c")}))
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -346,3 +346,12 @@ func ipFamilyToString(ipFamily clusterv1.ClusterIPFamily) string {
return "Invalid"
}
}

// ToMap converts a list of Variables to a map of JSON (name is the map key).
func ToMap(variables []runtimehooksv1.Variable) map[string]apiextensionsv1.JSON {
variablesMap := map[string]apiextensionsv1.JSON{}
for i := range variables {
variablesMap[variables[i].Name] = variables[i].Value
}
return variablesMap
}
5 changes: 4 additions & 1 deletion test/e2e/cluster_upgrade_runtimesdk.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,10 @@ func clusterUpgradeWithRuntimeSDKSpec(ctx context.Context, inputGetter func() cl
func extensionConfig(specName string, namespace *corev1.Namespace) *runtimev1.ExtensionConfig {
return &runtimev1.ExtensionConfig{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("%s-%s", specName, util.RandomString(6)),
// FIXME(sbueringer): use constant name for now as we have to be able to reference it in the ClusterClass.
// Random generate later on again when Yuvaraj's PR has split up the cluster lifecycle.
//Name: fmt.Sprintf("%s-%s", specName, util.RandomString(6)),
Name: specName,
Annotations: map[string]string{
runtimev1.InjectCAFromSecretAnnotation: fmt.Sprintf("%s/webhook-service-cert", namespace.Name),
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,8 @@ spec:
# TODO: enable external patches once topology mutation is implemented
# - name: lbImageRepository
# external:
# generateExtension: generate-patches.test-extension-config
# validateExtension: validate-topology.test-extension-config
# generateExtension: generate-patches.k8s-upgrade-with-runtimesdk
# validateExtension: validate-topology.k8s-upgrade-with-runtimesdk
- name: imageRepository
description: "Sets the imageRepository used for the KubeadmControlPlane."
definitions:
Expand Down
88 changes: 88 additions & 0 deletions test/extension/handlers/topologymutation/handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
Copyright 2022 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 topologymutation contains the handlers for the topologymutation webhook.
package topologymutation

import (
"context"
"strconv"

apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
ctrl "sigs.k8s.io/controller-runtime"

runtimehooksv1 "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1"
patchvariables "sigs.k8s.io/cluster-api/internal/controllers/topology/cluster/patches/variables"
infrav1 "sigs.k8s.io/cluster-api/test/infrastructure/docker/api/v1beta1"
)

// NewHandler returns a new topology mutation Handler.
func NewHandler(scheme *runtime.Scheme) *Handler {
return &Handler{
decoder: serializer.NewCodecFactory(scheme).UniversalDecoder(
infrav1.GroupVersion,
),
}
}

// Handler is a topology mutation handler.
type Handler struct {
decoder runtime.Decoder
}

// GeneratePatches returns a function that generates patches for the given request.
func (h *Handler) GeneratePatches(ctx context.Context, req *runtimehooksv1.GeneratePatchesRequest, resp *runtimehooksv1.GeneratePatchesResponse) {
log := ctrl.LoggerFrom(ctx)
log.Info("GeneratePatches called")

walkTemplates(h.decoder, req, resp, func(obj runtime.Object, variables map[string]apiextensionsv1.JSON) error {
if dockerClusterTemplate, ok := obj.(*infrav1.DockerClusterTemplate); ok {
if err := patchDockerClusterTemplate(dockerClusterTemplate, variables); err != nil {
return err
}
}

return nil
})
}

// patchDockerClusterTemplate patches the DockerClusterTepmlate.
func patchDockerClusterTemplate(dockerClusterTemplate *infrav1.DockerClusterTemplate, templateVariables map[string]apiextensionsv1.JSON) error {
// Get the variable value as JSON string.
value, err := patchvariables.GetVariableValue(templateVariables, "lbImageRepository")
if err != nil {
return err
}

// Unquote the JSON string.
stringValue, err := strconv.Unquote(string(value.Raw))
if err != nil {
return err
}

dockerClusterTemplate.Spec.Template.Spec.LoadBalancer.ImageRepository = stringValue
return nil
}

// ValidateTopology returns a function that validates the given request.
func (h *Handler) ValidateTopology(ctx context.Context, _ *runtimehooksv1.ValidateTopologyRequest, resp *runtimehooksv1.ValidateTopologyResponse) {
log := ctrl.LoggerFrom(ctx)
log.Info("ValidateTopology called")

resp.Status = runtimehooksv1.ResponseStatusSuccess
}
Loading

0 comments on commit 72b68bf

Please sign in to comment.