ssmconfig

package module
v0.4.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 10, 2021 License: Apache-2.0 Imports: 16 Imported by: 0

README

SSMconfig

GoDoc GitHub Actions

SSMconfig populates struct field values based on SSM parameters or arbitrary lookup functions. It supports pre-setting mutations, which is useful for things like converting values to uppercase, trimming whitespace, or looking up secrets.

Note: Versions prior to v0.2 used a different import path. This README and examples are for v0.2+.

Usage

Define a struct with fields using the ssm tag:

type MyConfig struct {
  Port     int    `ssm:"PORT"`
  Username string `ssm:"USERNAME"`
}

Set some SSM parameters:

export PORT=5555
export USERNAME=yoyo

Process it using ssmconfig:

package main

import (
  "context"
  "log"

  "github.com/sparkfly/go-ssmconfig"
)

func main() {
  ctx := context.Background()

  var c MyConfig
  if err := ssmconfig.Process(ctx, &c); err != nil {
    log.Fatal(err)
  }

  // c.Port = 5555
  // c.Username = "yoyo"
}

You can also use nested structs, just remember that any fields you want to process must be public:

type MyConfig struct {
  Database *DatabaseConfig
}

type DatabaseConfig struct {
  Port     int    `ssm:"PORT"`
  Username string `ssm:"USERNAME"`
}

Configuration

Use the ssm struct tag to define configuration.

Required

If a field is required, processing will error if the SSM parameter is unset.

type MyStruct struct {
  Port int `ssm:"PORT,required"`
}

It is invalid to have a field as both required and default.

Default

If an SSM parameter is not set, the field will be set to the default value. Note that the SSM parameter must not be set (e.g. unset PORT). If the SSM parameter is the empty string, that counts as a "value" and the default will not be used.

type MyStruct struct {
  Port int `ssm:"PORT,default=5555"`
}

You can also set the default value to another field or value from the ssmironment, for example:

type MyStruct struct {
  DefaultPort int `ssm:"DEFAULT_PORT,default=5555"`
  Port        int `ssm:"OVERRIDE_PORT,default=$DEFAULT_PORT"`
}

The value for Port will default to the value of DEFAULT_PORT.

It is invalid to have a field as both required and default.

Prefix

For shared, embedded structs, you can define a prefix to use when processing struct values for that embed.

type SharedConfig struct {
  Port int `ssm:"PORT,default=5555"`
}

type Server1 struct {
  // This processes Port from $FOO_PORT.
  *SharedConfig `ssm:",prefix=FOO_"`
}

type Server2 struct {
  // This processes Port from $BAR_PORT.
  *SharedConfig `ssm:",prefix=BAR_"`
}

It is invalid to specify a prefix on non-struct fields.

Complex Types

Durations

In SSM, time.Duration values are specified as a parsable Go duration:

type MyStruct struct {
  MyVar time.Duration `ssm:"MYVAR"`
}
export MYVAR="10m" # 10 * time.Minute
TextUnmarshaler / BinaryUnmarshaler

Types that implement TextUnmarshaler or BinaryUnmarshaler are processed as such.

json.Unmarshaler

Types that implement json.Unmarshaler are processed as such.

gob.Decoder

Types that implement gob.Decoder are processed as such.

Slices

Slices are specified as comma-separated values:

type MyStruct struct {
  MyVar []string `ssm:"MYVAR"`
}
export MYVAR="a,b,c,d" # []string{"a", "b", "c", "d"}

Note that byte slices are special cased and interpreted as strings from the ssmironment.

Maps

Maps are specified as comma-separated key:value pairs:

type MyStruct struct {
  MyVar map[string]string `ssm:"MYVAR"`
}
export MYVAR="a:b,c:d" # map[string]string{"a":"b", "c":"d"}
Structs

SSMconfig walks the entire struct, so deeply-nested fields are also supported. You can also define your own decoder (see below).

Prefixing

You can define a custom prefix using the PrefixLookuper. This will lookup values in SSM by prefixing the keys with the provided value:

type MyStruct struct {
  MyVar string `ssm:"MYVAR"`
}
// Process variables, but look for the "APP_" prefix.
l := ssmconfig.PrefixLookuper("APP_", ssmconfig.OsLookuper())
if err := ssmconfig.ProcessWith(ctx, &c, l); err != nil {
  panic(err)
}
export APP_MYVAR="foo"

Extension

All built-in types are supported except Func and Chan. If you need to define a custom decoder, implement Decoder:

type MyStruct struct {
  field string
}

func (v *MyStruct) EnvDecode(val string) error {
  v.field = fmt.Sprintf("PREFIX-%s", val)
  return nil
}

If you need to modify SSM parameter values before processing, you can specify a custom Mutator:

type Config struct {
  Password `ssm:"PASSWORD"`
}

func resolveSecretFunc(ctx context.Context, key, value string) (string, error) {
  if strings.HasPrefix(key, "secret://") {
    return secretmanager.Resolve(ctx, value) // example
  }
  return value, nil
}

var config Config
ssmconfig.ProcessWith(ctx, &config, ssmconfig.OsLookuper(), resolveSecretFunc)

Testing

Relying on SSM in tests can be troublesome because ssmironment variables are global, which makes it difficult to parallelize the tests. SSMconfig supports extracting data from anything that returns a value:

lookuper := ssmconfig.MapLookuper(map[string]string{
  "FOO": "bar",
  "ZIP": "zap",
})

var config Config
ssmconfig.ProcessWith(ctx, &config, lookuper)

Now you can parallelize all your tests by providing a map for the lookup function. In fact, that's how the tests in this repo work, so check there for an example.

You can also combine multiple lookupers with MultiLookuper. See the GoDoc for more information and examples.

Inspiration

This library is conceptually similar to kelseyhightower/ssmconfig, with the following major behavioral differences:

  • Adds support for specifying a custom lookup function (such as a map), which is useful for testing.

  • Only populates fields if they contain zero or nil values. This means you can pre-initialize a struct and any pre-populated fields will not be overwritten during processing.

  • Support for interpolation. The default value for a field can be the value of another field.

  • Support for arbitrary mutators that change/resolve data before type conversion.

Documentation

Overview

Package ssmconfig populates struct fields based on SSM parameter values (or anything that responds to "Lookup"). Structs declare their SSM dependencies using the `ssm` tag with the key being the name of the SSM parameter, case sensitive.

type MyStruct struct {
    A string `ssm:"A"` // resolves A to $A
    B string `ssm:"B,required"` // resolves B to $B, errors if $B is unset
    C string `ssm:"C,default=foo"` // resolves C to $C, defaults to "foo"

    D string `ssm:"D,required,default=foo"` // error, cannot be required and default
    E string `ssm:""` // error, must specify key
}

All built-in types are supported except Func and Chan. If you need to define a custom decoder, implement Decoder:

type MyStruct struct {
    field string
}

func (v *MyStruct) EnvDecode(val string) error {
    v.field = fmt.Sprintf("PREFIX-%s", val)
    return nil
}

In SSM, slices are specified as comma-separated values:

export MYVAR="a,b,c,d" // []string{"a", "b", "c", "d"}

In SSM, maps are specified as comma-separated key:value pairs:

export MYVAR="a:b,c:d" // map[string]string{"a":"b", "c":"d"}

If you need to modify SSM parameter values before processing, you can specify a custom mutator:

type Config struct {
    Password `ssm:"PASSWORD_SECRET"`
}

func resolveSecretFunc(ctx context.Context, key, value string) (string, error) {
    if strings.HasPrefix(key, "secret://") {
        return secretmanager.Resolve(ctx, value) // example
    }
    return value, nil
}

var config Config
ProcessWith(&config, OsLookuper(), resolveSecretFunc)

Index

Constants

View Source
const (
	ErrInvalidMapItem     = Error("invalid map item")
	ErrLookuperNil        = Error("lookuper cannot be nil")
	ErrMissingKey         = Error("missing key")
	ErrMissingRequired    = Error("missing required value")
	ErrNotPtr             = Error("input must be a pointer")
	ErrNotStruct          = Error("input must be a struct")
	ErrPrefixNotStruct    = Error("prefix is only valid on struct types")
	ErrPrivateField       = Error("cannot parse private fields")
	ErrRequiredAndDefault = Error("field cannot be required and have a default value")
	ErrUnknownOption      = Error("unknown option")
)

Variables

This section is empty.

Functions

func Process

func Process(ctx context.Context, i interface{}) error

Process processes the struct using SSM. See ProcessWith for a more customizable version.

func ProcessWith

func ProcessWith(ctx context.Context, i interface{}, l Lookuper, fns ...MutatorFunc) error

ProcessWith processes the given interface with the given lookuper. See the package-level documentation for specific examples and behaviors.

Types

type Base64Bytes

type Base64Bytes []byte

Base64Bytes is a slice of bytes where the information is base64-encoded in the SSM parameter.

func (Base64Bytes) Bytes

func (b Base64Bytes) Bytes() []byte

Bytes returns the underlying bytes.

func (*Base64Bytes) EnvDecode

func (b *Base64Bytes) EnvDecode(val string) error

EnvDecode implements ssm.Decoder.

type Decoder

type Decoder interface {
	EnvDecode(val string) error
}

Decoder is an interface that custom types/fields can implement to control how decoding takes place. For example:

type MyType string

func (mt MyType) EnvDecode(val string) error {
    return "CUSTOM-"+val
}

type Error

type Error string

Error is a custom error type for errors returned by ssmconfig.

func (Error) Error

func (e Error) Error() string

Error implements error.

type HexBytes

type HexBytes []byte

HexBytes is a slice of bytes where the information is hex-encoded in the SSM parameter.

func (HexBytes) Bytes

func (b HexBytes) Bytes() []byte

Bytes returns the underlying bytes.

func (*HexBytes) EnvDecode

func (b *HexBytes) EnvDecode(val string) error

EnvDecode implements env.Decoder.

type Lookuper

type Lookuper interface {
	// Lookup searches for the given key and returns the corresponding string
	// value. If a value is found, it returns the value and true. If a value is
	// not found, it returns the empty string and false.
	Lookup(key string) (string, error)
}

Lookuper is an interface that provides a lookup for a string-based key.

func MapLookuper

func MapLookuper(m map[string]string) Lookuper

MapLookuper looks up environment configuration from a provided map. This is useful for testing, especially in parallel, since it does not require you to mutate the parent environment (which is stateful).

func MultiLookuper

func MultiLookuper(lookupers ...Lookuper) Lookuper

MultiLookuper wraps a collection of lookupers. It does not combine them, and lookups appear in the order in which they are provided to the initializer.

func PrefixLookuper

func PrefixLookuper(prefix string, l Lookuper) Lookuper

PrefixLookuper looks up environment configuration using the specified prefix. This is useful if you want all your variables to start with a particular prefix like "MY_APP_".

func SSMLookuper

func SSMLookuper() Lookuper

SSMLookuper returns a lookuper that uses the AWS SSM API to resolve values.

type MutatorFunc

type MutatorFunc func(ctx context.Context, k, v string) (string, error)

MutatorFunc is a function that mutates a given value before it is passed along for processing. This is useful if you want to mutate SSM variable value before it's converted to the proper type.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL