Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ jobs:
cd sdk/tools && go test ./... -count=1 -coverprofile=../../cov-tools.out -covermode=atomic & pids+=($!)
cd sdk/contrib/viper && go test ./... -count=1 -coverprofile=../../cov-contrib-viper.out -covermode=atomic & pids+=($!)
cd sdk/contrib/envconfig && go test ./... -count=1 -coverprofile=../../cov-contrib-envconfig.out -covermode=atomic & pids+=($!)
cd sdk/contrib/koanf && go test ./... -count=1 -coverprofile=../../cov-contrib-koanf.out -covermode=atomic & pids+=($!)
cd cmd/decree && go test ./... -count=1 -coverprofile=../../cov-decree.out -covermode=atomic & pids+=($!)
fail=0
for pid in "${pids[@]}"; do wait "$pid" || fail=1; done
Expand All @@ -278,7 +279,7 @@ jobs:
- name: Merge coverage profiles
run: |
echo "mode: atomic" > coverage.out
for f in coverage-internal.out cov-configclient.out cov-adminclient.out cov-configwatcher.out cov-grpctransport.out cov-tools.out cov-contrib-viper.out cov-contrib-envconfig.out cov-decree.out; do
for f in coverage-internal.out cov-configclient.out cov-adminclient.out cov-configwatcher.out cov-grpctransport.out cov-tools.out cov-contrib-viper.out cov-contrib-envconfig.out cov-contrib-koanf.out cov-decree.out; do
[ -f "$f" ] && grep -v "^mode:" "$f" >> coverage.out || true
done

Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ SERVER_LDFLAGS := -X github.com/opendecree/decree/internal/version.Version=$(GIT
CLI_LDFLAGS := -X main.cliVersion=$(GIT_VERSION) -X main.cliCommit=$(GIT_COMMIT)

# Module list for multi-module operations.
SDK_MODULES := sdk/retry sdk/configclient sdk/adminclient sdk/configwatcher sdk/grpctransport sdk/tools sdk/contrib/viper sdk/contrib/envconfig
SDK_MODULES := sdk/retry sdk/configclient sdk/adminclient sdk/configwatcher sdk/grpctransport sdk/tools sdk/contrib/viper sdk/contrib/envconfig sdk/contrib/koanf

.PHONY: all generate generate-proto generate-sqlc deps test lint lint-go lint-proto lint-migrations build image ui migrate e2e e2e-jwt examples bench bench-e2e stress chaos docs docs-api docs-cli docs-man docs-serve docs-deploy pre-commit clean tools help demo-gif validate-meta-schemas

Expand Down
99 changes: 99 additions & 0 deletions sdk/contrib/koanf/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# contrib/koanf

A [koanf](https://github.com/knadh/koanf) provider for OpenDecree configuration.

> **Alpha** — API subject to change.

## Installation

```bash
go get github.com/opendecree/decree/sdk/contrib/koanf
```

You also need a transport to connect to an OpenDecree server. The gRPC
transport lives in the sibling `sdk/grpctransport` module:

```bash
go get github.com/opendecree/decree/sdk/grpctransport
```

## Usage

```go
import (
"log"

"github.com/knadh/koanf/v2"
koanfcontrib "github.com/opendecree/decree/sdk/contrib/koanf"
"github.com/opendecree/decree/sdk/configclient"
"github.com/opendecree/decree/sdk/grpctransport"
"google.golang.org/grpc"
)

func main() {
conn, err := grpc.NewClient("decree-server:443", grpc.WithTransportCredentials(...))
if err != nil {
log.Fatal(err)
}
defer conn.Close()

transport := grpctransport.NewConfigTransport(conn)
client := configclient.New(transport)

provider := koanfcontrib.New(client, "my-tenant")

k := koanf.New(".")
if err := k.Load(provider, nil); err != nil {
log.Fatal(err)
}

log.Println("app.name =", k.String("app.name"))
}
```

## Options

### `WithTimeout(d time.Duration)`

Sets the per-call context timeout for `Read`. Default: 5 seconds.

```go
provider := koanfcontrib.New(client, "my-tenant",
koanfcontrib.WithTimeout(10*time.Second),
)
```

## Watching for changes

The provider exposes a `Watch` method that polls for configuration changes at a
30-second interval. Call it after loading to keep koanf in sync:

```go
provider := koanfcontrib.New(client, "my-tenant")
k := koanf.New(".")

if err := k.Load(provider, nil); err != nil {
log.Fatal(err)
}

provider.Watch(func(event interface{}, err error) {
if err != nil {
log.Println("watch error:", err)
return
}
// Re-load config on each tick.
if err := k.Load(provider, nil); err != nil {
log.Println("reload error:", err)
}
})
```

## Notes

- Configuration values are returned as a flat `map[string]interface{}` keyed by
field path (e.g. `"app.name"`). koanf treats the delimiter (`.` by default)
as a nesting separator, so `k.String("app.name")` accesses the nested path.
- `ReadBytes` is not supported. Always pass `nil` as the parser argument to
`koanf.Load`.
- For real-time change notifications (instead of polling), subscribe to the
decree change stream using `configwatcher`.
20 changes: 20 additions & 0 deletions sdk/contrib/koanf/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
module github.com/opendecree/decree/sdk/contrib/koanf

go 1.22.0

require (
github.com/knadh/koanf/v2 v2.1.2
github.com/opendecree/decree/sdk/configclient v0.1.2
)

require (
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/knadh/koanf/maps v0.1.1 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/opendecree/decree/sdk/retry v0.0.0 // indirect
)

replace github.com/opendecree/decree/sdk/configclient => ../../../sdk/configclient

replace github.com/opendecree/decree/sdk/retry => ../../../sdk/retry
10 changes: 10 additions & 0 deletions sdk/contrib/koanf/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs=
github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI=
github.com/knadh/koanf/v2 v2.1.2 h1:I2rtLRqXRy1p01m/utEtpZSSA6dcJbgGVuE27kW2PzQ=
github.com/knadh/koanf/v2 v2.1.2/go.mod h1:Gphfaen0q1Fc1HTgJgSTC4oRX9R2R5ErYMZJy8fLJBo=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
93 changes: 93 additions & 0 deletions sdk/contrib/koanf/koanf.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// Package koanfcontrib provides a koanf v2 provider backed by an OpenDecree
// configclient. It implements the koanf.Provider interface so that decree
// configuration values can be loaded directly into a koanf instance.
//
// Usage:
//
// provider := koanfcontrib.New(client, "my-tenant")
// k := koanf.New(".")
// if err := k.Load(provider, nil); err != nil {
// log.Fatal(err)
// }
package koanfcontrib

import (
"context"
"fmt"
"time"

"github.com/knadh/koanf/v2"
"github.com/opendecree/decree/sdk/configclient"
)

// Provider is a koanf provider backed by a decree configclient.
// It fetches all configuration values for a tenant via [configclient.Client.GetAll]
// and exposes them as a flat map of field-path keys.
type Provider struct {
client *configclient.Client
tenantID string
timeout time.Duration
}

// New creates a Provider for the given tenant.
// opts can be used to override defaults such as the per-call timeout.
func New(client *configclient.Client, tenantID string, opts ...Option) *Provider {
p := &Provider{client: client, tenantID: tenantID, timeout: 5 * time.Second}
for _, o := range opts {
o(p)
}
return p
}

// Option configures a Provider.
type Option func(*Provider)

// WithTimeout sets the per-call context timeout used by Read.
// The default timeout is 5 seconds.
func WithTimeout(d time.Duration) Option {
return func(p *Provider) { p.timeout = d }
}

// Read fetches all configuration values for the tenant from OpenDecree and
// returns them as a flat map[string]interface{} keyed by field path.
// Implements koanf.Provider.
func (p *Provider) Read() (map[string]interface{}, error) {
ctx, cancel := context.WithTimeout(context.Background(), p.timeout)
defer cancel()
all, err := p.client.GetAll(ctx, p.tenantID)
if err != nil {
return nil, err
}
m := make(map[string]interface{}, len(all))
for k, v := range all {
m[k] = v
}
return m, nil
}

// ReadBytes is not supported by this provider. Use Read instead (pass nil as
// the Parser argument to koanf.Load).
// Implements koanf.Provider.
func (p *Provider) ReadBytes() ([]byte, error) {
return nil, fmt.Errorf("koanfcontrib: ReadBytes not supported; use Read with a nil parser")
}

// Watch polls for configuration changes at a 30-second interval by invoking
// the callback, which causes koanf to re-load the provider.
// The callback receives a nil event and nil error on each tick.
//
// Watch launches a background goroutine and returns immediately.
// This is a convenience method — it is not part of the koanf.Provider interface.
func (p *Provider) Watch(cb func(event interface{}, err error)) error {
go func() {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for range ticker.C {
cb(nil, nil)
}
}()
return nil
}

// Ensure *Provider implements koanf.Provider at compile time.
var _ koanf.Provider = (*Provider)(nil)
Loading
Loading