github.com/argoproj/argo-events@v1.9.1/sensors/triggers/params.go (about)

     1  /*
     2  Copyright 2018 BlackRock, Inc.
     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 triggers
    18  
    19  import (
    20  	"bytes"
    21  	"encoding/json"
    22  	"fmt"
    23  	"text/template"
    24  
    25  	sprig "github.com/Masterminds/sprig/v3"
    26  	"github.com/ghodss/yaml"
    27  	"github.com/tidwall/gjson"
    28  	"github.com/tidwall/sjson"
    29  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    30  
    31  	"github.com/argoproj/argo-events/common"
    32  	"github.com/argoproj/argo-events/pkg/apis/sensor/v1alpha1"
    33  )
    34  
    35  const (
    36  	jsonType   = "JSON"
    37  	stringType = "String"
    38  )
    39  
    40  // ConstructPayload constructs a payload for operations involving request and responses like HTTP request.
    41  func ConstructPayload(events map[string]*v1alpha1.Event, parameters []v1alpha1.TriggerParameter) ([]byte, error) {
    42  	var payload []byte
    43  
    44  	for _, parameter := range parameters {
    45  		value, typ, err := ResolveParamValue(parameter.Src, events)
    46  		if err != nil {
    47  			return nil, err
    48  		}
    49  		if typ != stringType && parameter.Src.UseRawData {
    50  			tmp, err := sjson.SetRawBytes(payload, parameter.Dest, []byte(*value))
    51  			if err != nil {
    52  				return nil, err
    53  			}
    54  			payload = tmp
    55  		} else {
    56  			tmp, err := sjson.SetBytes(payload, parameter.Dest, *value)
    57  			if err != nil {
    58  				return nil, err
    59  			}
    60  			payload = tmp
    61  		}
    62  	}
    63  	return payload, nil
    64  }
    65  
    66  // ApplyTemplateParameters applies parameters to trigger template
    67  func ApplyTemplateParameters(events map[string]*v1alpha1.Event, trigger *v1alpha1.Trigger) error {
    68  	if trigger.Parameters != nil && len(trigger.Parameters) > 0 {
    69  		templateBytes, err := json.Marshal(trigger.Template)
    70  		if err != nil {
    71  			return err
    72  		}
    73  		tObj, err := ApplyParams(templateBytes, trigger.Parameters, events)
    74  		if err != nil {
    75  			return err
    76  		}
    77  		tmplt := &v1alpha1.TriggerTemplate{}
    78  		if err = json.Unmarshal(tObj, tmplt); err != nil {
    79  			return err
    80  		}
    81  		trigger.Template = tmplt
    82  	}
    83  	return nil
    84  }
    85  
    86  // ApplyResourceParameters applies parameters to K8s resource within trigger
    87  func ApplyResourceParameters(events map[string]*v1alpha1.Event, parameters []v1alpha1.TriggerParameter, obj *unstructured.Unstructured) error {
    88  	if parameters != nil {
    89  		jObj, err := obj.MarshalJSON()
    90  		if err != nil {
    91  			return err
    92  		}
    93  		jUpdatedObj, err := ApplyParams(jObj, parameters, events)
    94  		if err != nil {
    95  			return err
    96  		}
    97  		err = obj.UnmarshalJSON(jUpdatedObj)
    98  		if err != nil {
    99  			return err
   100  		}
   101  	}
   102  	return nil
   103  }
   104  
   105  // ApplyParams applies the params to the resource json object
   106  func ApplyParams(jsonObj []byte, params []v1alpha1.TriggerParameter, events map[string]*v1alpha1.Event) ([]byte, error) {
   107  	for _, param := range params {
   108  		// let's grab the param value
   109  		value, typ, err := ResolveParamValue(param.Src, events)
   110  		if err != nil {
   111  			return nil, err
   112  		}
   113  		if value == nil {
   114  			continue
   115  		}
   116  
   117  		switch op := param.Operation; op {
   118  		case v1alpha1.TriggerParameterOpAppend, v1alpha1.TriggerParameterOpPrepend:
   119  			// prepend or append the current value
   120  			current := gjson.GetBytes(jsonObj, param.Dest)
   121  
   122  			if current.Exists() {
   123  				typ = stringType
   124  				if op == v1alpha1.TriggerParameterOpAppend {
   125  					*value = current.String() + *value
   126  				} else {
   127  					*value += current.String()
   128  				}
   129  			}
   130  		case v1alpha1.TriggerParameterOpOverwrite, v1alpha1.TriggerParameterOpNone:
   131  			// simply overwrite the current value with the new one
   132  		default:
   133  			return nil, fmt.Errorf("unsupported trigger parameter operation: %+v", op)
   134  		}
   135  		// now let's set the value
   136  		if typ != stringType && param.Src.UseRawData {
   137  			tmp, err := sjson.SetRawBytes(jsonObj, param.Dest, []byte(*value))
   138  			if err != nil {
   139  				return nil, err
   140  			}
   141  			jsonObj = tmp
   142  		} else {
   143  			tmp, err := sjson.SetBytes(jsonObj, param.Dest, *value)
   144  			if err != nil {
   145  				return nil, err
   146  			}
   147  			jsonObj = tmp
   148  		}
   149  	}
   150  	return jsonObj, nil
   151  }
   152  
   153  func isJSON(b []byte) bool {
   154  	var js json.RawMessage
   155  	return json.Unmarshal(b, &js) == nil
   156  }
   157  
   158  // util method to render an event's data as a JSON []byte
   159  // json is a subset of yaml so this should work...
   160  func renderEventDataAsJSON(event *v1alpha1.Event) ([]byte, error) {
   161  	if event == nil {
   162  		return nil, fmt.Errorf("event is nil")
   163  	}
   164  	raw := event.Data
   165  	switch event.Context.DataContentType {
   166  	case common.MediaTypeJSON:
   167  		if isJSON(raw) {
   168  			return raw, nil
   169  		}
   170  		return nil, fmt.Errorf("event data is not valid JSON")
   171  	case common.MediaTypeYAML:
   172  		data, err := yaml.YAMLToJSON(raw)
   173  		if err != nil {
   174  			return nil, fmt.Errorf("failed converting yaml event data to JSON: %s", err)
   175  		}
   176  		return data, nil
   177  	default:
   178  		return nil, fmt.Errorf("unsupported event content type: %s", event.Context.DataContentType)
   179  	}
   180  }
   181  
   182  // helper method to resolve the parameter's value from the src
   183  // returns value and value type (jsonType or stringType or empty string if not found). jsonType represent a block while stringType represent a single value.
   184  // returns an error if the Path is invalid/not found and the default value is nil OR if the eventDependency event doesn't exist and default value is nil
   185  func ResolveParamValue(src *v1alpha1.TriggerParameterSource, events map[string]*v1alpha1.Event) (*string, string, error) {
   186  	var err error
   187  	var eventPayload []byte
   188  	var key string
   189  	var tmplt string
   190  	var resultValue string
   191  
   192  	event, eventExists := events[src.DependencyName]
   193  	switch {
   194  	case eventExists:
   195  		// If no data or context selection was provided
   196  		if src.ContextKey == "" && src.DataKey == "" && src.DataTemplate == "" && src.ContextTemplate == "" {
   197  			// Return default value if exists
   198  			if src.Value != nil {
   199  				resultValue = *src.Value
   200  			} else {
   201  				// Default value doesn't exist so return the whole event payload
   202  				eventPayload, err = json.Marshal(&event)
   203  				resultValue = string(eventPayload)
   204  			}
   205  
   206  			if err == nil {
   207  				return &resultValue, stringType, nil
   208  			}
   209  		}
   210  
   211  		// Get the context part of the payload
   212  		if src.ContextKey != "" || src.ContextTemplate != "" {
   213  			key = src.ContextKey
   214  			tmplt = src.ContextTemplate
   215  			eventPayload, err = json.Marshal(&event.Context)
   216  		}
   217  
   218  		// Get the data part of the payload
   219  		if src.DataKey != "" || src.DataTemplate != "" {
   220  			key = src.DataKey
   221  			tmplt = src.DataTemplate
   222  			eventPayload, err = renderEventDataAsJSON(event)
   223  		}
   224  	case src.Value != nil:
   225  		// Use the default value set by the user in case the event is missing
   226  		resultValue = *src.Value
   227  		return &resultValue, stringType, nil
   228  	default:
   229  		// The parameter doesn't have a default value and is referencing a dependency that is
   230  		// missing in the received events. This is not an error and may happen with || conditions.
   231  		return nil, stringType, nil
   232  	}
   233  	// If the event payload parsing failed
   234  	if err != nil {
   235  		// Fall back to the default value in case it exists
   236  		if src.Value != nil {
   237  			fmt.Printf("failed to parse the event payload, using default value. err: %+v\n", err)
   238  			resultValue = *src.Value
   239  			return &resultValue, stringType, nil
   240  		}
   241  		// Otherwise, return the error
   242  		return nil, "", err
   243  	}
   244  	// Get the value corresponding to specified key or template within event payload
   245  	if eventPayload != nil {
   246  		if tmplt != "" {
   247  			resultValue, err = getValueWithTemplate(eventPayload, tmplt)
   248  			if err == nil {
   249  				return &resultValue, stringType, nil
   250  			}
   251  			fmt.Printf("failed to execute the src event template, falling back to key or value. err: %+v\n", err)
   252  		}
   253  		if key != "" {
   254  			tmp, typ, err := getValueByKey(eventPayload, key)
   255  			// For block injection support
   256  			resultValue = tmp
   257  			if err == nil {
   258  				return &resultValue, typ, nil
   259  			}
   260  			fmt.Printf("failed to get value by key: %+v\n", err)
   261  		}
   262  		// In case neither key nor template resolving was successful, fall back to the default value if exists
   263  		if src.Value != nil {
   264  			resultValue = *src.Value
   265  			return &resultValue, stringType, nil
   266  		}
   267  	}
   268  
   269  	// if we got here it means that both key and template did not match the event payload
   270  	// and no default value was provided, so we need to return an error
   271  	return nil, "", fmt.Errorf("unable to resolve '%s' parameter value. err: %+v", src.DependencyName, err)
   272  }
   273  
   274  // getValueWithTemplate will attempt to execute the provided template against
   275  // the raw json bytes and then returns the result or any error
   276  func getValueWithTemplate(value []byte, templString string) (string, error) {
   277  	res := gjson.ParseBytes(value)
   278  	tpl, err := template.New("param").Funcs(sprig.FuncMap()).Parse(templString)
   279  	if err != nil {
   280  		return "", err
   281  	}
   282  	var buf bytes.Buffer
   283  	if err := tpl.Execute(&buf, map[string]interface{}{
   284  		"Input": res.Value(),
   285  	}); err != nil {
   286  		return "", err
   287  	}
   288  	out := buf.String()
   289  	if out == "" || out == "<no value>" {
   290  		return "", fmt.Errorf("template evaluated to empty string or no value: %s", templString)
   291  	}
   292  	return out, nil
   293  }
   294  
   295  // getValueByKey will return the value as raw json or a string and value's type at the provided key,
   296  // Value type (jsonType or stringType or empty string). JSON represent a block while String represent a single value.
   297  // or an error if it does not exist.
   298  func getValueByKey(value []byte, key string) (string, string, error) {
   299  	res := gjson.GetBytes(value, key)
   300  	if res.Exists() {
   301  		if res.Type.String() == jsonType {
   302  			return res.Raw, res.Type.String(), nil
   303  		}
   304  		return res.String(), res.Type.String(), nil
   305  	}
   306  	return "", "", fmt.Errorf("key %s does not exist to in the event payload", key)
   307  }