Skip to content

Commit

Permalink
add provider/appconfig
Browse files Browse the repository at this point in the history
  • Loading branch information
ktong committed Feb 4, 2024
1 parent a9a948a commit 92544d0
Show file tree
Hide file tree
Showing 14 changed files with 313 additions and 5 deletions.
7 changes: 7 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ updates:
schedule:
interval: daily

- package-ecosystem: gomod
directory: /provider/appconfig
labels:
- Skip-Changelog
schedule:
interval: daily

- package-ecosystem: github-actions
directory: /
labels:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/benchmark.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
benchmark:
strategy:
matrix:
module: [ '', 'provider/file', 'provider/pflag' ]
module: [ '', 'provider/file', 'provider/pflag', 'provider/appconfig' ]
name: Benchmark
runs-on: ubuntu-latest
steps:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
coverage:
strategy:
matrix:
module: [ '', 'provider/file', 'provider/pflag' ]
module: [ '', 'provider/file', 'provider/pflag', 'provider/appconfig' ]
name: Coverage
runs-on: ubuntu-latest
steps:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
lint:
strategy:
matrix:
module: [ '', 'provider/file', 'provider/pflag' ]
module: [ '', 'provider/file', 'provider/pflag', 'provider/appconfig' ]
name: Lint
runs-on: ubuntu-latest
steps:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/tag.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
tag:
strategy:
matrix:
module: [ 'provider/file', 'provider/pflag' ]
module: [ 'provider/file', 'provider/pflag', 'provider/appconfig' ]
name: Submodules
runs-on: ubuntu-latest
steps:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
test:
strategy:
matrix:
module: [ '', 'provider/file', 'provider/pflag' ]
module: [ '', 'provider/file', 'provider/pflag', 'provider/appconfig' ]
run-on: [ 'ubuntu', 'macOS', 'windows' ]
go-version: [ 'stable' ]
name: Test
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

- add Config.Explain to provide information about how Config resolve each value from loaders (#78).
- add Default to get the default Config (#81).
- add AWS AppConfig Loader (#92).

### Changed

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ There are providers for the following configuration sources:
- [`file`](provider/file) loads configuration from a file.
- [`flag`](provider/flag) loads configuration from flags.
- [`pflag`](provider/pflag) loads configuration from [spf13/pflag](https://github.com/spf13/pflag).
- [`appconfig`](provider/appconfig) loads configuration from [AWS AppConfig](https://aws.amazon.com/systems-manager/features/appconfig/).

## Inspiration

Expand Down
178 changes: 178 additions & 0 deletions provider/appconfig/appconfig.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
// Copyright (c) 2024 The konf authors
// Use of this source code is governed by a MIT license found in the LICENSE file.

// Package appconfig loads configuration from AWS AppConfig.
//
// AppConfig loads configuration from AWS AppConfig with the given application, environment, profile
// and returns a nested map[string]any that is parsed with the given unmarshal function.
//
// The unmarshal function must be able to unmarshal the file content into a map[string]any.
// For example, with the default json.Unmarshal, the file is parsed as JSON.
package appconfig

import (
"context"
"encoding/json"
"fmt"
"log/slog"
"sync/atomic"
"time"

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

// AppConfig is a Provider that loads configuration from AWS AppConfig.
//
// To create a new AppConfig, call [New].
type AppConfig struct {
logger *slog.Logger
unmarshal func([]byte, any) error

client appConfigClient
application string
environment string
profile string
pollInterval time.Duration

token atomic.Pointer[string]
}

type appConfigClient interface {
StartConfigurationSession(
ctx context.Context,
params *appconfigdata.StartConfigurationSessionInput,
optFns ...func(*appconfigdata.Options),
) (*appconfigdata.StartConfigurationSessionOutput, error)
GetLatestConfiguration(
ctx context.Context,
params *appconfigdata.GetLatestConfigurationInput,
optFns ...func(*appconfigdata.Options),
) (*appconfigdata.GetLatestConfigurationOutput, error)
}

// New creates an AppConfig with the given application, environment, profile and Option(s).
func New(application, environment, profile string, opts ...Option) *AppConfig {
if application == "" {
panic("cannot create AppConfig with empty application")
}
if environment == "" {
panic("cannot create AppConfig with empty environment")
}
if profile == "" {
panic("cannot create AppConfig with empty profile")
}

option := &options{
AppConfig: AppConfig{
application: application,
environment: environment,
profile: profile,
},
}
for _, opt := range opts {
opt(option)
}
if option.logger == nil {
option.logger = slog.Default()
}
if option.unmarshal == nil {
option.unmarshal = json.Unmarshal
}
if option.pollInterval <= 0 {
option.pollInterval = time.Minute
}
if option.awsConfig == nil {
awsConfig, err := config.LoadDefaultConfig(context.Background())
if err != nil {
panic(fmt.Sprintf("cannot load AWS default config: %v", err))
}
option.awsConfig = &awsConfig
}
option.AppConfig.client = appconfigdata.NewFromConfig(*option.awsConfig)

return &option.AppConfig
}

func (a *AppConfig) Load() (map[string]any, error) {
if a.token.Load() == nil {
input := &appconfigdata.StartConfigurationSessionInput{
ApplicationIdentifier: aws.String(a.application),
ConfigurationProfileIdentifier: aws.String(a.profile),
EnvironmentIdentifier: aws.String(a.environment),
RequiredMinimumPollIntervalInSeconds: aws.Int32(int32(max(a.pollInterval.Seconds()/2, 1))), //nolint:gomnd
}
output, err := a.client.StartConfigurationSession(context.Background(), input)
if err != nil {
return nil, fmt.Errorf("start configuration session: %w", err)
}
a.token.Store(output.InitialConfigurationToken)
}

input := &appconfigdata.GetLatestConfigurationInput{
ConfigurationToken: a.token.Load(),
}
output, err := a.client.GetLatestConfiguration(context.Background(), input)
if err != nil {
return nil, fmt.Errorf("get latest configuration: %w", err)
}
a.token.Store(output.NextPollConfigurationToken)

var out map[string]any
if err := a.unmarshal(output.Configuration, &out); err != nil {
return nil, fmt.Errorf("unmarshal: %w", err)
}

return out, nil
}

func (a *AppConfig) Watch(ctx context.Context, onChange func(map[string]any)) error {
ticker := time.NewTicker(a.pollInterval)
defer ticker.Stop()

for {
select {
case <-ticker.C:
input := &appconfigdata.GetLatestConfigurationInput{
ConfigurationToken: a.token.Load(),
}
output, err := a.client.GetLatestConfiguration(ctx, input)
if err != nil {
a.logger.WarnContext(
ctx, "Error when reloading from AWS AppConfig",
"application", a.application,
"environment", a.environment,
"profile", a.profile,
"error", err,
)

continue
}
a.token.Store(output.NextPollConfigurationToken)

if len(output.Configuration) == 0 {
// It may return empty configuration data
// if the client already has the latest version.
continue
}

var out map[string]any
if err := a.unmarshal(output.Configuration, &out); err != nil {
a.logger.WarnContext(
ctx, "Error when unmarshalling config from AWS AppConfig",
"application", a.application,
"environment", a.environment,
"profile", a.profile,
"error", err,
)

continue
}

onChange(out)
case <-ctx.Done():
return nil
}
}
}
4 changes: 4 additions & 0 deletions provider/appconfig/appconfig_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Copyright (c) 2024 The konf authors
// Use of this source code is governed by a MIT license found in the LICENSE file.

package appconfig
4 changes: 4 additions & 0 deletions provider/appconfig/benchmark_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Copyright (c) 2024 The konf authors
// Use of this source code is governed by a MIT license found in the LICENSE file.

package appconfig
23 changes: 23 additions & 0 deletions provider/appconfig/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
module github.com/nil-go/konf/provider/appconfig

go 1.21

require (
github.com/aws/aws-sdk-go-v2 v1.24.1
github.com/aws/aws-sdk-go-v2/config v1.26.6
github.com/aws/aws-sdk-go-v2/service/appconfigdata v1.12.0
)

require (
github.com/aws/aws-sdk-go-v2/credentials v1.16.16 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.3 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.18.7 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 // indirect
github.com/aws/smithy-go v1.19.0 // indirect
)
30 changes: 30 additions & 0 deletions provider/appconfig/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
github.com/aws/aws-sdk-go-v2 v1.24.1 h1:xAojnj+ktS95YZlDf0zxWBkbFtymPeDP+rvUQIH3uAU=
github.com/aws/aws-sdk-go-v2 v1.24.1/go.mod h1:LNh45Br1YAkEKaAqvmE1m8FUx6a5b/V0oAKV7of29b4=
github.com/aws/aws-sdk-go-v2/config v1.26.6 h1:Z/7w9bUqlRI0FFQpetVuFYEsjzE3h7fpU6HuGmfPL/o=
github.com/aws/aws-sdk-go-v2/config v1.26.6/go.mod h1:uKU6cnDmYCvJ+pxO9S4cWDb2yWWIH5hra+32hVh1MI4=
github.com/aws/aws-sdk-go-v2/credentials v1.16.16 h1:8q6Rliyv0aUFAVtzaldUEcS+T5gbadPbWdV1WcAddK8=
github.com/aws/aws-sdk-go-v2/credentials v1.16.16/go.mod h1:UHVZrdUsv63hPXFo1H7c5fEneoVo9UXiz36QG1GEPi0=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 h1:c5I5iH+DZcH3xOIMlz3/tCKJDaHFwYEmxvlh2fAcFo8=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11/go.mod h1:cRrYDYAMUohBJUtUnOhydaMHtiK/1NZ0Otc9lIb6O0Y=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 h1:vF+Zgd9s+H4vOXd5BMaPWykta2a6Ih0AKLq/X6NYKn4=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10/go.mod h1:6BkRjejp/GR4411UGqkX8+wFMbFbqsUIimfK4XjOKR4=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10 h1:nYPe006ktcqUji8S2mqXf9c/7NdiKriOwMvWQHgYztw=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10/go.mod h1:6UV4SZkVvmODfXKql4LCbaZUpF7HO2BX38FgBf9ZOLw=
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.3 h1:n3GDfwqF2tzEkXlv5cuy4iy7LpKDtqDMcNLfZDu9rls=
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.3/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY=
github.com/aws/aws-sdk-go-v2/service/appconfigdata v1.12.0 h1:MkRVTMyOWO4ZkLBLMDQHun98FYaPMkSYN91r6SkYsPw=
github.com/aws/aws-sdk-go-v2/service/appconfigdata v1.12.0/go.mod h1:bEPSlURhZxm6uNx1GAAwKHjqsCm6GHrf13qXzoh/2A8=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 h1:/b31bi3YVNlkzkBrm9LfpaKoaYZUxIAj4sHfOTmLfqw=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4/go.mod h1:2aGXHFmbInwgP9ZfpmdIfOELL79zhdNYNmReK8qDfdQ=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10 h1:DBYTXwIGQSGs9w4jKm60F5dmCQ3EEruxdc0MFh+3EY4=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10/go.mod h1:wohMUQiFdzo0NtxbBg0mSRGZ4vL3n0dKjLTINdcIino=
github.com/aws/aws-sdk-go-v2/service/sso v1.18.7 h1:eajuO3nykDPdYicLlP3AGgOyVN3MOlFmZv7WGTuJPow=
github.com/aws/aws-sdk-go-v2/service/sso v1.18.7/go.mod h1:+mJNDdF+qiUlNKNC3fxn74WWNN+sOiGOEImje+3ScPM=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7 h1:QPMJf+Jw8E1l7zqhZmMlFw6w1NmfkfiSK8mS4zOx3BA=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7/go.mod h1:ykf3COxYI0UJmxcfcxcVuz7b6uADi1FkiUz6Eb7AgM8=
github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 h1:NzO4Vrau795RkUdSHKEwiR01FaGzGOH1EETJ+5QHnm0=
github.com/aws/aws-sdk-go-v2/service/sts v1.26.7/go.mod h1:6h2YuIoxaMSCFf5fi1EgZAwdfkGMgDY+DVfa61uLe4U=
github.com/aws/smithy-go v1.19.0 h1:KWFKQV80DpP3vJrrA9sVAHQ5gc2z8i4EzrLhLlWXcBM=
github.com/aws/smithy-go v1.19.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
60 changes: 60 additions & 0 deletions provider/appconfig/option.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright (c) 2024 The konf authors
// Use of this source code is governed by a MIT license found in the LICENSE file.

package appconfig

import (
"log/slog"
"time"

"github.com/aws/aws-sdk-go-v2/aws"
)

// WithAWSConfig provides the AWS Config for the AWS SDK.
//
// By default, it loads the default AWS Config.
func WithAWSConfig(awsConfig *aws.Config) Option {
return func(options *options) {
options.awsConfig = awsConfig
}
}

// WithPollInterval provides the interval for polling the configuration.
//
// The default interval is 1 minute.
func WithPollInterval(pollInterval time.Duration) Option {
return func(options *options) {
options.pollInterval = pollInterval
}
}

// WithUnmarshal provides the function used to parses the configuration file.
// The unmarshal function must be able to unmarshal the file content into a map[string]any.
//
// The default function is json.Unmarshal.
func WithUnmarshal(unmarshal func([]byte, any) error) Option {
return func(options *options) {
options.unmarshal = unmarshal
}
}

// WithLogHandler provides the slog.Handler for logs from watch.
//
// By default, it uses handler from slog.Default().
func WithLogHandler(handler slog.Handler) Option {
return func(options *options) {
if handler != nil {
options.logger = slog.New(handler)
}
}
}

type (
// Option configures the a File with specific options.
Option func(options *options)
options struct {
AppConfig

awsConfig *aws.Config
}
)

0 comments on commit 92544d0

Please sign in to comment.