Skip to content

Commit

Permalink
Add new Kanister function WaitV2 (#1836)
Browse files Browse the repository at this point in the history
* Switch to go template syntax for Wait conditions (#1635)

* Switch to go template syntax for Wait conditions

Signed-off-by: Prasad Ghangal <prasad.ghangal@gmail.com>

* Update K8ssandra BP as per the new Wait func syntax

Signed-off-by: Prasad Ghangal <prasad.ghangal@gmail.com>

Signed-off-by: Prasad Ghangal <prasad.ghangal@gmail.com>

* Revert "Switch to go template syntax for Wait conditions (#1635)" (#1833)

This reverts commit 2be38c8.

* Add new Kanister function WaitV2

Signed-off-by: Prasad Ghangal <prasad.ghangal@gmail.com>

* Add documentation about new WaitV2 function

Signed-off-by: Prasad Ghangal <prasad.ghangal@gmail.com>

* Correct copyright headers

Signed-off-by: Prasad Ghangal <prasad.ghangal@gmail.com>

* Update docs/functions.rst

Co-authored-by: Pavan Navarathna <6504783+pavannd1@users.noreply.github.com>

* Update docs/functions.rst

Co-authored-by: Pavan Navarathna <6504783+pavannd1@users.noreply.github.com>

* Update docs/functions.rst

Co-authored-by: Pavan Navarathna <6504783+pavannd1@users.noreply.github.com>

Signed-off-by: Prasad Ghangal <prasad.ghangal@gmail.com>
Co-authored-by: Pavan Navarathna <6504783+pavannd1@users.noreply.github.com>
  • Loading branch information
PrasadG193 and pavannd1 committed Jan 10, 2023
1 parent dd1ffab commit 214da0f
Show file tree
Hide file tree
Showing 5 changed files with 445 additions and 17 deletions.
66 changes: 64 additions & 2 deletions docs/functions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1236,8 +1236,70 @@ Example:
namespace: "{{ .Phases.createDeploy.Output.namespace }}"
Wait
----
WaitV2
------

This function is used to wait on a Kubernetes resource
until a desired state is reached. The wait condition is defined
in a Go template syntax.

Arguments:

.. csv-table::
:header: "Argument", "Required", "Type", "Description"
:align: left
:widths: 5,5,5,15

`timeout`, Yes, `string`, wait timeout
`conditions`, Yes, `map[string]interface{}`, keys should be ``allOf`` and/or ``anyOf`` with value as ``[]Condition``

``Condition`` struct:

.. code-block:: yaml
condition: "Go template condition that returns true or false"
objectReference:
apiVersion: "Kubernetes resource API version"
resource: "Type of resource to wait for"
name: "Name of the resource"
The Go template conditions can be validated using kubectl commands with
``-o go-template`` flag.
E.g. To check if the Deployment is ready, the following Go template syntax
can be used with kubectl command


.. code-block:: bash
kubectl get deploy -n $NAMESPACE $DEPLOY_NAME \
-o go-template='{{ $available := false }}{{ range $condition := $.status.conditions }}{{ if and (eq .type "Available") (eq .status "True") }}{{ $available = true }}{{ end }}{{ end }}{{ $available }}'
The same Go template can be used as a condition in the WaitV2 function.

Example:

.. code-block:: yaml
:linenos:
- func: WaitV2
name: waitForDeploymentReady
args:
timeout: 5m
conditions:
anyOf:
- condition: '{{ $available := false }}{{ range $condition := $.status.conditions }}{{ if and (eq .type "Available") (eq .status "True") }}{{ $available = true }}{{ end }}{{ end }}{{ $available }}'
objectReference:
apiVersion: "v1"
group: "apps"
name: "{{ .Object.metadata.name }}"
namespace: "{{ .Object.metadata.namespace }}"
resource: "deployments"
Wait (deprecated)
-----------------

This function is used to wait on a Kubernetes resource
until a desired state is reached.
Expand Down
40 changes: 27 additions & 13 deletions pkg/function/wait.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ func (ktf *waitFunc) Exec(ctx context.Context, tp param.TemplateParams, args map
if err != nil {
return nil, errors.Wrap(err, "Failed to parse timeout")
}
err = waitForCondition(ctx, dynCli, conditions, timeoutDur, tp)
err = waitForCondition(ctx, dynCli, conditions, timeoutDur, tp, evaluateWaitCondition)
return nil, err
}

Expand All @@ -113,16 +113,23 @@ func (*waitFunc) Arguments() []string {
}

// waitForCondition wait till the condition satisfies within the timeout duration
func waitForCondition(ctx context.Context, dynCli dynamic.Interface, waitCond WaitConditions, timeout time.Duration, tp param.TemplateParams) error {
func waitForCondition(
ctx context.Context,
dynCli dynamic.Interface,
waitCond WaitConditions,
timeout time.Duration,
tp param.TemplateParams,
eval func(context.Context, dynamic.Interface, Condition, param.TemplateParams) (bool, error),
) error {
ctxTimeout, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
var evalErr error
result := false
err := poll.Wait(ctxTimeout, func(ctx context.Context) (bool, error) {
for _, cond := range waitCond.AnyOf {
result, evalErr = evaluateCondition(ctx, dynCli, cond, tp)
result, evalErr = eval(ctx, dynCli, cond, tp)
if evalErr != nil {
// TODO: Fail early if the error is due to jsonpath syntax
// TODO: Fail early if the error is due to invalid syntax
log.Debug().WithError(evalErr).Print("Failed to evaluate the condition", field.M{"result": result})
return false, nil
}
Expand All @@ -131,9 +138,9 @@ func waitForCondition(ctx context.Context, dynCli dynamic.Interface, waitCond Wa
}
}
for _, cond := range waitCond.AllOf {
result, evalErr = evaluateCondition(ctx, dynCli, cond, tp)
result, evalErr = eval(ctx, dynCli, cond, tp)
if evalErr != nil {
// TODO: Fail early if the error is due to jsonpath syntax
// TODO: Fail early if the error is due to invalid syntax
log.Debug().WithError(evalErr).Print("Failed to evaluate the condition", field.M{"result": result})
return false, nil
}
Expand All @@ -151,16 +158,12 @@ func waitForCondition(ctx context.Context, dynCli dynamic.Interface, waitCond Wa
return err
}

// evaluateCondition evaluate the go template condition
func evaluateCondition(ctx context.Context, dynCli dynamic.Interface, cond Condition, tp param.TemplateParams) (bool, error) {
objRefRaw := map[string]crv1alpha1.ObjectReference{
"objRef": cond.ObjectReference,
}
rendered, err := param.RenderObjectRefs(objRefRaw, tp)
// evaluateWaitCondition evaluate the go template condition
func evaluateWaitCondition(ctx context.Context, dynCli dynamic.Interface, cond Condition, tp param.TemplateParams) (bool, error) {
objRef, err := resolveWaitConditionObjRefs(cond, tp)
if err != nil {
return false, err
}
objRef := rendered["objRef"]

obj, err := fetchObjectFromRef(ctx, dynCli, objRef)
if err != nil {
Expand All @@ -183,6 +186,17 @@ func evaluateCondition(ctx context.Context, dynCli dynamic.Interface, cond Condi
return strings.TrimSpace(buf.String()) == "true", nil
}

func resolveWaitConditionObjRefs(cond Condition, tp param.TemplateParams) (crv1alpha1.ObjectReference, error) {
objRefRaw := map[string]crv1alpha1.ObjectReference{
"objRef": cond.ObjectReference,
}
rendered, err := param.RenderObjectRefs(objRefRaw, tp)
if err != nil {
return crv1alpha1.ObjectReference{}, err
}
return rendered["objRef"], nil
}

func fetchObjectFromRef(ctx context.Context, dynCli dynamic.Interface, objRef crv1alpha1.ObjectReference) (runtime.Object, error) {
gvr := schema.GroupVersionResource{Group: objRef.Group, Version: objRef.APIVersion, Resource: objRef.Resource}
if objRef.Namespace != "" {
Expand Down
117 changes: 117 additions & 0 deletions pkg/function/waitv2.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// Copyright 2023 The Kanister Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package function

import (
"bytes"
"context"
"strings"
"time"

"github.com/pkg/errors"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/cli-runtime/pkg/printers"
"k8s.io/client-go/dynamic"

kanister "github.com/kanisterio/kanister/pkg"
"github.com/kanisterio/kanister/pkg/kube"
"github.com/kanisterio/kanister/pkg/param"
)

const (
// WaitV2FuncName specifies the function name
WaitV2FuncName = "WaitV2"
WaitV2TimeoutArg = "timeout"
WaitV2ConditionsArg = "conditions"
)

func init() {
_ = kanister.Register(&waitV2Func{})
}

var _ kanister.Func = (*waitV2Func)(nil)

type waitV2Func struct{}

func (*waitV2Func) Name() string {
return WaitV2FuncName
}

func (ktf *waitV2Func) Exec(ctx context.Context, tp param.TemplateParams, args map[string]interface{}) (map[string]interface{}, error) {
var timeout string
if err := Arg(args, WaitV2TimeoutArg, &timeout); err != nil {
return nil, err
}

// get the 'conditions' from the unrendered arguments list.
// they will be evaluated in the 'evaluateCondition()` function.
var conditions WaitConditions
if err := Arg(args, WaitV2ConditionsArg, &conditions); err != nil {
return nil, err
}

dynCli, err := kube.NewDynamicClient()
if err != nil {
return nil, err
}
timeoutDur, err := time.ParseDuration(timeout)
if err != nil {
return nil, errors.Wrap(err, "Failed to parse timeout")
}
err = waitForCondition(ctx, dynCli, conditions, timeoutDur, tp, evaluateWaitV2Condition)
return nil, err
}

func (*waitV2Func) RequiredArgs() []string {
return []string{
WaitV2TimeoutArg,
WaitV2ConditionsArg,
}
}

func (*waitV2Func) Arguments() []string {
return []string{
WaitV2TimeoutArg,
WaitV2ConditionsArg,
}
}

// evaluateWaitV2Condition evaluate the go template condition
func evaluateWaitV2Condition(ctx context.Context, dynCli dynamic.Interface, cond Condition, tp param.TemplateParams) (bool, error) {
objRef, err := resolveWaitConditionObjRefs(cond, tp)
if err != nil {
return false, err
}

obj, err := fetchObjectFromRef(ctx, dynCli, objRef)
if err != nil {
return false, err
}
value, err := evaluateGoTemplate(obj, cond.Condition)
if err != nil {
return false, err
}
return strings.TrimSpace(value) == "true", nil
}

func evaluateGoTemplate(obj runtime.Object, goTemplateStr string) (string, error) {
var buff bytes.Buffer
jp, err := printers.NewGoTemplatePrinter([]byte(goTemplateStr))
if err != nil {
return "", nil
}
err = jp.PrintObj(obj, &buff)
return buff.String(), err
}
Loading

0 comments on commit 214da0f

Please sign in to comment.