snapshot of https://github.com/ory/keto.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
CI / Run lints and checks (push) Failing after 47s
CI / Run tests (push) Failing after 1s
CI / Run race tests (push) Failing after 5s
CI / Build CLI docs (push) Has been skipped
CI / Run buf actions (push) Failing after 5s
CodeQL / Analyze (go) (push) Failing after 3s
CodeQL / Analyze (javascript) (push) Failing after 2s
Docker Image Scanners / scanners (push) Failing after 3s
Format / format (push) Failing after 3s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Draft newsletter (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
Some checks failed
CI / Run lints and checks (push) Failing after 47s
CI / Run tests (push) Failing after 1s
CI / Run race tests (push) Failing after 5s
CI / Build CLI docs (push) Has been skipped
CI / Run buf actions (push) Failing after 5s
CodeQL / Analyze (go) (push) Failing after 3s
CodeQL / Analyze (javascript) (push) Failing after 2s
Docker Image Scanners / scanners (push) Failing after 3s
Format / format (push) Failing after 3s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Draft newsletter (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
This commit is contained in:
295
internal/driver/config/namespace_watcher.go
Normal file
295
internal/driver/config/namespace_watcher.go
Normal file
@@ -0,0 +1,295 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
"github.com/ory/herodot"
|
||||
"github.com/ory/x/logrusx"
|
||||
"github.com/ory/x/stringsx"
|
||||
"github.com/ory/x/urlx"
|
||||
"github.com/ory/x/watcherx"
|
||||
"github.com/pelletier/go-toml"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/ory/keto/internal/namespace"
|
||||
)
|
||||
|
||||
type (
|
||||
Parser func([]byte, interface{}) error
|
||||
|
||||
NamespaceFile struct {
|
||||
Name string
|
||||
Contents []byte
|
||||
Parser Parser
|
||||
|
||||
namespace *namespace.Namespace // last successfully parsed namespace. May be nil if there was a parse error
|
||||
}
|
||||
|
||||
NamespaceWatcher struct {
|
||||
sync.RWMutex
|
||||
namespaces map[string]*NamespaceFile
|
||||
logger *logrusx.Logger
|
||||
target string
|
||||
}
|
||||
|
||||
eventHandler interface {
|
||||
handleRemove(*watcherx.RemoveEvent)
|
||||
handleChange(*watcherx.ChangeEvent)
|
||||
handleError(*watcherx.ErrorEvent)
|
||||
}
|
||||
)
|
||||
|
||||
var _ namespace.Manager = (*NamespaceWatcher)(nil)
|
||||
|
||||
func NewNamespaceWatcher(ctx context.Context, l *logrusx.Logger, target string) (*NamespaceWatcher, error) {
|
||||
nw := NamespaceWatcher{
|
||||
logger: l,
|
||||
target: target,
|
||||
namespaces: make(map[string]*NamespaceFile),
|
||||
}
|
||||
|
||||
return &nw, watchTarget(ctx, target, &nw, l)
|
||||
}
|
||||
|
||||
func (nw *NamespaceWatcher) handleRemove(e *watcherx.RemoveEvent) {
|
||||
nw.Lock()
|
||||
defer nw.Unlock()
|
||||
|
||||
delete(nw.namespaces, e.Source())
|
||||
}
|
||||
|
||||
func (nw *NamespaceWatcher) handleChange(e *watcherx.ChangeEvent) {
|
||||
// the lock is acquired before parsing to ensure that the getters are
|
||||
// waiting for the updated values
|
||||
nw.Lock()
|
||||
defer nw.Unlock()
|
||||
|
||||
n := nw.readNamespaceFile(e.Reader(), e.Source())
|
||||
if n == nil {
|
||||
return
|
||||
} else if n.namespace == nil {
|
||||
// parse failed, rolling back to previous working version
|
||||
if existing, ok := nw.namespaces[e.Source()]; ok {
|
||||
existing.Contents = n.Contents
|
||||
} else {
|
||||
nw.namespaces[e.Source()] = n
|
||||
}
|
||||
} else {
|
||||
nw.namespaces[e.Source()] = n
|
||||
}
|
||||
}
|
||||
|
||||
func (nw *NamespaceWatcher) handleError(e *watcherx.ErrorEvent) {
|
||||
nw.logger.
|
||||
WithError(e).
|
||||
Errorf("Received error while watching namespace files at target %s.", nw.target)
|
||||
}
|
||||
|
||||
func watchTarget(ctx context.Context, target string, handler eventHandler, log *logrusx.Logger) error {
|
||||
var (
|
||||
eventCh = make(watcherx.EventChannel)
|
||||
watcher watcherx.Watcher
|
||||
)
|
||||
|
||||
targetUrl, err := urlx.Parse(target)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
info, err := os.Stat(targetUrl.Path) //nolint:gosec // Path is from a validated configuration URL, not user input.
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
if info.IsDir() {
|
||||
watcher, err = watcherx.WatchDirectory(ctx, urlx.GetURLFilePath(targetUrl), eventCh)
|
||||
} else {
|
||||
watcher, err = watcherx.Watch(ctx, targetUrl, eventCh)
|
||||
}
|
||||
// this handles the watcher init error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// trigger initial load
|
||||
done, err := watcher.DispatchNow()
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
initialEventsProcessed := make(chan struct{})
|
||||
go startEventHandler(ctx, eventCh, handler, done, initialEventsProcessed, log)
|
||||
|
||||
// wait for initial load to be done
|
||||
<-initialEventsProcessed
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func startEventHandler(ctx context.Context,
|
||||
eventCh watcherx.EventChannel,
|
||||
handler eventHandler,
|
||||
done <-chan int,
|
||||
initialEventsProcessed chan<- struct{},
|
||||
log *logrusx.Logger) {
|
||||
|
||||
initialDone := false
|
||||
for {
|
||||
select {
|
||||
// because we use an unbuffered chan we can be sure that at least all
|
||||
// initial events are handled
|
||||
case <-done:
|
||||
initialDone = true
|
||||
close(initialEventsProcessed)
|
||||
// Disable this case after it fires once. watcherx closes done when
|
||||
// the watcher shuts down (e.g. directoryWatcher on ctx cancel); a
|
||||
// closed channel is always ready, so leaving done set would re-enter
|
||||
// this branch and close(initialEventsProcessed) a second time (panic).
|
||||
done = nil
|
||||
|
||||
case <-ctx.Done():
|
||||
return
|
||||
|
||||
case e, open := <-eventCh:
|
||||
if !open {
|
||||
return
|
||||
}
|
||||
|
||||
if initialDone {
|
||||
log.
|
||||
WithField("file", e.Source()).
|
||||
WithField("event_type", fmt.Sprintf("%T", e)).
|
||||
Info("A change to a namespace file was detected.")
|
||||
}
|
||||
|
||||
switch e := e.(type) {
|
||||
case *watcherx.RemoveEvent:
|
||||
handler.handleRemove(e)
|
||||
case *watcherx.ChangeEvent:
|
||||
handler.handleChange(e)
|
||||
case *watcherx.ErrorEvent:
|
||||
handler.handleError(e)
|
||||
default:
|
||||
log.Warnf("Ignored unknown event %T", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (nw *NamespaceWatcher) readNamespaceFile(r io.Reader, source string) *NamespaceFile {
|
||||
parse, err := GetParser(source)
|
||||
if err != nil {
|
||||
nw.logger.
|
||||
WithError(err).
|
||||
WithField("file_name", source).
|
||||
Warn("could not infer format from file extension")
|
||||
return nil
|
||||
}
|
||||
|
||||
raw, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
nw.logger.
|
||||
WithError(errors.WithStack(err)).
|
||||
WithField("file_name", source).
|
||||
Error("could not read namespace file")
|
||||
return nil
|
||||
}
|
||||
|
||||
n := namespace.Namespace{}
|
||||
if err := parse(raw, &n); err != nil {
|
||||
nw.logger.
|
||||
WithError(errors.WithStack(err)).
|
||||
WithField("file_name", source).
|
||||
Error("could not parse namespace file")
|
||||
return &NamespaceFile{Name: source, Contents: raw, Parser: parse}
|
||||
}
|
||||
|
||||
return &NamespaceFile{Name: source, Contents: raw, Parser: parse, namespace: &n}
|
||||
}
|
||||
|
||||
func (nw *NamespaceWatcher) GetNamespaceByName(_ context.Context, name string) (*namespace.Namespace, error) {
|
||||
nw.RLock()
|
||||
defer nw.RUnlock()
|
||||
|
||||
for _, nsf := range nw.namespaces {
|
||||
if nsf.namespace != nil && nsf.namespace.Name == name {
|
||||
return nsf.namespace, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errors.WithStack(herodot.ErrNotFound().WithErrorf(
|
||||
"Unknown namespace with name %s", name))
|
||||
}
|
||||
|
||||
func (nw *NamespaceWatcher) GetNamespaceByConfigID(_ context.Context, id int32) (*namespace.Namespace, error) {
|
||||
nw.RLock()
|
||||
defer nw.RUnlock()
|
||||
|
||||
for _, nspace := range nw.namespaces {
|
||||
//lint:ignore SA1019 backwards compatibility
|
||||
//nolint:staticcheck
|
||||
if nspace.namespace.ID == id {
|
||||
return nspace.namespace, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errors.WithStack(herodot.ErrNotFound().WithErrorf(
|
||||
"Unknown namespace with ID %d", id))
|
||||
}
|
||||
|
||||
func (nw *NamespaceWatcher) Namespaces(_ context.Context) ([]*namespace.Namespace, error) {
|
||||
nw.RLock()
|
||||
defer nw.RUnlock()
|
||||
|
||||
nspaces := make([]*namespace.Namespace, 0, len(nw.namespaces))
|
||||
for _, nsf := range nw.namespaces {
|
||||
if nsf.namespace != nil {
|
||||
nspaces = append(nspaces, nsf.namespace)
|
||||
}
|
||||
}
|
||||
return nspaces, nil
|
||||
}
|
||||
|
||||
func (nw *NamespaceWatcher) NamespaceFiles() []*NamespaceFile {
|
||||
nw.RLock()
|
||||
defer nw.RUnlock()
|
||||
|
||||
nsfs := make([]*NamespaceFile, 0, len(nw.namespaces))
|
||||
for _, nsf := range nw.namespaces {
|
||||
nsfs = append(nsfs, nsf)
|
||||
}
|
||||
return nsfs
|
||||
}
|
||||
|
||||
func (nw *NamespaceWatcher) ShouldReload(newValue interface{}) bool {
|
||||
v, ok := newValue.(string)
|
||||
if !ok {
|
||||
// the manager type changed
|
||||
return true
|
||||
}
|
||||
// reload if target changed
|
||||
return v != nw.target
|
||||
}
|
||||
|
||||
func GetParser(fn string) (Parser, error) {
|
||||
switch ext := stringsx.SwitchExact(filepath.Ext(fn)); {
|
||||
case ext.AddCase(".yaml"), ext.AddCase(".yml"):
|
||||
return func(b []byte, i interface{}) error {
|
||||
return yaml.Unmarshal(b, i)
|
||||
}, nil
|
||||
case ext.AddCase(".json"):
|
||||
return json.Unmarshal, nil
|
||||
case ext.AddCase(".toml"):
|
||||
return toml.Unmarshal, nil
|
||||
default:
|
||||
return nil, ext.ToUnknownCaseErr()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user