Skip to main content

Workflow

Workflows are used to implement multi-step processes. This could be something as simple as automating a deployment or something as complex as automating entire internal developer platforms, cloud environments, or infrastructure control planes. Workflows, in combination with Functions, provide a means for programming or orchestrating Kubernetes controllers. This could be Kubernetes "built-in" controllers, such as Deployment or Job controllers, "off-the-shelf" controllers, such as GCP's Config Connector or AWS's ACK, or custom controllers you've built yourself. Workflows enable you to build automations around these controllers or compose them into cohesive platforms.

Workflows specify which Functions should be run, how their outputs map into inputs, and manages their execution. In essence, Workflows themselves define a controller. That is, Workflow is a control-loop-driven workflow orchestrator. For this reason, we sometimes refer to Koreo as a "meta-controller programming language" because Workflows and Functions provide controller-based primitives for managing other controllers.

In general, Workflow definitions are simple. They specify the resource type that will cause the Workflow to run, i.e. the "trigger", perform a set of steps, and optionally surface conditions or state. Think of a Workflow as a specification which is instantiated with configuration. Once instantiated, an instance of the Workflow will run according to its configuration. Many instances of a Workflow may exist and run concurrently. Many Workflows may be defined within one system, and Workflows themselves may be composed.

A Workflow is responsible for running Logic, which is a ValueFunction, ResourceFunction, or another Workflow. Logic should be thought of as defining the body of a loop. The Workflow schedules iterations of that loop and manages the "external" (to that Logic's body) state interactions.

Running a Workflow

A Workflow may be externally triggered to run, and have its configuration provided by a resource specified using crdRef. This resource serves to provide the Workflow's configuration and the Workflow instance may optionally report its conditions and state within this resource's status block. The Workflow also writes information about the resources it manages to an annotation on this resource.

apiVersion: koreo.dev/v1beta1
kind: Workflow
metadata:
name: simple-example.v1
namespace: koreo-demo
spec:
crdRef:
apiGroup: demo.koreo.dev
version: v1beta1
kind: TriggerDummy
# ...

We refer to the instance of the crdRef resource which triggers a Workflow as its "parent" or "trigger".

warning

Any resource kind can be used as a Workflow trigger, but take care when using resources controlled by another controller. Koreo applies updates to the resource's status and annotations which could result in a dangerous interaction for resources with specific semantics used by another controller. For this reason, it's encouraged to create your own CRDs unless you understand what you are doing. Koreo Tooling provides a tool to generate a CRD from a Workflow.

Additionally, a Workflow can be triggered by another Workflow as a sub-Workflow. This is done by specifying a ref with kind: Workflow on a step in the parent Workflow.

apiVersion: koreo.dev/v1beta1
kind: Workflow
metadata:
name: simple-example.v1
namespace: koreo-demo
spec:
steps:
- label: nested_workflow
ref:
kind: Workflow
name: nested-workflow.v1
inputs:
string: =steps.config.string
int: =steps.config.int
# ...

Defining the Logic

Each step defines some Logic to be called, specifies the inputs the Logic is to be provided with, specifies an optional status condition, and optionally specifies any state you wish exposed within the parent resource's status.state.

Logic is defined by ValueFunction, ResourceFunction, or using a sub-Workflow to compose Functions. To reference the Logic, you specify the kind and name. Logic may be statically specified using ref, which specifies the exact Logic to run. It may also be dynamically selected from a fixed set of references using refSwitch, which provides the ability to select between multiple Logics which implement a compatible interface—this is discussed in more detail below.

Steps may run concurrently

A step is run once all steps it references have completed. To help make the sequencing clearer, you are required to list steps after any step(s) they reference. Note that steps may run concurrently as their dependencies complete, so you should not depend on the order in which they are listed.

Steps also specify inputs to be provided to the Logic. Within Functions, the inputs are directly accessible within inputs. Within Workflows, the inputs are exposed under parent. This enables a Workflow to be directly triggered via a crdRef or it may be directly called as a sub-Workflow. That makes reuse and testing of Workflows easier. Steps can depend on other steps by referencing them as an input value. This allows you to map outputs from one step into inputs of another step.

- label: static_get_value
ref:
kind: ValueFunction
name: get-value.v1
inputs:
input: =steps.config.output
Avoiding tight coupling

The parent resource can be passed as an input to steps from a Workflow with =parent. However, rather than passing the entire parent resource to steps, it's recommended to pass only what is needed from the parent to avoid tightly coupling Logic to triggering resources. For example, =parent.metadata rather than =parent if only the metadata is needed by a Function.

Control Flow

A step may also specify a forEach block, which will cause the Logic to be executed once per item in the forEach.itemIn list. Each item will be provided within inputs with the key name specified in forEach.inputKey. This makes using any Function within a forEach viable.

Steps may be conditionally run using skipIf. When the skipIf evaluates to true, the step and its dependencies are Skipped without resulting in an error by stopping further evaluation of the step and its dependencies. skipIf enables the Workflow to dynamically determine which steps to run. This allows Logic to define a common interface, then for the Workflow to call the correct Logic. This enables if or switch statement semantics.

Logic may be dynamically selected from a set of choices using refSwitch. refSwitch allows Logic to define a common interface, then for the Workflow to call the appropriate Logic based on input or computed values. The switchOn expression has access to the return values from prior steps within steps. It also has access to the inputs that will be provided to the Logic. Using inputs enables refSwitch to work with forEach and dispatch the correct Logic for each item.

State

A step may expose a Condition on the parent resource using condition. The Condition's type will match condition.type, and this should be unique within your Workflow. Note that uniqueness is intentionally not enforced so that you may update / change conditions subsequently, but you should be cautious about reusing the same type since it makes debugging much harder. condition.name is used within the condition message sentence to make human-friendly status messages. It should be a meaningful name or short descriptive phrase.

warning

Be careful not to accidentally step on the condition.type value as it makes debugging much harder and reduces visibility into a Workflow's status.

The Logic's results may be exposed via the state key. The Koreo Expressions within the Workflow step may access the Logic's return value within value. If specified, state must be a mapping and it will be merged with other step.state values. This allows for fine control over what and how state is exposed.

warning

If multiple steps set the same state keys, the return values will be merged. This can lead to confusing values, so be cautious.

Logic Example

apiVersion: koreo.dev/v1beta1
kind: Workflow
metadata:
name: simple-example.v1
namespace: koreo-demo
spec:
steps:
- label: config
ref:
kind: ValueFunction
name: simple-example-config.v1
inputs:
metadata: =parent.metadata

- label: simple_return_value
ref:
kind: ValueFunction
name: simple-return-value.v1
inputs:
string: =steps.config.string
int: =steps.config.int
state:
config:
nested_string: =value.nested.a_string
empty_list: =value.empties.emptyList

- label: switched_resource_reader
refSwitch:
switchOn: =inputs.result
cases:
- case: "1"
kind: ResourceFunction
name: resource-reader-1.v1
- case: "2"
kind: ResourceFunction
name: resource-reader-2.v1
- case: "3"
kind: ResourceFunction
name: resource-reader-3.v1
inputs:
result: =steps.simple_return_value.result

- label: resource_factory
ref:
kind: ResourceFunction
name: resource-factory.v1
forEach:
itemIn: =["a", "b", "c"]
inputKey: suffix
inputs:
name: resource-function-test

Managed Resources

Workflows write a metadata annotation called koreo.dev/managed-resources on the parent resource which triggered them. This annotation is a recursive JSON structure which contains two top-level keys: workflow, which is the name of the Workflow that processed the parent, and resources, which is a mapping of Workflow steps to managed resources. This resource mapping can be recursive depending on the structure of the Workflow step. In this context, a "managed resource" can be one of four things:

Kubernetes resource

This is a true managed resource, indicating the step pertains to a ResourceFunction. This is an object containing several keys pertaining to the Kubernetes resource: apiVersion, kind, name, namespace (optional), plural (optional), resourceFunction, and readonly. resourceFunction is the name of the ResourceFunction interfacing with the resource. readonly indicates if the ResourceFunction only reads the resource or if it is a manager of the resource.

Managed resources object

This is a recursive object containing the workflow and resources keys, indicating the step pertains to a sub-Workflow.

Array of Kubernetes resources and/or managed resources objects

This indicates the step pertains to a ResourceFunction or sub-Workflow within a forEach. This array can contain both Kubernetes resources and managed resources objects because the step may be a refSwitch which could result in both ResourceFunctions and sub-Workflows executing for different iterations of the loop, depending on which refSwitch cases are selected.

null

A null value indicates the step does not pertain to a resource. Examples include the step is a ValueFunction, a ResourceFunction with a skipIf, or a forEach on an empty list.

An example of a koreo.dev/managed-resources annotation value is shown below. In this example, the config, metadata, and resource_tags steps on aws-workload-workflow are ValueFunctions, which is why they map to null values. environment_metadata corresponds to a read-only ResourceFunction. The resources step is a forEach, which is why it's an array. runtime is a sub-Workflow called lambda-workflow with four steps which ultimately produces three Kubernetes resources. Lastly, the triggers step is a forEach whose itemIn expression evaluated to an empty list, meaning the sub-Workflow the step pertains to did not execute.

{
"workflow": "aws-workload-workflow",
"resources": {
"config": null,
"metadata": null,
"resource_tags": null,
"environment_metadata": {
"apiVersion": "aws.konfigurate.realkinetic.com/v1beta1",
"kind": "AwsEnvironment",
"plural": "awsenvironments",
"name": "dev",
"readonly": true,
"namespace": "realkinetic-dev",
"resourceFunction": "aws-environment"
},
"resources": [
{
"apiVersion": "s3.services.k8s.aws/v1alpha1",
"kind": "Bucket",
"plural": "buckets",
"name": "test-1312e8",
"readonly": false,
"namespace": "realkinetic-dev",
"resourceFunction": "aws-s3-bucket-factory"
}
],
"runtime": {
"workflow": "lambda-workflow",
"resources": {
"config": null,
"lambda_policy": {
"apiVersion": "iam.services.k8s.aws/v1alpha1",
"kind": "Policy",
"plural": "policies",
"name": "test-lambda",
"readonly": false,
"namespace": "realkinetic-dev",
"resourceFunction": "lambda-policy"
},
"lambda_role": {
"apiVersion": "iam.services.k8s.aws/v1alpha1",
"kind": "Role",
"plural": "roles",
"name": "test-lambda",
"readonly": false,
"namespace": "realkinetic-dev",
"resourceFunction": "lambda-role"
},
"lambda_resource": {
"apiVersion": "lambda.services.k8s.aws/v1alpha1",
"kind": "Function",
"plural": "functions",
"name": "test",
"readonly": false,
"namespace": "realkinetic-dev",
"resourceFunction": "lambda-resource"
}
}
},
"triggers": null
}
}

This managed resources data is exposed for consumption by tooling as well as to aid with operations and debugging. For example, this data is leveraged by Koreo UI in order to render Workflow instance graphs and surface other information about Workflows.

Workflow Example

The following Workflow demonstrates some of the capabilities. Refer to the Workflow spec for the complete set of Workflow configurations.

apiVersion: koreo.dev/v1beta1
kind: Workflow
metadata:
name: simple-example.v1
namespace: koreo-demo
spec:

# Creation, modification, or deletion of a TriggerDummy will trigger
# this Workflow to run. That is, this Workflow will act as a controller of
# this resource type. An instance of this resource type is referred to as the
# "parent" resource for a Workflow.
crdRef:
apiGroup: demo.koreo.dev
version: v1beta1
kind: TriggerDummy

# Steps may be run once all steps they reference have been run. To help make
# the sequencing clearer, you are required to list steps after any step(s)
# they reference. Note that steps may run concurrently as their dependencies
# complete, so you should not depend on the order in which they are listed.
steps:
# Each step must have a label, which acts as an identifier. The return
# value of the Logic is available to be passed into other steps' inputs as
# `steps.simple_return_value`. If a step does not return successfully, then
# any step referencing it will automatically be skipped and marked as
# `depSkip`.
- label: config

# The logic to be run.
ref:
kind: ValueFunction
name: simple-example-config.v1

# The inputs are available within the Logic under `=inputs.`
# `step.inputs` must be a mapping, but these may be Koreo Expressions,
# simple values, lists, or objects. The parent resource can be accessed
# with `=parent`.
inputs:
name: =parent.metadata.name
namespace: =parent.metadata.namespace

- label: simple_return_value
ref:
kind: ValueFunction
name: simple-return-value.v1

# Use an expression to pass the return value from another step.
inputs:
string: =steps.config.string
int: =steps.config.int

# Some or all of the return value may be surfaced into the parent's
# `status.state`. This is useful to cache values or to surface them to
# other tools such as a UI or CLI. By default, nothing is surfaced.
state:
config:
nested_string: =value.nested.a_string
empty_list: =value.empties.emptyList

- label: resource_reader
ref:
kind: ResourceFunction
name: resource-reader.v1

# `step.inputs` may be a more complex structure, and Koreo Expressions
# may be used for specific subkeys or nested within an object or list.
inputs:
name: resource-function-test
validators:
skip: false
depSkip: false
permFail: false
retry: false
ok: false
values:
string: =steps.config.string
int: =steps.config.int

- label: resource_factory
ref:
kind: ResourceFunction
name: resource-factory.v1

# Steps may be run once per item in an array. Each may be run
# concurrently, so there is no execution ordering guarantee. The return
# value of this step is an array of the return values who's order matches
# source array's order. Each iterated value is provided to the Logic as
# `=inputs[forEach.inputKey]`. That is, the value of input key is the
# subkey within inputs.
forEach:
itemIn: =["a", "b", "c"]
inputKey: suffix

inputs:
name: resource-function-test
validators:
skip: false
depSkip: false
permFail: false

Specification

NameTypeDescriptionRequired
apiVersionstringkoreo.dev/v1beta1true
kindstringWorkflowtrue
metadataobjectRefer to the Kubernetes API documentation for the fields of the metadata field.true
specobject
false
statusobject
false

spec

NameTypeDescriptionRequired
steps[]object

A collection of Functions and Workflows (Logic) which define this Workflow. The steps may be run as soon as all of their dependencies have successfully evaluated. Any step referencing another step must be listed after the step it references, but evaluation order is not guaranteed.


true
crdRefobject

Optionally specify a Resource that will cause this Workflow to run. The triggering-object is available to pass as an input as parent.


false

spec.steps[index]

NameTypeDescriptionRequired
labelstring

The name other steps may use to access this step's return value (or to express dependence on this step). This is an identifier it must be alphanumeric + underscores.



Validations:

  • self.matches("^[[:word:]]+$"): must be alphanumeric + underscores
  • true
    conditionobject

    Optional configuration for a Condition that will be set in the triggering-object's status.conditions list.


    false
    forEachobject

    Invoke the Logic once per item in itemIn, passing the item within inputs as the value of inputKey.


    false
    inputsobject

    The inputs to be provided to the Logic. inputs must be an object, but static values, arrays, and objects are allowed. Koreo Expressions are allowed as values and are indicated with a leading =. Note that no default inputs are provided. The return values from prior steps, may be referenced using the step's label within the steps object. You may pass in the entire value or extract sub values from prior steps (steps.prior_step.sub.value).


    false
    refobject

    The Function or Workflow to run as the Logic. No default inputs are provided to the Logic. For Workflows, inputs are passed as the parent, which allows workflows to be invoked as sub-workflows or by their crdRef.


    false
    refSwitchobject

    Dyanmically select the Function or Workflow to run as the step's Logic. No default inputs are provided to the Logic. For Workflows, inputs are passed as the workflow's parent, which allows workflows to be invoked as sub-workflows or by their crdRef.


    false
    skipIfstring

    Skip running this step if the Koreo Expression evaluates to true.



    Validations:

  • self.startsWith('='): must be an expression
  • false
    stateobject

    Defines an object that will be set on the parent's status.state. Useful for surfacing values for informational or debugging purposes, or to act as a cache. The state objects from all steps are merged, so if steps shares keys, the values from last step to run may overwrite earlier values.


    false

    spec.steps[index].condition

    Optional configuration for a Condition that will be set in the triggering-object's status.conditions list.

    NameTypeDescriptionRequired
    namestring

    A short, human-meaningful description of what the step does or manages. It is used within simple template descriptions to describe the step's outcome.



    Validations:

  • !self.startsWith('='): name may not be an expression
  • true
    typestring

    The type for a condition acts as its key. If steps share type, the condition from last run will be reported on the parent.



    Validations:

  • self.matches("^[A-Z][a-zA-Z0-9]+$"): must be PascalCase and contain only alphanumeric chars
  • true

    spec.steps[index].forEach

    Invoke the Logic once per item in itemIn, passing the item within inputs as the value of inputKey.

    NameTypeDescriptionRequired
    inputKeystring

    The key within inputs that the item will be passed under.


    true
    itemInstring

    An expression that evaluates to a list of values.



    Validations:

  • self.startsWith('='): must be an expression (start with '=').
  • true

    spec.steps[index].ref

    The Function or Workflow to run as the Logic. No default inputs are provided to the Logic. For Workflows, inputs are passed as the parent, which allows workflows to be invoked as sub-workflows or by their crdRef.

    NameTypeDescriptionRequired
    kindenum

    Enum: ResourceFunction, ValueFunction, Workflow

    true
    namestring
    true

    spec.steps[index].refSwitch

    Dyanmically select the Function or Workflow to run as the step's Logic. No default inputs are provided to the Logic. For Workflows, inputs are passed as the workflow's parent, which allows workflows to be invoked as sub-workflows or by their crdRef.

    NameTypeDescriptionRequired
    cases[]object
    false
    switchOnstring

    Validations:

  • self.startsWith('='): must be an expression
  • false

    spec.steps[index].refSwitch.cases[index]

    NameTypeDescriptionRequired
    casestring

    Validations:

  • !self.startsWith('='): may not be an expression
  • true
    kindenum

    Enum: ResourceFunction, ValueFunction, Workflow

    true
    namestring
    true
    defaultboolean

    Default: false

    false

    spec.crdRef

    Optionally specify a Resource that will cause this Workflow to run. The triggering-object is available to pass as an input as parent.

    NameTypeDescriptionRequired
    apiGroupstring
    true
    kindstring
    true
    versionstring
    true