Skip to content

Functions

Overview

Functions makes it easier to run functions in popular programming languages like Go, and Python directly within workflows. Users no longer need to package these functions in containers; the framework takes care of all the necessary packaging and dependencies, simplifying the process.

Users can leverage this feature for various tasks, such as executing actions on infrastructure, integrating with services like Jira and ServiceNow, or sending notifications. By allowing functions to run without needing containers, the feature reduces complexity and makes it easier to implement actions quickly.

The framework is flexible and can be expanded to support more programming languages in the future. It also includes helpful tools for error handling and logging, making it easier to troubleshoot issues. Additionally, users can choose to run functions either synchronously or asynchronously, depending on their needs. Overall, this feature streamlines workflows and improves the user experience by making function execution straightforward and efficient.

Did you know ?

Functions can be managed through various methods:

Functions via UI

To create a Functions Workflow Handler, perform the below steps:

  • Select Workflow Handlers under the Environments and click New Workflow Handler
  • Provide a name for the Workflow Handler. Optionally, enter a description
  • Select the Workflow Handler type Function and click Create

Driver

Workflow Handler General Configuration page appears.

Function Configuration

  • Select Functions and provide a Function Name
  • Enable/disable Skip Build and provide the Image Options

Enable Skip Build

If the images are already available, enable Skip Build to bypass the image creation process and specify the image details.

  • Provide the required image. Images utilizing only the Terraform Workflow Handler are supported
  • To access a private registry and its application image, users need to provide Image Pull Credentials. Enter the Registry encompassing the private registry name, username, and password

Driver

  • Under Advanced, provide the Kube Options:
    • Namespace where the user wants to create the pod
    • Enter the Service Account Name. Using a service account name when creating a function Workflow Handler ensures proper identity, access control, and security practices within an environment
    • Enable/disable the Privileged option to grant the container elevated privileges, allowing it to access host devices and perform low-level operations. By default, it is NotSet
    • Enable/disable the Read-Only Root File System option to prevent any modifications to the file system within the pod. By default, it is NotSet

Driver

  • Optionally, add the below fields to the function workflow handler
Feature Description
Add Resource(s) Specify a required dependency by entering the relevant resource name
Label Add a label to the resource. Labels are key-value pairs used to specify identifying attributes of objects (e.g., key: demo, value: value1)
Node Selector Ensure the container runs on Kubernetes nodes with specific requirements, such as certain hardware or labels
Toleration Allow a pod to be scheduled on nodes with specific taints, ensuring placement based on attributes or restrictions (e.g., GPU toleration for nodes with a gpu taint)
Required Node Affinity Define strict scheduling constraints, ensuring pods are placed only on nodes that match specific label-based conditions. If no matching nodes are found, the pod remains unscheduled
Preferred Node Affinity Specify preferred scheduling rules, allowing pods to be placed on matching nodes when possible. If no preferred matches are found, the scheduler can still place the pod on other nodes
Required Pod Affinity Enforce strict rules for scheduling pods on nodes that already have specific pods based on labels. If no matching nodes are found, the pod remains unscheduled
Preferred Pod Affinity Suggest scheduling pods on nodes with specific pods when possible. If no preferred matches are found, the scheduler can still place the pod elsewhere
Required Pod Anti Affinity Prevent pods from being scheduled on nodes that already have specific pods based on labels. If no suitable nodes are found, the pod remains unscheduled
Preferred Pod Anti Affinity Suggest avoiding scheduling pods on nodes with specific pods when possible. If no alternative nodes are available, the scheduler may still place the pod on a matching node

Driver

Disable Skip Build

If "Skip Build" is not enabled, the user will provide the source code, which should be either in Python or Go. The framework will then use the provided source code to handle the build process, including packaging the necessary dependencies and preparing the function for execution. The system will automatically create an image or package the function based on the code provided, without requiring the user to manage this process manually.

Driver

  • The Target Platform is linux/amd64 by default. This is the platform for which the function has to be built, ensuring compatibility with the required environments

⚠️ Note: Currently, only linux/amd64 is supported as the target platform. Support for additional platforms will be available in the future

  • Add function dependencies required for the function, such as helm.sh/helm/v3 v3.15.3 and k8s.io/cli-runtime v0.30.0 for Go, or boto3==1.34.144 for Python. These packages are typically listed in go.mod for Go and requirements.txt for Python
  • Add system packages that need to be installed for the function's execution, such as git, wget, etc. These ensure the function has the required tools and utilities at the system level

Driver

  • Provide the Advanced (optional) fields that are available, similar to the 'Skip Build' enabled scenario, as they apply when the option is disabled

Container Options (Optional)

Optionally, provide CPU and memory limits. This practice contributes to resource isolation, fair sharing, and overall predictability in resource usage. If not provided, default values will be used

  • CPU Limit (Milli) – Maximum CPU allocation in millicores
  • Memory Limit (MB) – Maximum memory allocation
  • Num Replicas – Number of function instances running concurrently
  • Inactivity Timeout Seconds (Sec) – Time before an idle function instance is stopped
  • Max Concurrency – Maximum concurrent requests allowed for the function

Driver

Configuration (Optional)

  • Specify the duration in the Timeout field to indicate how long the pod needs to run. The Environment Manager/kubernetes terminates the pod upon completing this duration
  • Provide the Success Condition. This ensures that the containerized tasks are considered successful only when the provided conditions are met
  • Select the required config context as the Input

Driver

  • Click Save

These Function Workflow Handlers can be configured in Resource Templates as terraform provider or in any of the hooks.


Go Function Specification

Below is an example specification that defines a function resource named simple-func-go, which executes a 'Go' function within an environment management framework. It includes configuration settings for resource limits, polling behavior, and the function's execution logic, allowing it to process requests and handle various scenarios dynamically.

apiVersion: eaas.envmgmt.io/v1
kind: WorkflowHandler
metadata:
  name: simple-func-go
  project: defaultproject
spec:
  config:
    type: function
    timeoutSeconds: 300
    pollingConfig:
      repeat: "15s"
      until: "1h"
    function:
      cpuLimitMilli: "50"
      memoryLimitMi: "128"
      language: go
      languageVersion: "1.22"
      maxConcurrency: 10
      numReplicas: 1
      source: |
        package function

        import (
          "context"

          sdk "github.com/RafaySystems/function-templates/sdk/go"
        )

        func Handle(ctx context.Context, logger sdk.Logger, req sdk.Request) (sdk.Response, error) {
          logger.Info("received request", "req", req)

          counter := 0.0
          if prev, ok := req["previous"]; ok {
            logger.Info("previous request", "prev", prev)
            counter = prev.(map[string]any)["counter"].(float64)
          }

          resp := make(sdk.Response)
          resp["output"] = "Hello World"
          resp["request"] = req

          if err, ok := req["error"]; ok {
            errString, _ := err.(string)
            switch errString {
            case "execute_again":
              if counter > 1 {
                break
              }
              return nil, sdk.NewErrExecuteAgain(errString, map[string]any{
                "rkey":    "rvalue",
                "counter": counter + 1,
              })
            case "transient":
              return nil, sdk.NewErrTransient(errString)
            default:
              return nil, sdk.NewErrFailed(errString)
            }
          }

          return sdk.Response(resp), nil
        }

Environment Template Integration

The below specification illustrates how the simple-func-go function is integrated into an environment template named demo-envtemp as hooks. These hooks are triggered upon environment initialization, allowing for dynamic execution of functions and enhancing automation capabilities within resource management.

apiVersion: eaas.envmgmt.io/v1
kind: EnvironmentTemplate
metadata:
  name: demo-envtemp
  project: defaultproject
spec:
  version: v1
  resources:
    - type: dynamic
      kind: resourcetemplate
      name: resource1
      resourceOptions:
        version: v1
  hooks:
    onInit:
      - name: func-go
        type: workflowHandler
        onFailure: unspecified
        workflowHandler:
          name: simple-func-go
      - name: func-py
        type: workflowHandler
        options: {}
        onFailure: unspecified
        workflowHandler:
          name: simple-func-py

Note: Functions can be attached as hooks in environment templates, resource templates, or as tasks in the Resource Template - Custom Provider.


Return Errors/Exceptions

Functions executed can return three (3) types of errors/exceptions:

  • Failed: This error type indicates that the function has encountered a critical issue and cannot proceed. When a function returns a Failed error, it transitions to a terminal state, which signifies that no further processing will occur for that invocation
  • Transient: A Transient error signals that the function has encountered a temporary issue that may resolve upon retrying. The function will be retried for the number of times specified in its configuration. This mechanism allows for handling situations where external factors might cause intermittent failures
  • ExecuteAgain: This error type indicates that the function should be executed again based on specific conditions defined in the polling configuration. It allows the function to pause and wait for a certain condition to be met before proceeding to the next step in the workflow

Important

The function can have only two terminal states: Success and Failed

Example 1

Below is an example showcasing how different return errors/exceptions can be handled within a Go function:

package function

import (
    "context"
    sdk "github.com/RafaySystems/function-templates/sdk/go"
)

func Handle(ctx context.Context, logger sdk.Logger, req sdk.Request) (sdk.Response, error) {
    logger.Info("Request received", "request", req)
    // Access inputs and metadata
    i1 := req["input1"]
    meta := req["metadata"]
    prev := req["previous"]

    // Example logic for triggering errors
    if executeAgain {
        // If the function needs to be executed again before proceeding
        return nil, sdk.NewErrExecuteAgain("execute again", map[string]interface{}{"key": "value"})
    }

    if transientError {
        // For retrying the function in case of a transient error (e.g., network issues)
        return nil, sdk.NewErrTransient("transient error")
    }

    if failedError {
        // Mark the function as failed and transition to a terminal state
        return nil, sdk.NewErrFailed("failed")
    }

    // On success, return the desired output
    return sdk.Response{"output1": "value1"}, nil
}

Example 2

Below is an example showcasing how different return errors/exceptions can be handled within a Python function:

from typing import *
from logging import Logger

def handle(logger: Logger,request: Dict[str, Any]) -> Dict[str, Any]:
    logger.info(f"inside function handler, request: {request}", extra={"request": request})
    # inputs can be accessed from (request)
    i1 = req["input1"]
    # metadata of the function can be accessed using "metadata" key
    meta = req["metadata"]
    # data passed using NewErrExecuteAgain can be accessed using "previous" key
    prev = req["previous"]

    # logic
    # ...
    # logic

    if executeAgain:
        # If the function has to be executed again
        # before proceeding to next activity.
        # For eg: to check the status of a third party integration.
        # Any data passed using this error
        # is available for the next execution of the function.
        raise sdk.ExecuteAgainException("Please wait for the ticket to be approved or declined", ticket_id=id, counter=counter+1)

    if transientError:
        # If the function has to be retried.
        # For eg: due to intermittent errors like network errors.
        # Will be only retried for the num of times configured
        raise sdk.TransientException(f"ConnectionError: {str(e)}")

    if failedError:
        # If the function has to marked as failure
        raise sdk.FailedException(f"FailedException: {str(e)}")

    return {
       "output1": "value1"
    }