Docs / Go SDK

Go SDK

Read variables that change while your process runs, and rebuild the things made from them, without a restart.

Install

sh
go get github.com/hoot-codes/dynv/sdk/go

How it works

The same API runs in one of three modes. Connect picks one for you:

ModeWhenWhat happens
agentunder dynv run (DYNV_AGENT_URL is set)The SDK talks to the small agent dynv run starts next to your app, over loopback, with a per-run key. The agent has already verified and decrypted the data. Your app holds no token and does no cryptography. Preferred.
directno dynv run, a service token in DYNV_TOKEN or DYNV_TOKEN_FILEThe SDK does what dynv run does, in your process: fetch, verify, decrypt, merge shared groups, resolve templates, watch and cache. See Without the CLI.
envneither (local development, tests, a migration in progress)Get and Lookup read the process environment and no change events fire, so the same code works everywhere.

Read variables

Mark the variables you want live as hot, start your app with dynv run -- ./app, then call Connect. It waits for the first snapshot and returns a client whose methods are safe for concurrent use:

go
proxy := d.Get("PROXY_SERVER")              // "" when unset
value, ok := d.Lookup("PROXY_SERVER")       // current value and whether it is set
dsn, err := d.Require("DATABASE_URL")       // an error naming the key when unset

version := d.Version()                      // local revision, increasing on every change
mode := d.Mode()                            // dynv.ModeAgent, ModeDirect or ModeEnv
source := d.Source("DB_HOST")               // shared group name, or ""
isTemplate := d.IsTemplate("DATABASE_URL") // Get already returns the resolved value
unresolved := d.Unresolved()                // template key names that do not resolve now
  • Lookup falls back to the process environment only for a key Dynv has never managed. Once Dynv has managed a key, removing it makes it read as unset, never as a stale inherited value.
  • A template that stops resolving keeps its last good value, or stays unset if it never had one. Unresolved returns key names only.
  • Read on every use. Do not save d.Get(...) in a package-level variable, or you will pin the old value.

Without the CLI: direct mode

Can't put the dynv binary in your image, or can't wrap the start command? Give the app a service token instead and call Connect. The server still sends only ciphertext. The SDK verifies and decrypts it in your process, exactly as dynv run would:

  • it checks every signature against the signers named inside the sealed data key, and merges the shared groups attached to your service;
  • it watches for changes and resolves templates such as postgres://${DB_USER}@${DB_HOST}/app against your service's variables;
  • it keeps an encrypted cache plus the newest version it has accepted, so it starts when Dynv is down and refuses a rolled-back or forked document, even after a restart.

Set DYNV_URL explicitly in direct mode. Use https://api.dynv.dev for Dynv Cloud.

sh
export DYNV_URL=https://api.dynv.dev
export DYNV_PROCESS=web
export DYNV_TOKEN=dynv_pt_…                      # a service token (or DYNV_TOKEN_FILE=/run/secrets/dynv)
export DYNV_CACHE=/var/lib/web/dynv-cache.json   # encrypted cache: boots when Dynv is down, refuses rollbacks

./app                                            # no dynv run
main.go
package main

import (
	"context"
	"fmt"
	"log"
	"os"
	"os/signal"
	"syscall"

	dynv "github.com/hoot-codes/dynv/sdk/go"
)

func main() {
	// Reads DYNV_URL, DYNV_TOKEN (or DYNV_TOKEN_FILE), DYNV_PROCESS and DYNV_CACHE.
	// Under `dynv run` the same call uses the local agent instead.
	d, err := dynv.Connect(context.Background(), dynv.Options{})
	if err != nil {
		log.Fatal(err)
	}
	defer d.Close()
	fmt.Printf("connected in %s mode, version %d\n", d.Mode(), d.Version())

	if _, err := d.Require("PROXY_SERVER"); err != nil {
		log.Fatal(err)
	}

	d.OnKey("PROXY_SERVER", func(value string, ok bool) {
		// value is the new verified value; ok is false when the key was removed.
		fmt.Println("PROXY_SERVER changed")
	})
	d.OnChange(func(e dynv.ChangeEvent) {
		// Changed contains key names only, never values.
		fmt.Printf("changed: %v (restart: %t)\n", e.Changed, e.Restart)
	})

	sig := make(chan os.Signal, 1)
	signal.Notify(sig, os.Interrupt, syscall.SIGTERM)
	<-sig
}

By default, values are also mirrored into the process environment: every value at startup, then only hot changes to variables that don't control code loading (PATH, LD_PRELOAD, …). Restart-mode and code-loading changes update Get and events but reach the environment only on the next start. Removed variables are deleted. With cgo, set DisableEnvMirror: os.Setenv racing C code's getenv is a data race in libc, and C libraries may cache values.

Restart-mode variables

Nobody supervises your process in direct mode, so the SDK can't restart it. Every change is applied live. When a changed variable is in restart mode, ChangeEvent.Restart is true. Set ExitOnRestart to have the SDK send your process SIGTERM, so your platform restarts it with the new values.

Options

go
package main

import (
	"context"
	"fmt"
	"log"
	"os"
	"time"

	dynv "github.com/hoot-codes/dynv/sdk/go"
)

func main() {
	d, err := dynv.Connect(context.Background(), dynv.Options{
		Mode:              dynv.ModeAuto,         // agent under dynv run, else direct with a token, else env
		Token:             "",                    // default: DYNV_TOKEN, or the file in DYNV_TOKEN_FILE
		Process:           "",                    // default: DYNV_PROCESS; a token for another service is refused
		URL:               os.Getenv("DYNV_URL"), // required in direct mode; https://api.dynv.dev for Dynv Cloud
		Cache:             "",                    // default: DYNV_CACHE; the same file format as dynv run --cache
		Require:           false,                 // true: fail if Dynv is unreachable and there is no cache
		BootTimeout:       10 * time.Second,      // first fetch limit before falling back to the cache
		PollInterval:      120 * time.Second,     // poll interval with DisableLongPoll; longest error backoff
		DisableLongPoll:   false,                 // true: poll instead (networks that drop idle connections)
		AllowInsecureHTTP: false,                 // default also honors DYNV_ALLOW_INSECURE_HTTP=1
		KeepTokenInEnv:    false,                 // by default DYNV_TOKEN is unset once read
		ExitOnRestart:     false,                 // true: SIGTERM yourself when a restart-mode variable changes
		DisableEnvMirror:  false,                 // by default values are mirrored into the process environment
		Logger:            nil,                   // default: stderr; key names and codes only, never values
		OnError:           nil,                   // optional callback; also sees errors during Connect
		AgentURL:          "",                    // default: DYNV_AGENT_URL
		AgentKey:          "",                    // default: DYNV_AGENT_KEY
		HTTPClient:        nil,                   // default HTTP client; redirects are never followed
	})
	if err != nil {
		log.Fatal(err)
	}
	defer d.Close()
	fmt.Printf("connected in %s mode\n", d.Mode())
}

React to changes

go
cancelChange := d.OnChange(func(e dynv.ChangeEvent) {
    // e.Changed lists key names only; e.Restart marks a restart-mode change.
    log.Printf("dynv v%d changed %v", e.Version, e.Changed)
})
cancelKey := d.OnKey("PROXY_SERVER", func(value string, ok bool) {
    // ok is false when the variable was removed.
    rebuildProxy(value, ok)
})
cancelError := d.OnError(func(err error) {
    log.Printf("dynv: %v", err)
})

// later: cancelChange(); cancelKey(); cancelError(); d.Close()

Each registration returns a cancel function. The first snapshot is the starting point, not a change. Callbacks run on the watcher goroutine; keep them short and hand off blocking work. A panic in a callback is recovered and logged. Errors and refused updates go to OnError; the SDK keeps the last good values and retries.

Using another runtime? See the Node / TypeScript SDK, the Python SDK, or the protocol and test vectors.