Skip to content

Pardon Collections

This tutorial builds on the concepts explained in Pardon Templates,

Services and Endpoints

A Pardon collection is generally one or more services anchored by a service.yaml file and with each endpoint represented by an https (HTTP Schema) file.

Schema files are a sequence of http request and response templates, and optionally some additional inline configuration.

A basic collection defining a service and the actions for a products REST API might look like this.

  • Directorycollection
    • Directoryexample the example service
      service=“example”
      • service.yaml common configuration for all the endpoints
      • ping.https a basic endpoint
        endpoint=example/ping
        action=ping
      • Directoryproducts we can group related endpoints
        • create.https create a product
          action=“create”
          endpoint=“example/products/create”
        • get.https get a product
          action=“get”
          endpoint=“example/products/get”
        • update.https update a product
        • delete.https delete a product
        • list.https list products

When developing a collection, one might start with a service.yaml and confirm the setup with something like a ping.https endpoint.

The service.yaml file contains common specifications like config, defaults, and mixins (which we’ll get to later in this tutorial).

config:
origin:
env:
prod: https://example.com
stage: https://stage.example.com

So far, so good: we should be able to build requests for both stage and production origins.

Input values

The input request is the “most direct” mechanism for getting data into Pardon, but not everything can be solved without another layer of indirection!

Values can supply data to templates, and can even be used to rewrite templates where config alternatives are involved.

In these exercises a new input field for values is available to explore this mechanism (the input).

Exercises
Try setting a value in the values input.

config choices can be overridden with values. This allows any request to be easily repurposed to a different environment (in particular, replaying tests).

env=stage
Loading Pardon Playground...

A RESTful resource

The products lifecycle would also defined with https files:

For product creation, this template only defines the name field (which makes it mandatory),

create.https
>>>
POST https://example.com/products
Content-Type: application/json
{
"name": "{{name}}"
}

Defining the response template instructs pardon to extract the product value from the response, assuming it matches. Equally importantly, it provides a little documentation about the shape of the response for humans. We will cover dataflow in a later section, as it’s more of an advanced topic for scripting.

The REST of the product resources endpoints are specified minimally here, just enough to distinguish them from each other.

Requests info for a single product.

example/products/get.https
>>>
GET https://example.com/products/{{product}}

With all this set up we can finally explore how pardon works with a collection of requests.

Exercises
Set a value via values.

Notice how we can provide the {{name}} value here without having to specify a request body at all!

Many simple requests can be parameterized to take advantage of this abbreviated input mechanism.

env=stage name=thneed
Loading Pardon Playground...

Config

The config mapping enables templates to assert possible combinations of certain values.

A simple mapping of

config:
key: value

specifies one option of { "key": "value" }, and

config:
key1: value1
key2: value2

specifies one option of { "key1": "value1", "key2": "value2" }. This is not functionally any better than normal template matching, but it has its uses since the data is outside the rendering of the template.

Revisting the config we introduced earlier

config:
origin:
env:
prod: https://example.com
stage: https://stage.example.com

You can read this as

  • origin = (when env is prod) https://example.com, and
  • origin = (when env is stage) https://stage.example.com

and this allows for two variants of the request

  • { env: "prod", origin: "https://example.com" }, and
  • { env: "stage", origin: "https://stage.example.com" }

Specifying env=prod implies the origin value and vice-versa, and env becomes a value that can be used in scripts.

Patterns, with expressions, can be included, e,g., this config

config:
origin:
env:
prod: https://example.com
stage: https://stage.example.com
local: http://localhost:{{port=8000}}

also allows env=local and provides a default port.

Mixins

Oh no. In our rush to add features, we forgot to include authorization headers to call these endpoints: the security team took care of this in the gateway on stage, and now all of our endpoints need to be updated, and tested.

We want to tell Pardon that by default all calls to this service needs authentication, rather than updating every endpoint.

We can do this with a mixin at the service level.

example/service.yaml
config:
origin:
env:
prod: https://example.com
stage: https://stage.example.com
mixin:
- ./auth.mix.https

We’re putting it next to our service in the file tree. But any relative path inside the service directory is fine.

  • Directorycollection
    • Directoryexample
      • service.yaml
      • auth.mix.https
      • ping.https
      • Directoryproducts/
        • create.https

The contents look (kind of) like any other https endpoint.

example/auth.mix.https
>>>
ANY //
Authorization: {{ @auth = `${env}-auth-token` }}

The special request method ANY matches all request methods, and the special value // matches any URL. (A “mixin” is mixed-in only if it is “compatible” with the request. So this mixin can apply to any service call.)

This adds an authorization header to all requests.

Let’s try it out in stage and confirm all our endpoints are working and have authorization applied… (The @ in @auth marks it a secret. You can unlock the secret for viewing by tapping the lock button in this exercise.).

Loading Pardon Playground...

Mixin-Match

TL;DR: mixins progressively enhance the input request, but only if they match.

They do not affect whether the endpoint template matches the input request.

Awesome! We have applied a security measure applied to our requests. And we provided confirmation of all our requests working on stage, so ops rolled these changes out to production.

Unfortunately, now alerts are firing because the (oops) only-running-in-production-liveness-probe hitting /ping also requires an auth token now.

We missed this case in staging because when we tested our updated collection we sent auth tokens to every endpoint.

Well, it’s not actually an outage, and the fix isn’t even on our end (ops is in control of removing the Authorization header check from the /ping endpoint). But we do want to adjust our collection to not send an unnecessary auth token for ping calls.

It is cleaner (in this case, at least) to specify an opt-out for the few exceptional endpoints that don’t need auth, rather than opting-in. To have the mixin supply an opt-out behavior, we can add a config section.

example/auth.mix.https
config:
authorization: token
>>>
ANY //
Authorization: {{ @auth = `${env}-auth-token` }}

Just like origin in the main request, this authorization value needs to match (or be not specified) in the input for this mixin to be applicable.

We can then default authorization as none in our ping endpoint (choosing any value other than token), which disables the mixin selectively.

example/ping.https
defaults:
authorization: none
>>>
GET https://example.com/ping

This implements the same change in behavior with two changes instead of N+1, and provides some additional controls we explore here:

Exercises

Notice the authorization header is gone, because it’s disabled for ping by default now.

GET https://example.com/ping
Loading Pardon Playground...

default.https

A service can have a default request that is tried if no specific request matches. The default.https request extends the service.yaml specification.

Since multiple services can have defaults, we probably want to use defaults only for stage/prod origins, and not localhost ones (since then they would conflict with each other).

To specify a subset of environments, you can use a list in the config mapping.

example/default.https
config:
env:
- stage
- prod
mixin:
- ./auth.mix.https
>>>
ANY //

When merging config mappings, all possibilities of the last mapping will be retained, along with all options of compatible values for each of them, if present. So in this case the config acts as a filter, only applying “default.https” to requests to https://example.com/ and https://stage.example.com/ that don’t match a more specific template.

Collection Layering

To match team structures, collections are defined in layers:

A base layer might define the endpoints, but credentials can be defined in another. Different teams have different access to credentials to the same services, so Pardon supports separation of concerns and access requirements by allowing layers of functionality to be applied.

Requests, mixins, and service configs can all be layered.

A root pardon config defines all the layers of collections to use. We’ll need to revisit this in the section on scripting.

Next Steps

Next we will cover more dataflow mechanisms.

Then layering and scripting collections.