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