From 7f95bee23ca6c1baf932aa9a83923f95cb8282fb Mon Sep 17 00:00:00 2001
From: Kumar Mallikarjuna <kumarmallikarjuna1@gmail.com>
Date: Thu, 18 Nov 2021 01:47:17 +0530
Subject: [PATCH] Added time_since() custom JMESPath function (#2680)

* Added time_since() custom JMESPath function

Signed-off-by: Kumar Mallikarjuna <kumarmallikarjuna1@gmail.com>

* Remove time.Layout (not supported in Go 1.16)

Signed-off-by: Kumar Mallikarjuna <kumarmallikarjuna1@gmail.com>

* Modify time_since() for 3 arguments

Signed-off-by: Kumar Mallikarjuna <kumarmallikarjuna1@gmail.com>

* Add tests for functions_test.go

Signed-off-by: Kumar Mallikarjuna <kumarmallikarjuna1@gmail.com>

* Timestamp literals and tabulated tests

Signed-off-by: Kumar Mallikarjuna <kumarmallikarjuna1@gmail.com>

* Remove layout map and default to RFC3339

Signed-off-by: Kumar Mallikarjuna <kumarmallikarjuna1@gmail.com>
---
 pkg/engine/jmespath/functions.go      | 54 +++++++++++++++++++++++++++
 pkg/engine/jmespath/functions_test.go | 31 +++++++++++++++
 2 files changed, 85 insertions(+)

diff --git a/pkg/engine/jmespath/functions.go b/pkg/engine/jmespath/functions.go
index 9799e80c51..746792c45b 100644
--- a/pkg/engine/jmespath/functions.go
+++ b/pkg/engine/jmespath/functions.go
@@ -8,6 +8,7 @@ import (
 	"regexp"
 	"strconv"
 	"strings"
+	"time"
 
 	gojmespath "github.com/jmespath/go-jmespath"
 )
@@ -46,6 +47,7 @@ var (
 	modulo                 = "modulo"
 	base64Decode           = "base64_decode"
 	base64Encode           = "base64_encode"
+	timeSince              = "time_since"
 )
 
 const errorPrefix = "JMESPath function '%s': "
@@ -210,6 +212,15 @@ func getFunctions() []*gojmespath.FunctionEntry {
 			},
 			Handler: jpBase64Encode,
 		},
+		{
+			Name: timeSince,
+			Arguments: []ArgSpec{
+				{Types: []JpType{JpString}},
+				{Types: []JpType{JpString}},
+				{Types: []JpType{JpString}},
+			},
+			Handler: jpTimeSince,
+		},
 	}
 
 }
@@ -550,6 +561,49 @@ func jpBase64Encode(arguments []interface{}) (interface{}, error) {
 	return base64.StdEncoding.EncodeToString([]byte(str.String())), nil
 }
 
+func jpTimeSince(arguments []interface{}) (interface{}, error) {
+	var err error
+	layout, err := validateArg("", arguments, 0, reflect.String)
+	if err != nil {
+		return nil, err
+	}
+
+	ts1, err := validateArg("", arguments, 1, reflect.String)
+	if err != nil {
+		return nil, err
+	}
+
+	ts2, err := validateArg("", arguments, 2, reflect.String)
+	if err != nil {
+		return nil, err
+	}
+
+	var t1, t2 time.Time
+	if layout.String() != "" {
+		t1, err = time.Parse(layout.String(), ts1.String())
+	} else {
+		t1, err = time.Parse(time.RFC3339, ts1.String())
+	}
+	if err != nil {
+		return nil, err
+	}
+
+	t2 = time.Now()
+	if ts2.String() != "" {
+		if layout.String() != "" {
+			t2, err = time.Parse(layout.String(), ts2.String())
+		} else {
+			t2, err = time.Parse(time.RFC3339, ts2.String())
+		}
+
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	return t2.Sub(t1).String(), nil
+}
+
 // InterfaceToString casts an interface to a string type
 func ifaceToString(iface interface{}) (string, error) {
 	switch i := iface.(type) {
diff --git a/pkg/engine/jmespath/functions_test.go b/pkg/engine/jmespath/functions_test.go
index 64d4acb0d3..b6a0d784bc 100644
--- a/pkg/engine/jmespath/functions_test.go
+++ b/pkg/engine/jmespath/functions_test.go
@@ -504,3 +504,34 @@ func Test_Base64Decode_Secret(t *testing.T) {
 	assert.Assert(t, ok)
 	assert.Equal(t, string(result), "Hello, world!")
 }
+
+func Test_TimeSince(t *testing.T) {
+	testCases := []struct {
+		test           string
+		expectedResult string
+	}{
+		{
+			test:           "time_since('', '2021-01-02T15:04:05-07:00', '2021-01-10T03:14:05-07:00')",
+			expectedResult: "180h10m0s",
+		},
+		{
+			test:           "time_since('Mon Jan _2 15:04:05 MST 2006', 'Mon Jan 02 15:04:05 MST 2021', 'Mon Jan 10 03:14:16 MST 2021')",
+			expectedResult: "180h10m11s",
+		},
+	}
+
+	for i, tc := range testCases {
+		t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
+			query, err := New(tc.test)
+			assert.NilError(t, err)
+
+			res, err := query.Search("")
+			assert.NilError(t, err)
+
+			result, ok := res.(string)
+			assert.Assert(t, ok)
+
+			assert.Equal(t, result, tc.expectedResult)
+		})
+	}
+}