sigs.k8s.io/cluster-api@v1.7.1/internal/controllers/topology/cluster/patches/template.go (about)

     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 patches
    18  
    19  import (
    20  	"encoding/json"
    21  	"strconv"
    22  
    23  	"github.com/pkg/errors"
    24  	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    25  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    26  	"k8s.io/apimachinery/pkg/runtime"
    27  	"k8s.io/apimachinery/pkg/runtime/schema"
    28  	"k8s.io/apimachinery/pkg/runtime/serializer"
    29  	"k8s.io/apimachinery/pkg/types"
    30  	"k8s.io/apimachinery/pkg/util/uuid"
    31  	"sigs.k8s.io/controller-runtime/pkg/client"
    32  
    33  	runtimehooksv1 "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1"
    34  	"sigs.k8s.io/cluster-api/internal/controllers/topology/cluster/patches/variables"
    35  )
    36  
    37  // unstructuredDecoder is used to decode byte arrays into Unstructured objects.
    38  var unstructuredDecoder = serializer.NewCodecFactory(nil).UniversalDeserializer()
    39  
    40  // requestItemBuilder builds a GeneratePatchesRequestItem.
    41  type requestItemBuilder struct {
    42  	template *unstructured.Unstructured
    43  	holder   runtimehooksv1.HolderReference
    44  }
    45  
    46  // requestTopologyName is used to specify the topology name to match in a GeneratePatchesRequest.
    47  type requestTopologyName struct {
    48  	mdTopologyName string
    49  	mpTopologyName string
    50  }
    51  
    52  // newRequestItemBuilder returns a new requestItemBuilder.
    53  func newRequestItemBuilder(template *unstructured.Unstructured) *requestItemBuilder {
    54  	return &requestItemBuilder{
    55  		template: template,
    56  	}
    57  }
    58  
    59  // WithHolder adds holder to the requestItemBuilder.
    60  // Note: We pass in gvk explicitly as we can't rely on GVK being set on all objects
    61  // (only on Unstructured).
    62  func (t *requestItemBuilder) WithHolder(object client.Object, gvk schema.GroupVersionKind, fieldPath string) *requestItemBuilder {
    63  	t.holder = runtimehooksv1.HolderReference{
    64  		APIVersion: gvk.GroupVersion().String(),
    65  		Kind:       gvk.Kind,
    66  		Namespace:  object.GetNamespace(),
    67  		Name:       object.GetName(),
    68  		FieldPath:  fieldPath,
    69  	}
    70  	return t
    71  }
    72  
    73  // uuidGenerator is defined as a package variable to enable changing it during testing.
    74  var uuidGenerator func() types.UID = uuid.NewUUID
    75  
    76  // Build builds a GeneratePatchesRequestItem.
    77  func (t *requestItemBuilder) Build() (*runtimehooksv1.GeneratePatchesRequestItem, error) {
    78  	tpl := &runtimehooksv1.GeneratePatchesRequestItem{
    79  		HolderReference: t.holder,
    80  		UID:             uuidGenerator(),
    81  	}
    82  
    83  	jsonObj, err := json.Marshal(t.template)
    84  	if err != nil {
    85  		return nil, errors.Wrap(err, "failed to marshal template to JSON")
    86  	}
    87  
    88  	tpl.Object = runtime.RawExtension{
    89  		Raw:    jsonObj,
    90  		Object: t.template,
    91  	}
    92  
    93  	return tpl, nil
    94  }
    95  
    96  // getTemplateAsUnstructured is a utility func that returns a template matching the holderKind, holderFieldPath
    97  // and topologyNames from a GeneratePatchesRequest.
    98  func getTemplateAsUnstructured(req *runtimehooksv1.GeneratePatchesRequest, holderKind, holderFieldPath string, topologyNames requestTopologyName) (*unstructured.Unstructured, error) {
    99  	// Find the requestItem.
   100  	requestItem := getRequestItem(req, holderKind, holderFieldPath, topologyNames)
   101  
   102  	if requestItem == nil {
   103  		return nil, errors.Errorf("failed to get request item with holder kind %q, holder field path %q, MD topology name %q, and MP topology name %q", holderKind, holderFieldPath, topologyNames.mdTopologyName, topologyNames.mpTopologyName)
   104  	}
   105  
   106  	// Unmarshal the template.
   107  	template, err := bytesToUnstructured(requestItem.Object.Raw)
   108  	if err != nil {
   109  		return nil, errors.Wrapf(err, "failed to convert template to Unstructured")
   110  	}
   111  
   112  	return template, nil
   113  }
   114  
   115  // getRequestItemByUID is a utility func that returns a template matching the uid from a GeneratePatchesRequest.
   116  func getRequestItemByUID(req *runtimehooksv1.GeneratePatchesRequest, uid types.UID) *runtimehooksv1.GeneratePatchesRequestItem {
   117  	for i := range req.Items {
   118  		if req.Items[i].UID == uid {
   119  			return &req.Items[i]
   120  		}
   121  	}
   122  	return nil
   123  }
   124  
   125  // getRequestItem is a utility func that returns a template matching the holderKind, holderFiledPath and topologyNames from a GeneratePatchesRequest.
   126  func getRequestItem(req *runtimehooksv1.GeneratePatchesRequest, holderKind, holderFieldPath string, topologyNames requestTopologyName) *runtimehooksv1.GeneratePatchesRequestItem {
   127  	for _, template := range req.Items {
   128  		if holderKind != "" && template.HolderReference.Kind != holderKind {
   129  			continue
   130  		}
   131  		if holderFieldPath != "" && template.HolderReference.FieldPath != holderFieldPath {
   132  			continue
   133  		}
   134  		if topologyNames.mdTopologyName != "" {
   135  			templateVariables := toMap(template.Variables)
   136  			v, err := variables.GetVariableValue(templateVariables, "builtin.machineDeployment.topologyName")
   137  			if err != nil || string(v.Raw) != strconv.Quote(topologyNames.mdTopologyName) {
   138  				continue
   139  			}
   140  		}
   141  		if topologyNames.mpTopologyName != "" {
   142  			templateVariables := toMap(template.Variables)
   143  			v, err := variables.GetVariableValue(templateVariables, "builtin.machinePool.topologyName")
   144  			if err != nil || string(v.Raw) != strconv.Quote(topologyNames.mpTopologyName) {
   145  				continue
   146  			}
   147  		}
   148  
   149  		return &template
   150  	}
   151  	return nil
   152  }
   153  
   154  // bytesToUnstructured provides a utility method that converts a (JSON) byte array into an Unstructured object.
   155  func bytesToUnstructured(b []byte) (*unstructured.Unstructured, error) {
   156  	// Unmarshal the JSON.
   157  	u := &unstructured.Unstructured{}
   158  	if _, _, err := unstructuredDecoder.Decode(b, nil, u); err != nil {
   159  		return nil, errors.Wrap(err, "failed to unmarshal object from json")
   160  	}
   161  
   162  	return u, nil
   163  }
   164  
   165  // toMap converts a list of Variables to a map of JSON (name is the map key).
   166  func toMap(variables []runtimehooksv1.Variable) map[string]apiextensionsv1.JSON {
   167  	variablesMap := map[string]apiextensionsv1.JSON{}
   168  
   169  	for i := range variables {
   170  		variablesMap[variables[i].Name] = variables[i].Value
   171  	}
   172  	return variablesMap
   173  }