sigs.k8s.io/cluster-api@v1.7.1/internal/controllers/topology/cluster/patches/inline/json_patch_generator.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 inline implements the inline JSON patch generator. 18 package inline 19 20 import ( 21 "bytes" 22 "context" 23 "encoding/json" 24 "strconv" 25 "strings" 26 "text/template" 27 28 "github.com/Masterminds/sprig/v3" 29 "github.com/pkg/errors" 30 apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 31 kerrors "k8s.io/apimachinery/pkg/util/errors" 32 "sigs.k8s.io/controller-runtime/pkg/client" 33 "sigs.k8s.io/yaml" 34 35 clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" 36 runtimehooksv1 "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1" 37 "sigs.k8s.io/cluster-api/exp/runtime/topologymutation" 38 "sigs.k8s.io/cluster-api/internal/contract" 39 "sigs.k8s.io/cluster-api/internal/controllers/topology/cluster/patches/api" 40 patchvariables "sigs.k8s.io/cluster-api/internal/controllers/topology/cluster/patches/variables" 41 ) 42 43 // jsonPatchGenerator generates JSON patches for a GeneratePatchesRequest based on a ClusterClassPatch. 44 type jsonPatchGenerator struct { 45 patch *clusterv1.ClusterClassPatch 46 } 47 48 // NewGenerator returns a new inline Generator from a given ClusterClassPatch object. 49 func NewGenerator(patch *clusterv1.ClusterClassPatch) api.Generator { 50 return &jsonPatchGenerator{ 51 patch: patch, 52 } 53 } 54 55 // Generate generates JSON patches for the given GeneratePatchesRequest based on a ClusterClassPatch. 56 func (j *jsonPatchGenerator) Generate(_ context.Context, _ client.Object, req *runtimehooksv1.GeneratePatchesRequest) (*runtimehooksv1.GeneratePatchesResponse, error) { 57 resp := &runtimehooksv1.GeneratePatchesResponse{} 58 59 globalVariables := topologymutation.ToMap(req.Variables) 60 61 // Loop over all templates. 62 errs := []error{} 63 for i := range req.Items { 64 item := &req.Items[i] 65 objectKind := item.Object.Object.GetObjectKind().GroupVersionKind().Kind 66 67 templateVariables := topologymutation.ToMap(item.Variables) 68 69 // Calculate the list of patches which match the current template. 70 matchingPatches := []clusterv1.PatchDefinition{} 71 for _, patch := range j.patch.Definitions { 72 // Add the patch to the list, if it matches the template. 73 if matchesSelector(item, templateVariables, patch.Selector) { 74 matchingPatches = append(matchingPatches, patch) 75 } 76 } 77 78 // Continue if there are no matching patches. 79 if len(matchingPatches) == 0 { 80 continue 81 } 82 83 // Merge template-specific and global variables. 84 variables, err := topologymutation.MergeVariableMaps(globalVariables, templateVariables) 85 if err != nil { 86 errs = append(errs, errors.Wrapf(err, "failed to merge global and template-specific variables for %q", objectKind)) 87 continue 88 } 89 90 enabled, err := patchIsEnabled(j.patch.EnabledIf, variables) 91 if err != nil { 92 errs = append(errs, errors.Wrapf(err, "failed to calculate if patch is enabled for %q", objectKind)) 93 continue 94 } 95 if !enabled { 96 // Continue if patch is not enabled. 97 continue 98 } 99 100 // Loop over all PatchDefinitions. 101 for _, patch := range matchingPatches { 102 // Generate JSON patches. 103 jsonPatches, err := generateJSONPatches(patch.JSONPatches, variables) 104 if err != nil { 105 errs = append(errs, errors.Wrapf(err, "failed to generate JSON patches for %q", objectKind)) 106 continue 107 } 108 109 // Add jsonPatches to the response. 110 resp.Items = append(resp.Items, runtimehooksv1.GeneratePatchesResponseItem{ 111 UID: item.UID, 112 Patch: jsonPatches, 113 PatchType: runtimehooksv1.JSONPatchType, 114 }) 115 } 116 } 117 118 if err := kerrors.NewAggregate(errs); err != nil { 119 return nil, err 120 } 121 122 return resp, nil 123 } 124 125 // matchesSelector returns true if the GeneratePatchesRequestItem matches the selector. 126 func matchesSelector(req *runtimehooksv1.GeneratePatchesRequestItem, templateVariables map[string]apiextensionsv1.JSON, selector clusterv1.PatchSelector) bool { 127 gvk := req.Object.Object.GetObjectKind().GroupVersionKind() 128 129 // Check if the apiVersion and kind are matching. 130 if gvk.GroupVersion().String() != selector.APIVersion { 131 return false 132 } 133 if gvk.Kind != selector.Kind { 134 return false 135 } 136 137 // Check if the request is for an InfrastructureCluster. 138 if selector.MatchResources.InfrastructureCluster { 139 // Cluster.spec.infrastructureRef holds the InfrastructureCluster. 140 if req.HolderReference.Kind == "Cluster" && req.HolderReference.FieldPath == "spec.infrastructureRef" { 141 return true 142 } 143 } 144 145 // Check if the request is for a ControlPlane or the InfrastructureMachineTemplate of a ControlPlane. 146 if selector.MatchResources.ControlPlane { 147 // Cluster.spec.controlPlaneRef holds the ControlPlane. 148 if req.HolderReference.Kind == "Cluster" && req.HolderReference.FieldPath == "spec.controlPlaneRef" { 149 return true 150 } 151 // *.spec.machineTemplate.infrastructureRef holds the InfrastructureMachineTemplate of a ControlPlane. 152 // Note: this field path is only used in this context. 153 if req.HolderReference.FieldPath == strings.Join(contract.ControlPlane().MachineTemplate().InfrastructureRef().Path(), ".") { 154 return true 155 } 156 } 157 158 // Check if the request is for a BootstrapConfigTemplate or an InfrastructureMachineTemplate 159 // of one of the configured MachineDeploymentClasses. 160 if selector.MatchResources.MachineDeploymentClass != nil { 161 // MachineDeployment.spec.template.spec.bootstrap.configRef or 162 // MachineDeployment.spec.template.spec.infrastructureRef holds the BootstrapConfigTemplate or 163 // InfrastructureMachineTemplate. 164 if req.HolderReference.Kind == "MachineDeployment" && 165 (req.HolderReference.FieldPath == "spec.template.spec.bootstrap.configRef" || 166 req.HolderReference.FieldPath == "spec.template.spec.infrastructureRef") { 167 // Read the builtin.machineDeployment.class variable. 168 templateMDClassJSON, err := patchvariables.GetVariableValue(templateVariables, "builtin.machineDeployment.class") 169 170 // If the builtin variable could be read. 171 if err == nil { 172 // If templateMDClass matches one of the configured MachineDeploymentClasses. 173 for _, mdClass := range selector.MatchResources.MachineDeploymentClass.Names { 174 // We have to quote mdClass as templateMDClassJSON is a JSON string (e.g. "default-worker"). 175 if mdClass == "*" || string(templateMDClassJSON.Raw) == strconv.Quote(mdClass) { 176 return true 177 } 178 unquoted, _ := strconv.Unquote(string(templateMDClassJSON.Raw)) 179 if strings.HasPrefix(mdClass, "*") && strings.HasSuffix(unquoted, strings.TrimPrefix(mdClass, "*")) { 180 return true 181 } 182 if strings.HasSuffix(mdClass, "*") && strings.HasPrefix(unquoted, strings.TrimSuffix(mdClass, "*")) { 183 return true 184 } 185 } 186 } 187 } 188 } 189 190 // Check if the request is for a BootstrapConfigTemplate or an InfrastructureMachinePoolTemplate 191 // of one of the configured MachinePoolClasses. 192 if selector.MatchResources.MachinePoolClass != nil { 193 if req.HolderReference.Kind == "MachinePool" && 194 (req.HolderReference.FieldPath == "spec.template.spec.bootstrap.configRef" || 195 req.HolderReference.FieldPath == "spec.template.spec.infrastructureRef") { 196 // Read the builtin.machinePool.class variable. 197 templateMPClassJSON, err := patchvariables.GetVariableValue(templateVariables, "builtin.machinePool.class") 198 199 // If the builtin variable could be read. 200 if err == nil { 201 // If templateMPClass matches one of the configured MachinePoolClasses. 202 for _, mpClass := range selector.MatchResources.MachinePoolClass.Names { 203 // We have to quote mpClass as templateMPClassJSON is a JSON string (e.g. "default-worker"). 204 if mpClass == "*" || string(templateMPClassJSON.Raw) == strconv.Quote(mpClass) { 205 return true 206 } 207 unquoted, _ := strconv.Unquote(string(templateMPClassJSON.Raw)) 208 if strings.HasPrefix(mpClass, "*") && strings.HasSuffix(unquoted, strings.TrimPrefix(mpClass, "*")) { 209 return true 210 } 211 if strings.HasSuffix(mpClass, "*") && strings.HasPrefix(unquoted, strings.TrimSuffix(mpClass, "*")) { 212 return true 213 } 214 } 215 } 216 } 217 } 218 219 return false 220 } 221 222 func patchIsEnabled(enabledIf *string, variables map[string]apiextensionsv1.JSON) (bool, error) { 223 // If enabledIf is not set, patch is enabled. 224 if enabledIf == nil { 225 return true, nil 226 } 227 228 // Rendered template. 229 value, err := renderValueTemplate(*enabledIf, variables) 230 if err != nil { 231 return false, errors.Wrapf(err, "failed to calculate value for enabledIf") 232 } 233 234 // Patch is enabled if the rendered template value is `true`. 235 return bytes.Equal(value.Raw, []byte(`true`)), nil 236 } 237 238 // jsonPatchRFC6902 is used to render the generated JSONPatches. 239 type jsonPatchRFC6902 struct { 240 Op string `json:"op"` 241 Path string `json:"path"` 242 Value *apiextensionsv1.JSON `json:"value,omitempty"` 243 } 244 245 // generateJSONPatches generates JSON patches based on the given JSONPatches and variables. 246 func generateJSONPatches(jsonPatches []clusterv1.JSONPatch, variables map[string]apiextensionsv1.JSON) ([]byte, error) { 247 res := []jsonPatchRFC6902{} 248 249 for _, jsonPatch := range jsonPatches { 250 var value *apiextensionsv1.JSON 251 if jsonPatch.Op == "add" || jsonPatch.Op == "replace" { 252 var err error 253 value, err = calculateValue(jsonPatch, variables) 254 if err != nil { 255 return nil, err 256 } 257 } 258 259 res = append(res, jsonPatchRFC6902{ 260 Op: jsonPatch.Op, 261 Path: jsonPatch.Path, 262 Value: value, 263 }) 264 } 265 266 // Render JSON Patches. 267 resJSON, err := json.Marshal(res) 268 if err != nil { 269 return nil, errors.Wrapf(err, "failed to marshal JSON Patch %v", jsonPatches) 270 } 271 272 return resJSON, nil 273 } 274 275 // calculateValue calculates a value for a JSON patch. 276 func calculateValue(patch clusterv1.JSONPatch, variables map[string]apiextensionsv1.JSON) (*apiextensionsv1.JSON, error) { 277 // Return if values are set incorrectly. 278 if patch.Value == nil && patch.ValueFrom == nil { 279 return nil, errors.Errorf("failed to calculate value: neither .value nor .valueFrom are set") 280 } 281 if patch.Value != nil && patch.ValueFrom != nil { 282 return nil, errors.Errorf("failed to calculate value: both .value and .valueFrom are set") 283 } 284 if patch.ValueFrom != nil && patch.ValueFrom.Variable == nil && patch.ValueFrom.Template == nil { 285 return nil, errors.Errorf("failed to calculate value: .valueFrom is set, but neither .valueFrom.variable nor .valueFrom.template are set") 286 } 287 if patch.ValueFrom != nil && patch.ValueFrom.Variable != nil && patch.ValueFrom.Template != nil { 288 return nil, errors.Errorf("failed to calculate value: .valueFrom is set, but both .valueFrom.variable and .valueFrom.template are set") 289 } 290 291 // Return raw value. 292 if patch.Value != nil { 293 return patch.Value, nil 294 } 295 296 // Return variable. 297 if patch.ValueFrom.Variable != nil { 298 value, err := patchvariables.GetVariableValue(variables, *patch.ValueFrom.Variable) 299 if err != nil { 300 return nil, errors.Wrapf(err, "failed to calculate value") 301 } 302 return value, nil 303 } 304 305 // Return rendered value template. 306 value, err := renderValueTemplate(*patch.ValueFrom.Template, variables) 307 if err != nil { 308 return nil, errors.Wrapf(err, "failed to calculate value for template") 309 } 310 return value, nil 311 } 312 313 // renderValueTemplate renders a template with the given variables as data. 314 func renderValueTemplate(valueTemplate string, variables map[string]apiextensionsv1.JSON) (*apiextensionsv1.JSON, error) { 315 // Parse the template. 316 tpl, err := template.New("tpl").Funcs(sprig.HermeticTxtFuncMap()).Parse(valueTemplate) 317 if err != nil { 318 return nil, errors.Wrapf(err, "failed to parse template: %q", valueTemplate) 319 } 320 321 // Convert the flat variables map in a nested map, so that variables can be 322 // consumed in templates like this: `{{ .builtin.cluster.name }}` 323 // NOTE: Variable values are also converted to their Go types as 324 // they cannot be directly consumed as byte arrays. 325 data, err := calculateTemplateData(variables) 326 if err != nil { 327 return nil, errors.Wrap(err, "failed to calculate template data") 328 } 329 330 // Render the template. 331 var buf bytes.Buffer 332 if err := tpl.Execute(&buf, data); err != nil { 333 return nil, errors.Wrapf(err, "failed to render template: %q", valueTemplate) 334 } 335 336 // Unmarshal the rendered template. 337 // NOTE: The YAML library is used for unmarshalling, to be able to handle YAML and JSON. 338 value := apiextensionsv1.JSON{} 339 if err := yaml.Unmarshal(buf.Bytes(), &value); err != nil { 340 return nil, errors.Wrapf(err, "failed to unmarshal rendered template: %q", buf.String()) 341 } 342 343 return &value, nil 344 } 345 346 // calculateTemplateData calculates data for the template, by converting 347 // the variables to their Go types. 348 // Example: 349 // - Input: 350 // map[string]apiextensionsv1.JSON{ 351 // "builtin": {Raw: []byte(`{"cluster":{"name":"cluster-name"}}`}, 352 // "integerVariable": {Raw: []byte("4")}, 353 // "numberVariable": {Raw: []byte("2.5")}, 354 // "booleanVariable": {Raw: []byte("true")}, 355 // } 356 // - Output: 357 // map[string]interface{}{ 358 // "builtin": map[string]interface{}{ 359 // "cluster": map[string]interface{}{ 360 // "name": <string>"cluster-name" 361 // } 362 // }, 363 // "integerVariable": <float64>4, 364 // "numberVariable": <float64>2.5, 365 // "booleanVariable": <bool>true, 366 // } 367 func calculateTemplateData(variables map[string]apiextensionsv1.JSON) (map[string]interface{}, error) { 368 res := make(map[string]interface{}, len(variables)) 369 370 // Marshal the variables into a byte array. 371 tmp, err := json.Marshal(variables) 372 if err != nil { 373 return nil, errors.Wrapf(err, "failed to convert variables: failed to marshal variables") 374 } 375 376 // Unmarshal the byte array back. 377 // NOTE: This converts the "leaf nodes" of the nested map 378 // from apiextensionsv1.JSON to their Go types. 379 if err := json.Unmarshal(tmp, &res); err != nil { 380 return nil, errors.Wrapf(err, "failed to convert variables: failed to unmarshal variables") 381 } 382 383 return res, nil 384 }