pomerium/config/config_source_test.go
Caleb Doxsey 1f30dead31
fileutil: reimplement file watcher (#5498)
* remove context, add close

* update tests

* cleanup

* fileutil: reimplement file watcher

* remove test, simplify tree set code, fix data race
2025-02-26 09:21:06 -07:00

227 lines
5.6 KiB
Go

package config
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"syscall"
"testing"
"time"
"github.com/rs/zerolog"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/pomerium/pomerium/internal/log"
"github.com/pomerium/pomerium/internal/signal"
"github.com/pomerium/pomerium/internal/testutil"
)
func TestFileWatcherSource(t *testing.T) {
t.Parallel()
tmpdir := t.TempDir()
err := os.WriteFile(filepath.Join(tmpdir, "example.txt"), []byte{1}, 0o600)
if !assert.NoError(t, err) {
return
}
err = os.WriteFile(filepath.Join(tmpdir, "kubernetes-example.txt"), []byte{2}, 0o600)
if !assert.NoError(t, err) {
return
}
newTest := func(enabled bool) func(*testing.T) {
return func(t *testing.T) {
ssrc := NewStaticSource(&Config{
Options: &Options{
CAFile: filepath.Join(tmpdir, "example.txt"),
Policies: []Policy{{
KubernetesServiceAccountTokenFile: filepath.Join(tmpdir, "kubernetes-example.txt"),
}},
RuntimeFlags: map[RuntimeFlag]bool{
RuntimeFlagConfigHotReload: enabled,
},
},
})
src := NewFileWatcherSource(context.Background(), ssrc)
ch := make(chan struct{}, 10)
src.OnConfigChange(context.Background(), func(_ context.Context, _ *Config) {
ch <- struct{}{}
})
err := os.WriteFile(filepath.Join(tmpdir, "example.txt"), []byte{1, 2}, 0o600)
if !assert.NoError(t, err) {
return
}
select {
case <-ch:
if !enabled {
t.Error("expected OnConfigChange not to be fired after modifying a file")
}
case <-time.After(time.Second):
if enabled {
t.Error("expected OnConfigChange to be fired after modifying a file")
}
}
require.Empty(t, ch, "expected exactly one OnConfigChange event")
err = os.WriteFile(filepath.Join(tmpdir, "kubernetes-example.txt"), []byte{2, 3}, 0o600)
if !assert.NoError(t, err) {
return
}
select {
case <-ch:
if !enabled {
t.Error("expected OnConfigChange not to be fired after modifying a file")
}
case <-time.After(time.Second):
if enabled {
t.Error("expected OnConfigChange to be fired after modifying a policy file")
}
}
require.Empty(t, ch, "expected exactly one OnConfigChange event")
ssrc.SetConfig(context.Background(), &Config{
Options: &Options{
CAFile: filepath.Join(tmpdir, "example.txt"),
},
})
select {
case <-ch:
case <-time.After(time.Second):
if enabled {
t.Error("expected OnConfigChange to be fired after triggering a change to the underlying source")
}
}
require.Empty(t, ch, "expected exactly one OnConfigChange event")
}
}
t.Run("Hot Reload Enabled", newTest(true))
t.Run("Hot Reload Disabled", newTest(false))
}
func TestFileOrEnvironmentSource(t *testing.T) {
t.Parallel()
tmpdir := t.TempDir()
err := os.WriteFile(filepath.Join(tmpdir, "example.txt"), []byte{1}, 0o600)
if !assert.NoError(t, err) {
return
}
err = os.WriteFile(filepath.Join(tmpdir, "kubernetes-example.txt"), []byte{2}, 0o600)
if !assert.NoError(t, err) {
return
}
newTest := func(enabled bool) func(*testing.T) {
return func(t *testing.T) {
initialConfigYaml := fmt.Sprintf(`
certificate_authority_file: %s
policy:
- from: https://foo
to: https://bar
kubernetes_service_account_token_file: %s
codec_type: auto
runtime_flags:
config_hot_reload: %t
`,
filepath.Join(tmpdir, "example.txt"),
filepath.Join(tmpdir, "kubernetes-example.txt"),
enabled,
)
configFilePath := filepath.Join(tmpdir, "config.yaml")
err := os.WriteFile(configFilePath, []byte(initialConfigYaml), 0o600)
require.NoError(t, err)
var src Source
src, err = NewFileOrEnvironmentSource(context.Background(), configFilePath, "")
require.NoError(t, err)
src = NewFileWatcherSource(context.Background(), src)
ch := make(chan struct{}, 10)
src.OnConfigChange(context.Background(), func(_ context.Context, _ *Config) {
ch <- struct{}{}
})
err = os.WriteFile(filepath.Join(tmpdir, "example.txt"), []byte{1, 2}, 0o600)
require.NoError(t, err)
select {
case <-ch:
if !enabled {
t.Error("expected OnConfigChange not to be fired after modifying a file")
}
case <-time.After(time.Second):
if enabled {
t.Error("expected OnConfigChange to be fired after modifying a file")
}
}
require.Empty(t, ch, "expected exactly one OnConfigChange event")
err = os.WriteFile(filepath.Join(tmpdir, "kubernetes-example.txt"), []byte{2, 3}, 0o600)
if !assert.NoError(t, err) {
return
}
select {
case <-ch:
if !enabled {
t.Error("expected OnConfigChange not to be fired after modifying a file")
}
case <-time.After(time.Second):
if enabled {
t.Error("expected OnConfigChange to be fired after modifying a policy file")
}
}
require.Empty(t, ch, "expected exactly one OnConfigChange event")
}
}
t.Run("Hot Reload Enabled", newTest(true))
t.Run("Hot Reload Disabled", newTest(false))
t.Run("SIGHUP", func(t *testing.T) {
t.Parallel()
ready := signal.New()
readyCh := ready.Bind()
ctx := testutil.GetContext(t, time.Minute)
ctx = log.Ctx(ctx).Hook(zerolog.HookFunc(func(_ *zerolog.Event, _ zerolog.Level, message string) {
if strings.Contains(message, "received SIGHUP") {
ready.Broadcast(ctx)
}
})).WithContext(ctx)
tmp := t.TempDir()
cfgFP := filepath.Join(tmp, "config.json")
require.NoError(t, os.WriteFile(cfgFP, []byte(`{}`), 0o600))
_, err := NewFileOrEnvironmentSource(ctx, cfgFP, "")
assert.NoError(t, err)
require.NoError(t, syscall.Kill(syscall.Getpid(), syscall.SIGHUP))
select {
case <-ctx.Done():
t.Error("expected to receive SIGHUP log message")
case <-readyCh:
}
})
}