Configuration

Harness uses Protocol Buffers as a language-neutral and extensible interface definition language to define a configuration. Such configuration looks like this:

syntax = "proto3";

package example;

import "validate/validate.proto";

import "harness/wire.proto";
import "harness/http.proto";
import "harness/postgres.proto";

message Configuration {
    option (harness.service).name = "example";

    bool debug = 1;

    harness.postgres.Pool db = 2 [
        (validate.rules).message.required = true,
        (harness.wire).input.type = "harness.wires.asyncpg.PoolWire"
    ];
    harness.http.Server server = 3 [
        (validate.rules).message.required = true,
        (harness.wire).output.type = "harness.wires.aiohttp.web.ServerWire"
    ];
}

Note

It is required that your message type was named Configuration, this is a convention over configuration.

This example probably differs from what you expected to see in a .proto files because we are using message and field options:

  • (harness.service).name - message option to provide service’s name

  • (harness.wire).input - field option to describe a wire

These options are defined in the harness/wire.proto file and it is required to import this file in order to use them.

Also:

  • (validate.rules).message.required - field option to mark field as required

This option is defined in the validate/validate.proto file.

Wires Definition

You can plug wires into your service by describing them in your configuration. Wire definition look like this:

harness.http.Server server = 3 [(harness.wire).output.type = "..."];
^-----------------^             ^--------------------------------^
wire's configuration              wire's type and other options

Where:

  • harness.http.Server - is a runtime configuration format of a wire

  • (harness.wire).output.type = "..." - is a compile-time configuration of a wire, it describes which wire implementation you wish to use and other options

Note

There is a more detailed explanation of a Wires.

Runtime Configuration

Protocol Buffers has a canonical encoding into JSON, and YAML is chosen as a more human-friendly format. So even if your configuration is described as a protobuf message, this doesn’t mean that you have to deal with a binary data. Your runtime configuration is provided via YAML files.

Here is an example of a harness.http.Server configuration:

server:              # name of a wire in your Configuration
  bind:              # value for a harness.http.Server message type
    host: 0.0.0.0
    port: 8000

Validation

Protocol Buffers format provides a solid type system to describe your data, but it doesn’t include any of validation facilities. For a data validation we are using protoc-gen-validate project:

import "validate/validate.proto";

message Configuration {
    string support_email = 1 [(validate.rules).string.email = true];
}

This validation also works across different programming languages.

Harness validates your configuration when your service starts and before your service deploys. You can even validate your configurations without starting your services as an additional step in your CI/CD pipeline or using a pre-commit hooks.

$ harness check service.proto service.yaml
Validation error: host length is less than 1

Secrets

You can provide secrets for your service using JSON Merge Patch or JSON Patch formats. Secrets are applied to the main configuration and then validated as described in the previous section.

Here is how a connection to the database can be configured in a public configuration:

db:
  address:
    host: postgres.acme.svc.cluster.local
    port: 5432
  username: concierge
  database: users

Here is how a secrets merge patch looks like:

db:
  password: "really-strong-secret"