Skip to content
This repository has been archived by the owner on Aug 4, 2023. It is now read-only.

Latest commit

 

History

History
147 lines (113 loc) · 8.8 KB

architecture.md

File metadata and controls

147 lines (113 loc) · 8.8 KB

Software Architecture

This document describes the architecture of the til interpreter, and explains the design choices behind it.

Contents

  1. Overview
  2. Request Flow
  3. Graph Builder
  4. Components Implementations
  5. Bridge Translator

Overview

The Component Graph

The architecture of til borrows a few ideas from HashiCorp Terraform. For instance, we reason about a Bridge description as a directed graph where each component of a messaging system is a vertex ("node") connected to one or more other components by an edge ("link") that represents the flow of events in that system (Terraform calls it the "Resource Graph").

A graph is also a good fit for HCL's evaluation model, where the dependencies of each expression should be analyzed in order to infer the evaluation context. For example, the evaluation of a router block containing a reference to a transformer block can only be performed after the transformer block itself is evaluated. The HCL guide refers to this pattern as Interdependent Blocks.

However, this is where the comparison with Terraform ends due to fundamental differences in what both of these tools try to achieve.

Loose Comparisons with Terraform

Like Terraform, til...

  • Interprets configuration files written in a language based on the HCL syntax (the TriggerMesh Integration Language).
  • Supports cross-references between HCL configuration blocks using traversal expressions.
  • Represents configuration blocks and the relationships between them as a directed graph.
  • Uses internal schemas (hcldec.Spec) for decoding the contents of HCL blocks into concrete types/values.

Unlike Terraform, til...

  • Is not concerned about the deployment aspects of the components it interprets. The interpreter generates deployment manifests which users are free to deploy using the tool of their choice (kubectl, Helm, Terraform, ...).
  • Does not depend on side-effects during runtime to evaluate certain parts of a configuration (such as creating a certain resource in order to be able to determine the value of a variable). A Bridge Description File is interpreted statically.
  • Leverages graphs to represent data flows, not dependencies.

Request Flow

Below is a high level representation of the internal request flow within the interpreter when a user generates deployment manifests from a Bridge description. Each subsystem is described in more details in the rest of this document.

Architecture - Request flow

Graph Builder

The role of the core.GraphBuilder is to place each component of a Bridge onto a graph and connect it to other components, based on the information contained in its HCL block.

This is achieved by successive transformations performed by implementers of the core.GraphTransformer interface. In the process, the graph builder attaches to each graph vertex any information susceptible to be useful to translators (e.g. a schema to decode the contents of the corresponding HCL block, looked up in a map of supported component implementations). The inspiration for this iterative approach comes from Terraform's design, and aims at making the process of building the graph more testable, even though the complexity of til is not comparable to Terraform's.

In order to avoid leaking the "graph" domain into the "configuration" domain, and having to implement large interfaces for each type in the config package, we leverage Go's behavioral polymorphism by wrapping config types (such as config.Channel, config.Source, etc.) into structs that represent graph vertices. Each vertex type (e.g. core.ChannelVertex) implements methods that are only relevant to the core package, and generally to a single core.GraphTransformer. For instance, if a type of component can be the destination of events within the Bridge, and by definition exposes a Knative Addressable type, its vertex type will implement core.AddressableVertex.

Components Implementations

For a component type to be able to appear in a Bridge description, it needs to have a corresponding implementation that allows the interpreter to decode its HCL configuration and translate it into one or more Kubernetes API objects.

Such implementation combines three Go interfaces defined in the translation package: Decodable, Translatable and Addressable. The semantics of those interfaces maps directly into the types of operations performed by the interpreter:

  • Decodable provides the hcldec.Spec that is used by HCL APIs (such as hcldec.Decode) for decoding component-type-specific segments of HCL configuration into data that can be interpreted in the Go language. As long as a component requires some kind of non-generic configuration, it must implement this interface.
  • Translatable bridges the gap between a HCL configuration block and its representation in the Kubernetes space by generating values that represent Kubernetes objects, from configurations decoded from the Bridge description. All components implement this interface.
  • Addressable allows determining the address at which a component accepts events. Components that can ingest events implement this interface so that HCL reference expressions in other components can be evaluated to actual addresses.

As a means of comparisons, Terraform has a concept of "provider" which also interacts with custom "resource" types by using decode schemas and calling CRUD functions on the decoded configuration data.

Currently, all components implementations reside in-tree, inside sub-packages of internal/components.

Bridge Translator

The core.BridgeTranslator is responsible for evaluating the simple connected graph built by core.GraphBuilder, and translating it into a list of Kubernetes manifests which can be deployed to a target environment (cluster) to materialize the Bridge.

It does so by creating a topological ordering of the graph which ensures, whenever possible, that the address (event destination) of a given Bridge component is readily available when components which depend on that address are evaluated/translated. This ordering allows the translation to be accurate without having to visit a graph vertex multiple times (e.g. once to store its event address in the evaluation context, once to translate it after all addresses within the Bridge have been determined).

Event addresses are stored as variables inside a core.Evaluator, which accumulates them as the graph is being walked.

Evaluation - Topological sort