Skip to content
Open
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
49 changes: 49 additions & 0 deletions examples/read-k8smount/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package main

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

"github.com/knadh/koanf/providers/k8smount"
"github.com/knadh/koanf/v2"
)

var k = koanf.New(".")

func main() {
p := k8smount.Provider("mock/mount", ".", k8smount.Opt{
TransformFunc: func(k, v string) (string, any) {
return strings.ToLower(strings.ReplaceAll(k, "_", ".")), strings.TrimSpace(v)
},
})

if err := k.Load(p, nil); err != nil {
log.Fatalf("error loading config: %v", err)
}

fmt.Println("database's host is = ", k.String("database.host"))
fmt.Println("database's port is = ", k.Int("database.port"))

ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()

if err := p.Watch(func(_ any, err error) {
if err != nil {
log.Printf("watch error: %v", err)
return
}

log.Println("config changed. Reloading ...")
k.Load(p, nil)
k.Print()
}); err != nil {
log.Fatalf("error watching config: %v", err)
}

log.Println("waiting forever. Try making a change under mock/mount/ to live reload")
<-ctx.Done()
}
5 changes: 3 additions & 2 deletions go.work
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
go 1.24.4
go 1.25

toolchain go1.24.5
toolchain go1.25

use (
.
Expand All @@ -26,6 +26,7 @@ use (
./providers/etcd
./providers/file
./providers/fs
./providers/k8smount
./providers/nats
./providers/parameterstore
./providers/posflag
Expand Down
72 changes: 68 additions & 4 deletions go.work.sum

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions mock/mount/..2006_01_02_15_04_05.0000000000/database_host
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
localhost
1 change: 1 addition & 0 deletions mock/mount/..2006_01_02_15_04_05.0000000000/database_port
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
5432
1 change: 1 addition & 0 deletions mock/mount/..data
1 change: 1 addition & 0 deletions mock/mount/database_host
1 change: 1 addition & 0 deletions mock/mount/database_port
20 changes: 20 additions & 0 deletions providers/k8smount/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
module github.com/knadh/koanf/providers/k8smount

go 1.25

require (
github.com/fsnotify/fsnotify v1.9.0
github.com/knadh/koanf/maps v0.1.2
github.com/knadh/koanf/v2 v2.3.4
github.com/stretchr/testify v1.11.1
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/sys v0.13.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
24 changes: 24 additions & 0 deletions providers/k8smount/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo=
github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI=
github.com/knadh/koanf/v2 v2.3.4 h1:fnynNSDlujWE+v83hAp8wKr/cdoxHLO0629SN+U8Urc=
github.com/knadh/koanf/v2 v2.3.4/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28=
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=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
105 changes: 105 additions & 0 deletions providers/k8smount/helper_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package k8smount_test

import (
"fmt"
"os"
"path/filepath"
"testing"
"time"
)

const (
mountTimeFmt = "..2006_01_02_15_04_05.0000000000"
dataDir = "..data"
)

// writeVolumeMount creates a file structure that matches how a ConfigMap or Secret will be mounted
// in a Kubernetes Pod.
//
// First, files are created for each data field. These exist within a timestamp-based directory,
// likely when the ConfigMap or Secret was last modified.
//
// ..2025_06_28_09_28_32.3151791122/
// ├── database_hostname
// ├── database_name
// └── database_port
//
// A symlink is then created for "..data" to the directory containing the data:
//
// ..data -> ..2025_06_28_09_28_32.3151791122
//
// Finally, symlinks are created for the data files, via the "..data" symlink:
//
// database_hostname -> ..data/database_hostname
// database_name -> ..data/database_name
// database_port -> ..data/database_port
func writeVolumeMount(tb testing.TB, mount string, data map[string]string) error {
tb.Helper()

return writeVolumeMountAt(tb, time.Now(), mount, data)
}

// writeVolumeMountAtTime creates a file structure that matches how a ConfigMap or Secret will be
// mounted in a Kubernetes Pod, using the given time. See writeVolumeMount for a detailed
// description of the resulting file structure.
//
// This function can be called multiple times with different times, with the most recent call taking
// precedence if any conflicts occur.
func writeVolumeMountAt(tb testing.TB, t time.Time, mount string, data map[string]string) error {
tb.Helper()

dir := t.UTC().Format(mountTimeFmt)
dirPath := filepath.Join(mount, dir)

for key, value := range data {
if err := writeFile(tb, filepath.Join(dirPath, key), value); err != nil {
return err
}
}

tb.Chdir(mount)

if err := os.Remove(dataDir); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to cleanup existing symlink %s: %w", dataDir, err)
}

if err := os.Symlink(dir, dataDir); err != nil {
return fmt.Errorf("failed to create %s symlink to %q: %w", "..data", dir, err)
}

tb.Log("created symlink: ..data ->", dir)

for key := range data {
target := filepath.Join(dataDir, key)

if err := os.Remove(key); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to cleanup existing symlink %s: %w", key, err)
}

if err := os.Symlink(target, key); err != nil {
return fmt.Errorf("failed to create %q symlink to %q: %w", key, target, err)
}

tb.Log("created symlink:", key, "->", target)
}

return nil
}

func writeFile(tb testing.TB, path, content string) error {
tb.Helper()

dir := filepath.Dir(path)

if err := os.MkdirAll(dir, 0o755); err != nil {
return fmt.Errorf("failed to create directory %q: %w", dir, err)
}

if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
return fmt.Errorf("failed to create file %q: %w", path, err)
}

tb.Log("wrote:", path)

return nil
}
Loading