skyconf

package module
v1.3.4 Latest Latest
Warning

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

Go to latest
Published: Oct 27, 2025 License: Apache-2.0 Imports: 14 Imported by: 0

README

Ský Config for Go

Go Reference

Ský Config is a configuration library for Go. It can be used to load configuration values from cloud based parameter stores such as AWS SSM Parameter Store.

Ský, why?

Ský /skiː/ is the Danish word for cloud, which is where the configuration values are stored.

How to use

See the example for how to use the library.

Working with SSM Parameter Hierarchies

AWS Systems Manager (SSM) Parameter Store allows organizing parameters into hierarchies using forward slashes (/) in parameter names. This is a powerful feature for managing configuration for different environments (e.g., /dev, /staging, /prod) or different components of an application.

For example, you could structure your parameters like this:

/my-project/common/database_url
/my-project/common/database_port
/my-project/my-component/service_endpoint
/my-project/my-component/timeout

go-skyconf makes it easy to work with such hierarchical configurations. When you process a struct, go-skyconf will fetch parameters from the path prefix you provide.

Overriding the root path with source

A component often needs its own configuration as well as shared configuration from a higher level in the hierarchy. go-skyconf supports this by using the source tag on a struct field to refer to a named sky.Source.

First, you define named sources when you initialize go-skyconf. For example, you might have a "project" source for shared parameters and an "app" source for application-specific parameters.

// In your setup code:
projectSource := sky.SSMSourceWithID(ssmClient, "/my-project/", "project")
appSource := sky.SSMSourceWithID(ssmClient, "/my-project/apps/my-app/", "app")

// ... then later when parsing configuration
sky.Parse(ctx, &cfg, false, projectSource, appSource)

Then, in your configuration struct, you can use the source tag to specify which named source to use for a particular field or nested struct.

// Config contains settings for the application.
type Config struct {
	// This struct will be populated from the "project" source,
	// using the path "/my-project/".
	Database sqldb.Config `sky:"database/main_db,source:project"`

	// This struct will be populated from the "app" source,
	// using the path "/my-project/apps/my-app/".
	Finder FinderConfig `sky:",flatten,source:app"`

	// This field will be populated from the first source that contains it,
	// as no source is specified.
	LogLevel string `conf:"default:info"`
}

When sky.Parse is called on a Config struct:

  1. It will populate the Database field by looking for parameters under the path /my-project/database/main_db/ in SSM, because it's using the project source.
  2. It will populate the fields of the Finder struct by looking for parameters under /my-project/apps/my-app/, because it's using the app source.
  3. It will populate LogLevel from any of the provided sources, or use the default.

This allows different parts of your application to be configured from different parameter hierarchies, promoting separation of concerns and reusability of configuration.

For a more detailed example of using multiple sources, see the Example (MultipleSources) section below.

Acknowledgements

This library is inspired by the ardanlabs/conf library by Ardan Labs.

License

This project is licensed under the Apache License v2.0 - see the LICENSE file for details.

Copyright 2024, Red Matter Ltd.

Documentation

Overview

Example

Example demonstrates how to use go-skyconf to parse configuration from AWS SSM.

This example assumes you have a working AWS session and SSM client. It also assumes that the following parameters exist in AWS SSM under the path /myapplication/:

  • app/name: "MyApp"
  • app/version: "1.2.3"
  • db/host: "localhost"
  • db/port: "5432"
  • log_level: "info"
package main

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

	"github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/service/ssm"

	sky "github.com/redmatter/go-skyconf"
)

func main() {
	// 1. Define your configuration struct.
	// Use struct tags to control how fields are populated from SSM.
	type Config struct {
		App struct {
			Name    string `sky:"name"`
			Version string `sky:"version"`
		} `sky:"app"`
		Database struct {
			Host string `sky:"host"`
			Port int    `sky:"port"`
		} `sky:"db"`
		LogLevel string `sky:"log_level,optional"`
	}

	// 2. Set up a real SSM client from the AWS SDK.
	// In a real application, you would load the default AWS config.
	awsCfg, err := config.LoadDefaultConfig(context.TODO())
	if err != nil {
		log.Printf("unable to load SDK config, %v; skipping example", err)
		return
	}
	ssmClient := ssm.NewFromConfig(awsCfg)

	// 3. Define the SSM source for your configuration.
	// This tells go-skyconf where to look for parameters.
	ssmSource := sky.SSMSource(ssmClient, "/myapplication/")

	// 4. Create an instance of your config struct and parse the configuration.
	var cfg Config
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	_, err = sky.Parse(ctx, &cfg, false, ssmSource)
	if err != nil {
		// This will fail if the parameters don't exist in SSM.
		log.Printf("failed to parse configuration: %v; skipping example", err)
		return
	}

	// 5. Your config struct is now populated.
	fmt.Printf("AppName: %s\n", cfg.App.Name)
	fmt.Printf("AppVersion: %s\n", cfg.App.Version)
	fmt.Printf("DB Host: %s\n", cfg.Database.Host)
	fmt.Printf("DB Port: %d\n", cfg.Database.Port)
	fmt.Printf("Log Level: %s\n", cfg.LogLevel)

}
Output:
AppName: MyApp
AppVersion: 1.2.3
DB Host: localhost
DB Port: 5432
Log Level: info
Example (MultipleSources)

Example demonstrates how to use go-skyconf to parse configuration from multiple hierarchical sources in AWS SSM.

This example assumes a working AWS session and that the following parameters exist in AWS SSM:

Project-level parameters under /my-project/:

  • db/host: "project-db.example.com"
  • db/port: "5432"

App-level parameters under /my-project/apps/my-app/:

  • log_level: "debug"
  • app_name: "MyAwesomeApp"
package main

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

	"github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/service/ssm"

	sky "github.com/redmatter/go-skyconf"
)

func main() {
	// 1. Define your configuration struct with source tags.
	type Config struct {
		Database struct {
			Host string `sky:"host"`
			Port int    `sky:"port"`
		} `sky:"db,source:project"` // From project source
		App struct {
			LogLevel string `sky:"log_level"`
		} `sky:",flatten,source:app"` // From app source
		AppName string `sky:"app_name,source:app"` // From app source
	}

	// 2. Set up a real SSM client.
	awsCfg, err := config.LoadDefaultConfig(context.TODO())
	if err != nil {
		log.Printf("unable to load SDK config, %v; skipping example", err)
		return
	}
	ssmClient := ssm.NewFromConfig(awsCfg)

	// 3. Define multiple SSM sources with unique IDs.
	projectSource := sky.SSMSourceWithID(ssmClient, "/my-project/", "project")
	appSource := sky.SSMSourceWithID(ssmClient, "/my-project/apps/my-app/", "app")

	// 4. Parse the configuration using both sources.
	var cfg Config
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	_, err = sky.Parse(ctx, &cfg, false, projectSource, appSource)
	if err != nil {
		log.Printf("failed to parse configuration: %v; skipping example", err)
		return
	}

	// 5. Your config struct is now populated from multiple sources.
	fmt.Printf("DB Host: %s\n", cfg.Database.Host)
	fmt.Printf("DB Port: %d\n", cfg.Database.Port)
	fmt.Printf("Log Level: %s\n", cfg.App.LogLevel)
	fmt.Printf("AppName: %s\n", cfg.AppName)

}
Output:
DB Host: project-db.example.com
DB Port: 5432
Log Level: debug
AppName: MyAwesomeApp

Index

Examples

Constants

This section is empty.

Variables

View Source
var ErrBadDefaultFieldValue = errors.New("failed to set default value for field")

ErrBadDefaultFieldValue is returned when a specified default value could not be set for a field.

View Source
var ErrBadFieldValue = errors.New("failed to set value for field")

ErrBadFieldValue is returned when a value could not be set for a field.

View Source
var ErrBadTags = errors.New("error parsing tags for field")
View Source
var ErrGetParameters = errors.New("failed to get parameters")

ErrGetParameters is returned when no parameters could not be fetched from a source.

View Source
var ErrInvalidStruct = errors.New("config must be a pointer to a struct")
View Source
var ErrMissingKeyOnRefresh = errors.New("missing key on refresh")
View Source
var ErrNoSource = errors.New("no sources provided")

ErrNoSource is returned when no sources are provided to the Parse function.

View Source
var ErrParameterNotFound = errors.New("parameter not found in source")

ErrParameterNotFound is returned when a parameter is not found in the source.

View Source
var ErrSourceNotFound = errors.New("source not found")

ErrSourceNotFound is returned when a specified source was not found in the list of sources.

View Source
var ErrSourceNotRefreshable = errors.New("source is not refreshable")

Functions

func String

func String(cfg interface{}, withUntagged bool, withCurrentValue bool, sources ...Source) (str string, err error)

String returns a string representation of the provided configuration struct, describing source and parameter name for each field. If withCurrentValue is true, the current value of the field is also included.

func ToSnakeCase

func ToSnakeCase(str string) string

Types

type Refresher added in v1.1.0

type Refresher interface {
	// Refresh starts a new goroutine that updates the configuration at specified intervals until the context is
	// cancelled. It returns a channel that sends updated field IDs. If an error occurs, the provided error function is
	// called. If no error function is provided, the error is ignored.
	Refresh(ctx context.Context, ef func(err error)) <-chan string
	// RefreshOnce refreshes the configuration once, returning the first error that occurs.
	RefreshOnce(ctx context.Context) (err error)
}

Refresher refreshes configuration at specified intervals.

Example
package main

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

	"github.com/aws/aws-sdk-go-v2/service/ssm"

	sky "github.com/redmatter/go-skyconf"
)

func main() {
	// This example is for documentation purposes and is not runnable as a standalone test
	// as it requires a pre-configured SSM client and parameters in AWS SSM.
	// For a more complete example, see Example().

	// Initialise SSM client using aws-sdk-go-v2
	var ssmClient *ssm.Client

	// Create a timeout context
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)

	cfg := struct {
		LogLevel string
		DB       struct {
			Host     string `sky:"host"`
			Port     int    `sky:"port"`
			Username string `sky:"username"`
			Password string `sky:"password"`
		} `sky:",flatten"`
		EnabledProductIDs string `sky:"enabled_list,refresh=5m,id=enabled_product_list"`
	}{}

	// Parse database configuration from SSM
	refresher, err := sky.Parse(ctx, &cfg, false, sky.SSMSource(ssmClient, os.Getenv("APP_SSM_PATH")+"database/"))
	if err != nil {
		log.Fatalln("failed to parse configuration:", err)
	}

	// Refresh the configuration according to the timings specified in the struct tags.
	// In this case, the `enabled_list` field will be refreshed every 5 minutes.
	updates := refresher.Refresh(ctx, func(err error) {
		if err != nil {
			log.Println("error refreshing configuration:", err)
		}
	})

	go func() {
		for update := range updates {
			log.Println("updated field:", update)
			switch update {
			case "enabled_product_list": // The ID specified in the struct tag
				// Do something with the updated field
			default:
				// This should never happen unless a new field is added.
				log.Fatalln("unknown field updated:", update)
			}
		}
	}()

	// Use the configuration

	// When done, cancel the context to stop the refresh
	cancel()
}

func Parse

func Parse(ctx context.Context, cfg interface{}, withUntagged bool, sources ...Source) (r Refresher, err error)

Parse fetches configuration from the provided sources into the given struct. If a source is specified for a field, its value is queried only from that source. Otherwise, all sources are queried in order, with the last source's value taking precedence. Fields not tagged with `sky` are ignored unless `withUntagged` is true. Returns a Refresher for automatic configuration refresh.

The configuration struct must have fields tagged with `sky` and the following tags. All tags are optional.

  • default: sets the default value for the field.
  • optional: marks the field as optional, suppressing errors if the field is not found in the source.
  • flatten: flattens the field thereby ignoring the key of the outer struct.
  • source: specifies the source for the field.
  • refresh: sets the refresh duration for the field; duration must be in Go time.Duration format and greater than 0.
  • id: sets the identifier for the field, used for update notifications.
Example

ExampleParse demonstrates how to use the Parse function to parse configuration from AWS SSM.

package main

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

	"github.com/aws/aws-sdk-go-v2/service/ssm"

	sky "github.com/redmatter/go-skyconf"
)

func main() {
	// This example is for documentation purposes and is not runnable as a standalone test
	// as it requires a pre-configured SSM client and parameters in AWS SSM.
	// For a more complete example, see Example().

	// Initialise SSM client using aws-sdk-go-v2
	var ssmClient *ssm.Client

	// Create a timeout context
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	cfg := struct {
		LogLevel string
		DB       struct {
			Host     string `sky:"host"`
			Port     int    `sky:"port"`
			Username string `sky:"username"`
			Password string `sky:"password"`
		} `sky:",flatten"`
	}{}

	// Parse database configuration from SSM
	_, err := sky.Parse(ctx, &cfg, false, sky.SSMSource(ssmClient, os.Getenv("APP_SSM_PATH")+"database/"))
	if err != nil {
		log.Fatalln("failed to parse configuration:", err)
	}

	// Use the configuration
}

func ParseSSM

func ParseSSM(ctx context.Context, ssm *ssmpkg.Client, path string, cfg interface{}) (r Refresher, err error)

ParseSSM retrieves configuration from AWS SSM and populates the provided struct. It is a convenience function for Parse with a single SSM source named "ssm".

type Setter

type Setter interface {
	Set(value string) error
}

Setter is implemented by types can self-deserialize values.

type Source

type Source interface {
	// Source fetches the parameters from the source.
	Source(ctx context.Context, params []string) (values map[string]string, err error)
	// ParameterName formats the parameter name.
	ParameterName(parts []string) string
	// Refreshable returns true if the source is refreshable.
	Refreshable() bool
	// ID returns the ID of the source.
	ID() string
}

Source can format a parameter name and fetch a set of parameters from a source.

func SSMSource

func SSMSource(ssm *ssmpkg.Client, path string) Source

SSMSource creates a new SSM source.

func SSMSourceWithID

func SSMSourceWithID(ssm *ssmpkg.Client, path, id string) Source

SSMSourceWithID creates a new SSM source with a custom ID.

Jump to

Keyboard shortcuts

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