sigs.k8s.io/cluster-api@v1.7.1/internal/controllers/topology/cluster/patches/variables/value.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 variables
    18  
    19  import (
    20  	"fmt"
    21  	"strconv"
    22  	"strings"
    23  
    24  	"github.com/pkg/errors"
    25  	"github.com/valyala/fastjson"
    26  	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    27  	"k8s.io/utils/ptr"
    28  )
    29  
    30  const (
    31  	leftArrayDelim  = "["
    32  	rightArrayDelim = "]"
    33  )
    34  
    35  // GetVariableValue returns a variable from the variables map.
    36  func GetVariableValue(variables map[string]apiextensionsv1.JSON, variablePath string) (*apiextensionsv1.JSON, error) {
    37  	// Split the variablePath (format: "<variableName>.<relativePath>").
    38  	variableSplit := strings.Split(variablePath, ".")
    39  	variableName, relativePath := variableSplit[0], variableSplit[1:]
    40  
    41  	// Parse the path segment.
    42  	variableNameSegment, err := parsePathSegment(variableName)
    43  	if err != nil {
    44  		return nil, errors.Wrapf(err, "variable %q is invalid", variablePath)
    45  	}
    46  
    47  	// Get the variable.
    48  	value, ok := variables[variableNameSegment.path]
    49  	if !ok {
    50  		return nil, notFoundError{reason: notFoundReason, message: fmt.Sprintf("variable %q does not exist", variableName)}
    51  	}
    52  
    53  	// Return the value, if variablePath points to a top-level variable, i.e. hos no relativePath and no
    54  	// array index (i.e. "<variableName>").
    55  	if len(relativePath) == 0 && !variableNameSegment.HasIndex() {
    56  		return &value, nil
    57  	}
    58  
    59  	// Parse the variable object.
    60  	variable, err := fastjson.ParseBytes(value.Raw)
    61  	if err != nil {
    62  		return nil, errors.Wrapf(err, "cannot parse variable %q: %s", variableName, string(value.Raw))
    63  	}
    64  
    65  	// If variableName contains an array index, get the array element (i.e. starts with "<variableName>[i]").
    66  	// Then return it, if there is no relative path (i.e. "<variableName>[i]")
    67  	if variableNameSegment.HasIndex() {
    68  		variable, err = getVariableArrayElement(variable, variableNameSegment, variablePath)
    69  		if err != nil {
    70  			return nil, err
    71  		}
    72  
    73  		if len(relativePath) == 0 {
    74  			return &apiextensionsv1.JSON{
    75  				Raw: variable.MarshalTo([]byte{}),
    76  			}, nil
    77  		}
    78  	}
    79  
    80  	// If the variablePath points to a nested variable, i.e. has a relativePath, inspect the variable object.
    81  
    82  	// Retrieve each segment of the relativePath incrementally, taking care of resolving array indexes.
    83  	for _, p := range relativePath {
    84  		// Parse the path segment.
    85  		pathSegment, err := parsePathSegment(p)
    86  		if err != nil {
    87  			return nil, errors.Wrapf(err, "variable %q has invalid syntax", variablePath)
    88  		}
    89  
    90  		// Return if the variable does not exist.
    91  		if !variable.Exists(pathSegment.path) {
    92  			return nil, notFoundError{reason: notFoundReason, message: fmt.Sprintf("variable %q does not exist: failed to lookup segment %q", variablePath, pathSegment.path)}
    93  		}
    94  
    95  		// Get the variable from the variable object.
    96  		variable = variable.Get(pathSegment.path)
    97  
    98  		// Continue if the path doesn't contain an index.
    99  		if !pathSegment.HasIndex() {
   100  			continue
   101  		}
   102  
   103  		variable, err = getVariableArrayElement(variable, pathSegment, variablePath)
   104  		if err != nil {
   105  			return nil, err
   106  		}
   107  	}
   108  
   109  	// Return the marshalled value of the variable.
   110  	return &apiextensionsv1.JSON{
   111  		Raw: variable.MarshalTo([]byte{}),
   112  	}, nil
   113  }
   114  
   115  type pathSegment struct {
   116  	path  string
   117  	index *int
   118  }
   119  
   120  func (p pathSegment) HasIndex() bool {
   121  	return p.index != nil
   122  }
   123  
   124  func parsePathSegment(segment string) (*pathSegment, error) {
   125  	if (strings.Contains(segment, leftArrayDelim) && !strings.Contains(segment, rightArrayDelim)) ||
   126  		(!strings.Contains(segment, leftArrayDelim) && strings.Contains(segment, rightArrayDelim)) {
   127  		return nil, errors.Errorf("failed to parse path segment %q", segment)
   128  	}
   129  
   130  	if !strings.Contains(segment, leftArrayDelim) && !strings.Contains(segment, rightArrayDelim) {
   131  		return &pathSegment{
   132  			path: segment,
   133  		}, nil
   134  	}
   135  
   136  	arrayIndexStr := segment[strings.Index(segment, leftArrayDelim)+1 : strings.Index(segment, rightArrayDelim)]
   137  	index, err := strconv.Atoi(arrayIndexStr)
   138  	if err != nil {
   139  		return nil, errors.Wrapf(err, "failed to parse array index in path segment %q", segment)
   140  	}
   141  	if index < 0 {
   142  		return nil, errors.Errorf("invalid array index %d in path segment %q", index, segment)
   143  	}
   144  
   145  	return &pathSegment{
   146  		path:  segment[:strings.Index(segment, leftArrayDelim)], //nolint:gocritic // We already check above that segment contains leftArrayDelim,
   147  		index: ptr.To(index),
   148  	}, nil
   149  }
   150  
   151  // getVariableArrayElement gets the array element of a given array.
   152  func getVariableArrayElement(array *fastjson.Value, arrayPathSegment *pathSegment, fullVariablePath string) (*fastjson.Value, error) {
   153  	// Retrieve the array element, handling index out of range.
   154  	arr, err := array.Array()
   155  	if err != nil {
   156  		return nil, errors.Wrapf(err, "variable %q is invalid: failed to get array %q", fullVariablePath, arrayPathSegment.path)
   157  	}
   158  
   159  	if len(arr) < *arrayPathSegment.index+1 {
   160  		return nil, errors.Errorf("variable %q is invalid: array does not have index %d", fullVariablePath, arrayPathSegment.index)
   161  	}
   162  
   163  	return arr[*arrayPathSegment.index], nil
   164  }