Skip to content

Action hooks

Action hooks allows you to customize your control plane by injecting custom code at different phase of the lifecycle of your SaaS operations

Action hooks example

x-omnistrate-actionhooks:
      - scope: CLUSTER
        type: INIT
        commandTemplate: >
          PGPASSWORD={{ $var.postgresqlRootPassword }} psql -U postgres
          -h writer {{ $var.postgresqlDatabase }} -c "create extension vector"

As an example, in the above case, we are enabling vector extension for Postgres on cluster initialization.

Example usecases

Action hooks can be used for wide variety of use-cases. Here are some examples:

  • To enable extensions on cluster creation
  • To detect process deadlatches
  • To run rebalance command after adding or removing a node
  • To run post upgrade action

Action hooks concepts

Action hooks scope

Action hooks are categorized into two scopes:

  • Node: for every node of type service component, we will run the associated action hook
  • Cluster: for every service component instance, we will run the associated action hook
Node scope
  • HEALTH_CHECK: Runs periodic health checks on the node to ensure it's functioning correctly. This is mapped to Kubernetes liveness probes and is useful for detecting when a service becomes unresponsive and needs a restart.
    By default, a TCP ping is performed on the service port (if present). You can override this behavior by defining a custom action hook.
    (Optional: Define only if liveness probing is required.)

  • READINESS_CHECK: Determines whether the service is ready to accept traffic. This is mapped to Kubernetes readiness probes. If not explicitly defined, it defaults to HEALTH_CHECK.
    (Optional: Use when readiness conditions differ from general health status.)

  • STARTUP_CHECK: Ensures the service has fully started before it begins accepting traffic. This is mapped to Kubernetes startup probes. If not explicitly defined, it defaults to HEALTH_CHECK.
    (Optional: Useful for applications with long initialization time, for example a large backup restore operation)

  • POST_START: Executes a post-start action on the node after it starts.
    (Optional: Can be used for setup tasks like registering with an external system.)

  • ADD: Triggers an action when a new node is added.
    (Optional: Useful for tasks like joining a cluster or initializing configurations.)

  • REMOVE: Triggers an action when a node is removed.
    (Optional: Useful for cleanup tasks like de-registering from a service registry.)

  • INIT: Runs an initialization action before a node starts.
    (Optional: Can be used for pre-start setup like loading configurations or initializing dependencies.)

  • PROMOTE: Triggers an action when a node is promoted recovered and can be promote to a primary role.
    (Optional: Useful for tasks like promoting a replica to a leader role.)

  • DEMOTE: Triggers an action when a node is deemed unhealthy, before replacing the node.
    (Optional: Useful for tasks like demoting a leader role to a replica and relinquish leadership.)

Cluster scope
  • POST_START: Executes a post-start action after all nodes have started for a given service component.
    (Optional: Can be used for tasks like finalizing configurations, notifying external systems, or running post-deployment scripts.)

  • PRE_START: Runs a pre-start action before all nodes are started for a given service component.
    (Optional: Useful for preparing shared resources, validating configurations, or performing pre-initialization tasks.)

  • POST_STOP: Executes a post-stop action after all nodes are stopped for a given service component.
    (Optional: Can be used for cleanup tasks, releasing resources, or notifying dependent services.)

  • PRE_STOP: Runs a pre-stop action before all nodes are stopped for a given service component.
    (Optional: Useful for gracefully shutting down services, draining connections, or backing up critical data.)

  • POST_UPGRADE: Executes a post-upgrade action after all nodes are upgraded for a given service component.
    (Optional: Can be used for verifying upgrade success, running data migrations, or reloading configurations.)

  • PRE_UPGRADE: Runs a pre-upgrade action before all nodes are upgraded for a given service component.
    (Optional: Useful for backing up data, validating system state, or ensuring compatibility before upgrading.)

  • INIT: Runs an initialization action before all nodes are started for a given service component.
    (Optional: Can be used for configuring dependencies, or preparing the system for operation.)

If you have a requirement to add action hooks at other stages/operations, please reach out to us at support@omnistrate.com

Action hooks runtime environment

Action hooks are run as independent pods to minimize any runtime impact on the other data plane (aka application) resources. Having said that, action hooks can interact with other resources to perform its function.

Action hooks are defined based on when you want to run them, i.e. defining an action on service component doesn’t mean that it will be executed inside that component pod. Instead, it will be run based on the lifecycle of the service component as defined by the action type. In the above example, action hook will be run when cluster resource is first initialized.

Let's say tenant #2 is getting initialized and as you can see action hook is getting run as an independent pod alongside service component pod:

Action hooks environment

Action hooks programming model

We support bash scripts as a runtime environment. Btw, you can also invoke any third-party service/function directly from the action hooks

If you would like to see python based runtime environment or have other suggestion, please reach out to us at support@omnistrate.com with more details on your use case.

Action hooks variables

You can inject any system or dynamic variables into your action hook. For the full list, please see this and this.

Action hooks lifecycle

In this section, we will discuss how different action hooks types are run for different SaaS operations.

To illustrate it, let's say we are building a service with Cluster resource containing two service components: SC1 and SC2. Here is how a dependency structure may look like:

Action hooks example

Now, if you have to define different action hook types on the Cluster resource, here is how they will get executed for each of the SaaS operations:

  • Provisioning operation: As an example, you can see init action hook is run after provisioning of all the service components associated with the Cluster resource. Action hooks provisioning

  • Start/Restart operation Action hooks start

  • Upgrade operation Action hooks upgrade

  • Stop operation Action hooks stop

  • Add capacity operation Action hooks add capacity

  • Remove capacity operation Action hooks remove capacity

  • Deprovisioning operation Action hooks deprovisioning

How to configure action hooks?

Use x-omnistrate-actionhooks compose tag. For more details on compose tags, see this

Alternatively, you can use register action hook API here or follow our intuitive UI to configure your hooks.

More examples

Scaling example

In this example, we are using action hooks to add a node to MongoDB cluster on scale up:

services:
  mongodb-primary:
    image: docker.io/omnistrate/mongodb:6.0-3
    x-omnistrate-actionhooks:
      - scope: NODE
        type: ADD
        commandTemplate: |
          #!/bin/bash
          set -ex

          # Check if NODE_NAME is not equal to 'mongodb-primary-0'
          if [ "$NODE_NAME" != {{ $sys.compute.nodes[0].name }} ]; then
              # Run the mongosh command
              mongosh "mongodb://{{ $var.mongodbUsername }}:{{ $var.mongodbPassword }}@{{ $sys.compute.nodes[0].name }}:27017/?authMechanism=DEFAULT" --eval "rs.add( { host: '{{ $sys.compute.node.name }}' } )"
          fi

Healthcheck example

In this example, we are using action hooks to configure deep process liveness check on Couchbase Server.

#!/bin/sh

IP=${IP:=127.0.0.1}
PORT=${PORT:=8091}
QUERY_PORT=${QUERY_PORT:=8093}

USERNAME=${USERNAME:="Administrator"}
PASSWORD=${PASSWORD:="password"}

N1QL_STMT='SELECT name FROM `travel-sample`.inventory.hotel LIMIT 1'

i=1
numbered_echo() {
    echo "[$i] $@"
    i=$(expr $i + 1)
}


# Check if couchbase server is up
check_db() {
    curl --silent --output /dev/null http://$IP:$PORT
    echo $?
}

SUCCESS_CODE=200
EXIT_CODE=0
response=""
# Issue POST and extract response
launch_query() {
    # final_cmd="curl --silent $QUERY"
    numbered_echo "curl --write-out %{http_code} --output /dev/null --silent -u $USERNAME:$PASSWORD http://$IP:$QUERY_PORT/query/service -d \"statement=$N1QL_STMT\""
    response=$(curl --write-out %{http_code} --output /dev/null --silent -u $USERNAME:$PASSWORD http://$IP:$QUERY_PORT/query/service -d "statement=$N1QL_STMT")
    echo $response
    if [ $response -ne $SUCCESS_CODE ]
    then
        EXIT_CODE=1
        echo "Received: $response Expected: $SUCCESS_CODE => Exiting with exit code: $EXIT_CODE"
        exit $EXIT_CODE
    fi  
}

# Wait until cb db is ready
until [[ $(check_db) = 0 ]]; do
    numbered_echo "cb db not available yet"
    sleep 1
done

numbered_echo "cb db is available"

# LAUNCH QUERY
launch_query 

exit $EXIT_CODE