Prefetch: Typeahead Fields

Prefetch Reducers are used to dynamically populate a Slack prompt from an external data source.

Overview

Prefetch Reducers are useful for scenarios when you might not know every option for a given Prompt Fields ahead of time.

For example:

  • Growing datasets that live outside of Terraform, like lists of customer-specific AWS instances
  • Sensitive situations where you might want to check information about a requestor before rendering
  • Datasets that you do not control directly, as in the example below

Implementation

Prefetch Reducers are two steps to their implementation:

  1. Marking a Prompt Field with prefetch = true indicates that it will be filled via Prefetch.
  2. If using Prefetch for an Access Target, you must name your Prompt Field to match the settings attribute expected by that Target, and bind the corresponding Prompt Field to your Strategy via field_bindings (see example below)
  3. Each Prompt Field marked with prefetch = true must have a corresponding Prefetch Reducer, and there may be exactly one Prefetch Reducer for a given Prompt Field.

A Prefetch Reducer must return a list of FieldOptions, where a FieldOption defines a given item to be displayed in Slack.

The list of FieldOption returned by your reducer will dictate the allowed values a requester may select from for that Prompt Field. When filling out the request form, the requester's input will be used to search for a FieldOption based on its label.

🚧

Prefetch Reducer Limits

  1. Prefetch Reducers may return up to a maximum of 1000 items. If your reducer returns more than 1000 items, your Flow will terminate with an error.

  2. Prefetch Reducers are run before displaying the request form to users. If your reducer is slow, it may impact the performance of your requests!

  3. Prefetch reducers will be validated at runtime, and not during terraform apply. If there is a missing or duplicate prefetch reducer, an error will be raised as the user makes the request in Slack.

541

A demo of a Prompt Field that returns Pokemon names as FieldOptions

Prefetch for non-Target fields

Given the following Prompt Field definition:

prompt_field {
  name     = "pokemon"
  label    = "What Pokemon do you want?"
  type     = "string"
  prefetch = true
  required = true
}

You could define the following Prefetch Reducer:

import requests
from sym.sdk.annotations import prefetch
from sym.sdk.field_option import FieldOption


@prefetch(field_name="pokemon")
def get_pokemon(evt):
  # Make an API Call or even invoke an AWS Lambda
  response = requests.get(url="https://pokeapi.co/api/v2/pokemon?limit=100")
  all_pokemon = response.json()["results"]

  # Return a list of FieldOption
  return [
    FieldOption(value=pokemon["name"], label=pokemon["name"].upper())
    for pokemon 
    in all_pokemon
  ]

Prefetch for Access Targets

Prefetching values for an Access Target will vary depending on integration. In the below, we will show how to create a dynamic, typeahead field that uses Prefetch for a list of GitHub repos.

Static GitHub Strategy

The sym_target associated with Sym's GitHub Strategy expects a repo_name in the settings field.

resource "sym_target" "private-repo" {
  type = "github_repo"

  name  = "main-private-repo-access"
  label = "Private Repo"

  settings = {
    # `type=github_repo` sym_targets have a required setting `repo_nae`,
    # which must be name of the Repository the requester will be escalated to when this target is selected
    repo_name = "private-repo"
  }
}

Dynamic GitHub Strategy with Prefetch

To make the above example dynamic, we will need to:

  1. Create a Prompt Field named repo_name and mark it as prefetch = true
  2. Bind the field to our sym_target
  3. Provide a Prefetch Reducer to populate the field

πŸ“˜

See this example in full in our Examples Repo

GitHub Dynamic Targets Example

# 1. Create a Prompt Field named `repo_name` and mark it as `prefetch = true`
resource "sym_flow" "this" {
  name  = "github"
  label = "GitHub Repo Access"

  implementation = file("${path.module}/impl.py")
  environment_id = sym_environment.this.id

  params {
    strategy_id = sym_strategy.github.id

    prompt_field {
      # This prompt_field will be used to populate the `repo_name` setting of the GitHub Access Target.
      # The name must match the setting name and type.
      name     = "repo_name"
      label    = "Repository Name"
      type     = "string"
      required = true

      # Setting prefetch = true means that this field's allowed values will be populated by
      # a prefetch reducer defined in the impl.py
      prefetch = true
    }

    prompt_field {
      name           = "duration"
      type           = "duration"
      allowed_values = ["1h", "1d", "10d"]
      required       = true
    }
  }
}
# 2. Bind the `repo_name` field to our `sym_target` 
resource "sym_target" "private-repo" {
  type = "github_repo"

  name  = "private-repos"
  label = "Private Repos"

  # A special attribute indicating which settings will be dynamically populated by prompt fields.
  # In this case, the setting is the required `repo_name` setting. The value will be populated by the
  # `repo_name` field in the `sym_flow.params.prompt_fields` attribute.
  field_bindings = ["repo_name"]
}
# 3. Provide a Prefetch Reducer to populate the field
@prefetch(field_name="repo_name")
def get_repos(event):
    github_token = event.flow.environment.integrations["github"].settings["api_token_secret"].retrieve_value()
    headers = {"Authorization": f"Token {github_token}"}

    response_options = []

    while True:
        # Use the GitHub API to get all the private repos in the Sym Test organization
        # Note, we are skipping over pagination in this example
        response = requests.get(
            url=f"https://api.github.com/orgs/sym-test/repos&type=private",
            headers=headers
        )

        # For this example, the value and the label are the same for the options.
        response_options = [
            FieldOption(value=repo["name"], label=repo["name"]) for repo in response.json()
        ]
                return response_options