Runtime Configuration

The runtimevar package provides an easy and portable way to watch runtime configuration variables. This guide shows how to work with runtime configuration variables using the GDK.

Subpackages contain driver implementations of runtimevar for various services, including Cloud and on-prem solutions. You can develop your application locally using filevar or constantvar, then deploy it to multiple Cloud providers with minimal initialization reconfiguration.

Opening a Variable🔗

The first step in watching a variable is to instantiate a portable *runtimevar.Variable for your service.

The easiest way to do so is to use runtimevar.OpenVariable and a service-specific URL pointing to the variable, making sure you “blank import” the driver package to link it in.

import (
	"github.com/sraphs/gdk/runtimevar"
	_ "github.com/sraphs/gdk/runtimevar/<driver>"
)
...
v, err := runtimevar.OpenVariable(context.Background(), "<driver-url>")
if err != nil {
    return fmt.Errorf("could not open variable: %v", err)
}
defer v.Close()
// v is a *runtimevar.Variable; see usage below
...

See Concepts: URLs for general background and the guide below for URL usage for each supported service.

Alternatively, if you need fine-grained control over the connection settings, you can call the constructor function in the driver package directly (like etcdvar.OpenVariable).

import "github.com/sraphs/gdk/runtimevar/<driver>"
...
v, err := <driver>.OpenVariable(...)
...

You may find the wire package useful for managing your initialization code when switching between different backing services.

See the guide below for constructor usage for each supported service.

When opening the variable, you can provide a decoder parameter (either as a query parameter for URLs, or explicitly to the constructor) to specify whether the raw value stored in the variable is interpreted as a string, a []byte, or as JSON. Here’s an example of using a JSON encoder:

// Config is the sample config struct we're going to parse our JSON into.
type Config struct {
	Host string
	Port int
}

// A sample JSON config that will decode into Config.
const jsonConfig = `{"Host": "github.com/sraphs/gdk", "Port": 8080}`

// Construct a Decoder that decodes raw bytes into our config.
decoder := runtimevar.NewDecoder(Config{}, runtimevar.JSONDecode)

// Next, a construct a *Variable using a constructor or URL opener.
// This example uses constantvar.
// If you're using a URL opener, you can't decode JSON into a struct, but
// you can use the query parameter "decoder=jsonmap" to decode into a map.
v := constantvar.NewBytes([]byte(jsonConfig), decoder)
defer v.Close()
// snapshot.Value will be of type Config.

Using a Variable🔗

Once you have opened a runtimevar.Variable for the provider you want, you can use it portably.

Latest🔗

The easiest way to a Variable is to use the Variable.Latest method. It returns the latest good Snapshot of the variable value, blocking if no good value has ever been detected. The dynamic type of Snapshot.Value depends on the decoder you provided when creating the Variable.

snapshot, err := v.Latest(context.Background())
if err != nil {
	log.Fatalf("Error in retrieving variable: %v", err)
}

To avoid blocking, you can pass an already-Done context. You can also use Variable.CheckHealth, which reports as healthy when Latest will return a value without blocking.

Watch🔗

Variable also has a Watch method for obtaining the value of a variable; it has different semantics than Latest and may be useful in some scenarios. We recommend starting with Latest as it’s conceptually simpler to work with.

Other Usage Samples🔗

Supported Services🔗

etcd🔗

NOTE: Support for etcd has been temporarily dropped due to dependency issues. See https://github.com/sraphs/gdk/issues/2914.

You can use runtimevar.etcd in GDK version v0.20.0 or earlier.

HTTP🔗

httpvar supports watching a variable via an HTTP request. Use runtimevar.OpenVariable with a regular URL starting with http or https. httpvar will periodically make an HTTP GET request to that URL, with the decode URL parameter removed (if present).

import (
	"context"

	"github.com/sraphs/gdk/runtimevar"
	_ "github.com/sraphs/gdk/runtimevar/httpvar"
)

// runtimevar.OpenVariable creates a *runtimevar.Variable from a URL.
// The default opener connects to an etcd server based on the environment
// variable ETCD_SERVER_URL.

v, err := runtimevar.OpenVariable(ctx, "http://myserver.com/foo.txt?decoder=string")
if err != nil {
	return err
}
defer v.Close()

HTTP Constructor🔗

The httpvar.OpenVariable constructor opens a variable with a http.Client and a URL.

import (
	"net/http"

	"github.com/sraphs/gdk/runtimevar"
	"github.com/sraphs/gdk/runtimevar/httpvar"
)

// Create an HTTP.Client
httpClient := http.DefaultClient

// Construct a *runtimevar.Variable that watches the page.
v, err := httpvar.OpenVariable(httpClient, "http://example.com", runtimevar.StringDecoder, nil)
if err != nil {
	return err
}
defer v.Close()

Blob🔗

blobvar supports watching a variable based on the contents of a GDK blob. Set the environment variable BLOBVAR_BUCKET_URL to the URL of the bucket, and then use runtimevar.OpenVariable as shown below. blobvar will periodically re-fetch the contents of the blob.

import (
	"context"

	"github.com/sraphs/gdk/runtimevar"
	_ "github.com/sraphs/gdk/runtimevar/blobvar"
)

// runtimevar.OpenVariable creates a *runtimevar.Variable from a URL.
// The default opener opens a blob.Bucket via a URL, based on the environment
// variable BLOBVAR_BUCKET_URL.

v, err := runtimevar.OpenVariable(ctx, "blob://myvar.txt?decoder=string")
if err != nil {
	return err
}
defer v.Close()

You can also use blobvar.OpenVariable.

Local🔗

You can create an in-memory variable (useful for testing) using constantvar:

import (
	"context"
	"fmt"

	"github.com/sraphs/gdk/runtimevar"
	_ "github.com/sraphs/gdk/runtimevar/constantvar"
)

// runtimevar.OpenVariable creates a *runtimevar.Variable from a URL.

v, err := runtimevar.OpenVariable(ctx, "constant://?val=hello+world&decoder=string")
if err != nil {
	return err
}
defer v.Close()

Alternatively, you can create a variable based on the contents of a file using filevar:

import (
	"context"

	"github.com/sraphs/gdk/runtimevar"
	_ "github.com/sraphs/gdk/runtimevar/filevar"
)

// runtimevar.OpenVariable creates a *runtimevar.Variable from a URL.

v, err := runtimevar.OpenVariable(ctx, "file:///path/to/config.txt?decoder=string")
if err != nil {
	return err
}
defer v.Close()