Repository: kyverno/kyverno (go)

Issue:
[Enhancement] Update add() filter to add duration and absolute time
### Problem Statement

There is currently no way to take an absolute time in RRC3339 format and add some duration to it outputting the new absolute time.

### Solution Description

Enhance the `add()` filter to support inputs in the form of RFC3339 + scalar (duration) with the sum being RFC3339.

### Alternatives

_No response_

### Additional Context

_No response_

### Slack discussion

_No response_

### Research

- [X] I have read and followed the documentation AND the [troubleshooting guide](https://kyverno.io/docs/troubleshooting/).
- [X] I have searched other issues in this repository and mine is not recorded.

Pull request description:
 Added a time_add() filter to add duration and absolute time
Signed-off-by: Vishal Choudhary <contactvishaltech@gmail.com>

## Explanation

<!--
In a couple sentences, explain why this PR is needed and what it addresses. This should be an explanation a non-developer user can understand and covers the "why" question. It should also clearly indicate whether this PR represents an addition, a change, or a fix of existing behavior. This explanation will be used to assist in the release note drafting process.

THIS IS MANDATORY.
-->
Adds a custom JMESPath filter to support inputs in the form of RFC3339 + scalar (duration) with the sum being RFC3339.

## Related issue
Closes #5815
<!--
Please link the GitHub issue this pull request resolves in the format of `Closes #1234`. If you discussed this change
with a maintainer, please mention her/him using the `@` syntax (e.g. `@JimBugwadia`).

If this change neither resolves an existing issue nor has sign-off from one of the maintainers, there is a
chance substantial changes will be requested or that the changes will be rejected.

You can discuss changes with maintainers in the [Kyverno Slack Channel](https://kubernetes.slack.com/).
-->

## Milestone of this PR
<!--

Add the milestone label by commenting `/milestone 1.2.3`.

-->

## What type of PR is this

<!--

> Uncomment only one ` /kind <>` line, hit enter to put that in a new line, and remove leading white spaces from that line:
>
> /kind api-change
> /kind bug
> /kind cleanup
> /kind design
> /kind documentation
> /kind failing-test
> /kind feature
-->
/kind feature

## Proposed Changes

<!--
Describe the big picture of your changes here to communicate to the maintainers why we should accept this pull request. 

***NOTE***: If this PR results in new or altered behavior which is user facing, you **MUST** read and follow the steps outlined in the [PR documentation guide](pr_documentation.md) and add Proof Manifests as defined below.
-->

### Proof Manifests

<!--
Read and follow the [PR documentation guide](https://github.com/kyverno/kyverno/blob/main/.github/pr_documentation.md) for more details first. This section is for pasting your YAML manifests (Kubernetes resources and Kyverno policies) and Kyverno CLI test manifests which allow maintainers to prove the intended functionality is achieved by your PR. Please use proper fenced code block formatting, for example:

# Kubernetes resource

```yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: roles-dictionary
  namespace: default
data:
  allowed-roles: "[\"cluster-admin\", \"cluster-operator\", \"tenant-admin\"]"
```

# Kyverno CLI test manifest (please see docs for latest manifest format at https://kyverno.io/docs/kyverno-cli/). See kyverno/policies for complete examples of all related test files.

```yaml
name: prepend-image-registry
policies:
  - prepend_image_registry.yaml
resources:
  - resource.yaml
variables: values.yaml
results:
  - policy: prepend-registry
    rule: prepend-registry-containers
    resource: mypod
    # if mutate rule
    patchedResource: patchedResource01.yaml
    kind: Pod
    result: pass
```
-->

## Checklist

<!--
Put an `x` in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of
them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code.
-->

- [x] I have read the [contributing guidelines](https://github.com/kyverno/kyverno/blob/main/CONTRIBUTING.md).
- [x] I have read the [PR documentation guide](https://github.com/kyverno/kyverno/blob/main/.github/pr_documentation.md) and followed the process including adding proof manifests to this PR.
- [ ] This is a bug fix and I have added unit tests that prove my fix is effective.
- [ ] This is a feature and I have added CLI tests that are applicable.
- [ ] My PR needs to be cherry picked to a specific release branch which is <replace>.
- [ ] My PR contains new or altered behavior to Kyverno and
  - [ ] CLI support should be added and my PR doesn't contain that functionality.
  - [ ] I have added or changed [the documentation](https://github.com/kyverno/website) myself in an existing PR and the link is:
  <!-- Uncomment to link to the PR -->
  <!-- https://github.com/kyverno/website/pull/123 -->
  - [ ] I have raised an issue in [kyverno/website](https://github.com/kyverno/website) to track the documentation update and the link is:
  <!-- Uncomment to link to the issue -->
  <!-- https://github.com/kyverno/website/issues/1 -->

## Further Comments

<!--
If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution
you did and what alternatives you considered, etc...
-->


Patch:
diff --git a/pkg/engine/jmespath/functions.go b/pkg/engine/jmespath/functions.go
index f513aaf76..91ae20508 100644
--- a/pkg/engine/jmespath/functions.go
+++ b/pkg/engine/jmespath/functions.go
@@ -81,6 +81,8 @@ var (
 	objectFromLists        = "object_from_lists"
 	random                 = "random"
 	x509_decode            = "x509_decode"
+	timeAdd                = "time_add"
+	timeParse              = "time_parse"
 	timeToCron             = "time_to_cron"
 )
 
@@ -502,6 +504,31 @@ func GetFunctions() []*FunctionEntry {
 			ReturnType: []JpType{JpString},
 			Note:       "converts an absolute time (RFC 3339) to a cron expression (string).",
 		},
+		{
+			Entry: &gojmespath.FunctionEntry{
+				Name: timeAdd,
+				Arguments: []ArgSpec{
+					{Types: []JpType{JpString}},
+					{Types: []JpType{JpString}},
+					{Types: []JpType{JpString}},
+				},
+				Handler: jpTimeAdd,
+			},
+			ReturnType: []JpType{JpString},
+			Note:       "adds duration (third string) to a time value (second string) of a specified layout (first string)",
+		},
+		{
+			Entry: &gojmespath.FunctionEntry{
+				Name: timeParse,
+				Arguments: []ArgSpec{
+					{Types: []JpType{JpString}},
+					{Types: []JpType{JpString}},
+				},
+				Handler: jpTimeParse,
+			},
+			ReturnType: []JpType{JpString},
+			Note:       "changes a time value of a given layout to RFC 3339",
+		},
 	}
 }
 
@@ -1131,3 +1158,60 @@ func jpTimeToCron(arguments []interface{}) (interface{}, error) {
 
 	return cron, nil
 }
+
+func jpTimeAdd(arguments []interface{}) (interface{}, error) {
+	var err error
+	layout, err := validateArg("", arguments, 0, reflect.String)
+	if err != nil {
+		return nil, err
+	}
+
+	ts, err := validateArg("", arguments, 1, reflect.String)
+	if err != nil {
+		return nil, err
+	}
+
+	dr, err := validateArg("", arguments, 2, reflect.String)
+	if err != nil {
+		return nil, err
+	}
+
+	var t time.Time
+	if layout.String() != "" {
+		t, err = time.Parse(layout.String(), ts.String())
+	} else {
+		t, err = time.Parse(time.RFC3339, ts.String())
+	}
+	if err != nil {
+		return nil, err
+	}
+
+	var d time.Duration
+	d, err = time.ParseDuration(dr.String())
+	if err != nil {
+		return nil, err
+	}
+
+	return t.Add(d).Format(time.RFC3339), nil
+}
+
+func jpTimeParse(arguments []interface{}) (interface{}, error) {
+	var err error
+	layout, err := validateArg("", arguments, 0, reflect.String)
+	if err != nil {
+		return nil, err
+	}
+
+	ts, err := validateArg("", arguments, 1, reflect.String)
+	if err != nil {
+		return nil, err
+	}
+
+	var t time.Time
+	t, err = time.Parse(layout.String(), ts.String())
+	if err != nil {
+		return nil, err
+	}
+
+	return t.Format(time.RFC3339), nil
+}


Test patch:
diff --git a/pkg/engine/jmespath/functions_test.go b/pkg/engine/jmespath/functions_test.go
index cfbd73687..a20b3c536 100644
--- a/pkg/engine/jmespath/functions_test.go
+++ b/pkg/engine/jmespath/functions_test.go
@@ -1579,3 +1579,63 @@ func Test_TimeToCron(t *testing.T) {
 		})
 	}
 }
+
+func Test_TimeAdd(t *testing.T) {
+	testCases := []struct {
+		test           string
+		expectedResult string
+	}{
+		{
+			test:           "time_add('', '2021-01-02T15:04:05-07:00', '3h')",
+			expectedResult: "2021-01-02T18:04:05-07:00",
+		},
+		{
+			test:           "time_add('Mon Jan 02 15:04:05 MST 2006', 'Sat Jan 02 15:04:05 MST 2021', '5h30m40s')",
+			expectedResult: "2021-01-02T20:34:45Z",
+		},
+	}
+	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)
+		})
+	}
+}
+
+func Test_TimeParse(t *testing.T) {
+	testCases := []struct {
+		test           string
+		expectedResult string
+	}{
+		{
+			test:           "time_parse('2006-01-02T15:04:05Z07:00', '2021-01-02T15:04:05-07:00')",
+			expectedResult: "2021-01-02T15:04:05-07:00",
+		},
+		{
+			test:           "time_parse('Mon Jan 02 15:04:05 MST 2006', 'Sat Jan 02 15:04:05 MST 2021')",
+			expectedResult: "2021-01-02T15:04:05Z",
+		},
+	}
+	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)
+		})
+	}
+}