Policy enforcement
Some operations need a governance decision before they proceed. The built-in policy module supplies the decision: its evaluate action returns a verdict, and the workflow definition enforces it. AutoFlow acts on no verdict by itself; the definition decides what each one means, and a person enters only when the verdict asks for one.
Ask for a verdict
evaluate(trigger=, resource=, context=None) is the policy module’s only action. trigger names the event the decision is about, by convention reverse-DNS; resource names what it is about; the optional context is a JSON object string the policies read as input. The result is a dict with decision_id and verdict.
load("module:policy",
"evaluate", "ALLOW", "DENY", "REQUIRE_APPROVAL", "UNDECIDABLE")
decision = gather(evaluate(
trigger = "com.gitlab.deploy.requested",
resource = "projects/%d/environments/%s" % (project_id, environment),
))Like every action, evaluate returns a future; gather it to get the dict, see Futures and gather. The module exports the four verdicts as variables, so a definition compares against names rather than strings.
Enforce the verdict
| Verdict | Meaning | What the workflow definition does |
|---|---|---|
ALLOW | The policies permit the operation. | Proceeds. |
DENY | A policy forbids the operation. No other policy can overturn a deny. | Fails closed with fail(). |
UNDECIDABLE | The policies reached no verdict; nothing is known about the operation. | Fails closed, or escalates to a person the same way as REQUIRE_APPROVAL. |
REQUIRE_APPROVAL | The operation may proceed only if a person approves it. | Hands the decision to a person: creates a channel, hands it out, waits for the answer. |
Fail closed is the default. Anything that is not ALLOW, and not a verdict the definition explicitly escalates, ends the workflow. The example below does that with two branches: REQUIRE_APPROVAL asks a person, and every other verdict except ALLOW fails the workflow.
Example: approval by a person
This workflow definition asks the policy module whether a deployment may proceed. When the verdict is REQUIRE_APPROVAL it hands a channel to GitLab, suspends until somebody answers, and then creates the deployment. AutoFlow keeps the workflow alive for the whole wait without holding a single goroutine.
load("module:policy", "evaluate", "REQUIRE_APPROVAL", "ALLOW")
load("module:gitlab", "call_api", "post_value")
def main(w, project_id, environment, token):
headers = {
"Authorization": "Bearer " + token,
"Content-Type": "application/json",
}
decision = gather(evaluate(
trigger = "com.gitlab.deploy.requested",
resource = "projects/%d/environments/%s" % (project_id, environment),
))
if decision["verdict"] == REQUIRE_APPROVAL:
reply = channel()
gather(post_value(
"/api/v4/projects/%d/deploy_approvals" % project_id,
value = {"environment": environment, "reply": reply},
headers = headers,
))
if gather(reply, timeout = 3 * 24 * time.hour) != "approved":
fail("deployment not approved")
elif decision["verdict"] != ALLOW:
fail("deployment denied by policy")
status, _, _, err = gather(call_api(
"POST",
"/api/v4/projects/%d/deployments" % project_id,
headers = headers,
body = json.encode({"environment": environment}),
))
return status
sequenceDiagram
participant F as Workflow
participant R as AutoFlow
participant G as GitLab
participant H as Human
F->>R: evaluate(trigger, resource)
R-->>F: verdict REQUIRE_APPROVAL
F->>R: post_value(path, value with channel)
R->>G: POST deploy_approvals {value, channel_tokens}
G->>H: approval request
Note over F,R: Workflow suspended, holding no resources
H->>G: approve
G->>R: SendToWorkflowChannel(channel_token, workflow_token, "approved")
R-->>F: gather(reply) returns "approved"
F->>R: call_api("POST", deployments)
R->>G: POST /api/v4/projects/:id/deployments
Create a channel
channel() creates a stream the workflow can receive on. The workflow itself cannot send to it; only a module the channel was passed to, or whoever the module hands it on to, can. That is what makes it a safe handle to give away.
Hand it to the outside
post_value(path, value=, headers=None) POSTs a JSON body to a GitLab endpoint. The channel sits inside value like any other value. Because the invocation carries a channel, the module exchanges the channel’s token, which is minted for the module itself, for one that binds no principal, and puts it in the body next to the value:
{
"value": {"dict_value": {"key_values": [
{
"key": {"string_value": "environment"},
"val": {"string_value": "production"}
},
{
"key": {"string_value": "reply"},
"val": {"channel_value": {"name": "..."}}
}
]}},
"channel_tokens": [{"channel_name": "...", "token": "<JWT>"}]
}Content-Type is always application/json and an Idempotency-Key header is always sent, so a retried POST is recognizable. Like call_api, post_value returns the (status, headers, body, error) tuple; the definition above gathers it without inspecting it, which is enough to make the call happen.
Wait
gather(reply, timeout = 3 * 24 * time.hour) blocks until a value arrives on the channel or three days pass. On timeout it returns None, the default timeout_value, and consumes nothing. None != "approved", so a timeout is a rejection. During the wait the workflow is suspended; it holds no goroutine, no connection and no memory. See Durable waits.
Act
Once the answer is "approved", or the verdict was ALLOW to begin with, the workflow creates the deployment with call_api and returns the HTTP status as the workflow result. A non-2xx status is data in the tuple, so a real workflow definition inspects status and err before returning.
The receiving side
The endpoint that receives the posted value is responsible for showing the request to a person and, once they decide, calling the SendToWorkflowChannel RPC on AutoFlow’s API listener. The request has five fields:
{
"idempotency_key": "deploy-approval-4711",
"channel_token": "<token from the posted body>",
"value": {"string_value": "approved"},
"workflow_token": "<token StartWorkflow returned>",
"namespace_id": 1
}idempotency_keydeduplicates the message within the workflow. A retry with the same key delivers at most once; a new key is a second delivery.channel_tokenis taken fromchannel_tokensin the posted body, opaque material to forward.valuemust not contain a channel and must marshal to at most 64 KiB.workflow_tokenis the capabilityStartWorkflowreturned to whoever started the workflow. The channel token alone carries no send. The recipient must resolve the workflow token from its own records, never from the posted body: the body arrives without any AutoFlow credential, so nothing in it is attested.
The send is fire-and-forget. A successful response means AutoFlow accepted the value; the workflow sees it on its next round. A workflow that already ended refuses the send with FAILED_PRECONDITION.
Where this stands today
- The
/api/v4/projects/:id/deploy_approvalsendpoint in the definition is illustrative. No GitLab endpoint receives posted values and answers on the channel yet; the receiving side has to be built by the integration that starts the workflow. - The
policymodule returnsALLOWfor every evaluation until a policy source is wired to it, anddecision_idis a zero UUID placeholder. It is only available in an AutoFlow build that links the policy engine; otherwiseload("module:policy", ...)fails withpolicy engine is not enabled. - A channel a child workflow hands out does not work yet: the send succeeds and the value is dropped. Create the channel in the workflow that will gather it.
- The workflow token, channel token and token binding this guide relies on are an interim design. Expect them to change once GATE is available.
Related
- Call the GitLab API for
call_apiin detail. - API reference for
StartWorkflow, tokens andSendToWorkflowChannel.