diff --git a/.github/workflows/build_and_test.yaml b/.github/workflows/build_and_test.yaml index cd49c5a82c7..1c5e826076b 100644 --- a/.github/workflows/build_and_test.yaml +++ b/.github/workflows/build_and_test.yaml @@ -55,7 +55,7 @@ jobs: - name: Run Coverage Tests run: make go.test.coverage - name: Upload coverage to Codecov - uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4.6.0 + uses: codecov/codecov-action@015f24e6818733317a2da2edd6290ab26238649a # v5.0.7 with: fail_ci_if_error: true files: ./coverage.xml diff --git a/internal/filewatcher/filewatcher.go b/internal/filewatcher/filewatcher.go index 4fce5e9aba4..b7b5555aee7 100644 --- a/internal/filewatcher/filewatcher.go +++ b/internal/filewatcher/filewatcher.go @@ -8,6 +8,7 @@ package filewatcher import ( "errors" "fmt" + "os" "path/filepath" "sync" @@ -90,7 +91,6 @@ func (fw *fileWatcher) Add(path string) error { return err } -// Stop watching a path func (fw *fileWatcher) Remove(path string) error { fw.mu.Lock() defer fw.mu.Unlock() @@ -142,9 +142,7 @@ func (fw *fileWatcher) getWorker(path string) (*workerState, string, string, err return nil, "", "", errors.New("using a closed watcher") } - cleanedPath := filepath.Clean(path) - parentPath, _ := filepath.Split(cleanedPath) - + cleanedPath, parentPath := getPath(path) ws, workerExists := fw.workers[parentPath] if !workerExists { wk, err := newWorker(parentPath, fw.funcs) @@ -167,8 +165,7 @@ func (fw *fileWatcher) findWorker(path string) (*workerState, string, error) { return nil, "", errors.New("using a closed watcher") } - cleanedPath := filepath.Clean(path) - parentPath, _ := filepath.Split(cleanedPath) + cleanedPath, parentPath := getPath(path) ws, workerExists := fw.workers[parentPath] if !workerExists { @@ -177,3 +174,13 @@ func (fw *fileWatcher) findWorker(path string) (*workerState, string, error) { return ws, cleanedPath, nil } + +func getPath(path string) (cleanedPath, parentPath string) { + cleanedPath = filepath.Clean(path) + parentPath, _ = filepath.Split(cleanedPath) + if f, err := os.Lstat(cleanedPath); err == nil && f.IsDir() { + parentPath = cleanedPath + } + + return +} diff --git a/internal/filewatcher/filewatcher_test.go b/internal/filewatcher/filewatcher_test.go index 5230d7c05ad..5b451fa0df7 100644 --- a/internal/filewatcher/filewatcher_test.go +++ b/internal/filewatcher/filewatcher_test.go @@ -14,6 +14,7 @@ import ( "runtime" "sync" "testing" + "time" "github.com/fsnotify/fsnotify" "github.com/stretchr/testify/require" @@ -173,6 +174,44 @@ func TestWatchFile(t *testing.T) { }) } +func TestWatchDir(t *testing.T) { + // Given a file being watched + watchFile := newWatchFile(t) + _, err := os.Stat(watchFile) + require.NoError(t, err) + + w := NewWatcher() + defer func() { + _ = w.Close() + }() + d := path.Dir(watchFile) + require.NoError(t, w.Add(d)) + + timeout := time.After(5 * time.Second) + + wg := sync.WaitGroup{} + var timeoutErr error + wg.Add(1) + go func() { + select { + case <-w.Events(d): + + case <-w.Events(watchFile): + + case <-timeout: + timeoutErr = errors.New("timeout") + } + wg.Done() + }() + + // Overwriting the file and waiting its event to be received. + err = os.WriteFile(watchFile, []byte("foo: baz\n"), 0o600) + require.NoError(t, err) + wg.Wait() + + require.NoErrorf(t, timeoutErr, "timeout waiting for event") +} + func TestWatcherLifecycle(t *testing.T) { watchFile1, watchFile2 := newTwoWatchFile(t) @@ -295,27 +334,23 @@ func TestBadAddWatcher(t *testing.T) { func TestDuplicateAdd(t *testing.T) { w := NewWatcher() - name := newWatchFile(t) + defer func() { + _ = w.Close() + _ = os.Remove(name) + }() - if err := w.Add(name); err != nil { - t.Errorf("Expecting nil, got %v", err) - } - - if err := w.Add(name); err == nil { - t.Errorf("Expecting error, got nil") - } - - _ = w.Close() + require.NoError(t, w.Add(name)) + require.Error(t, w.Add(name)) } func TestBogusRemove(t *testing.T) { w := NewWatcher() - name := newWatchFile(t) - if err := w.Remove(name); err == nil { - t.Errorf("Expecting error, got nil") - } + defer func() { + _ = w.Close() + _ = os.Remove(name) + }() - _ = w.Close() + require.Error(t, w.Remove(name)) } diff --git a/internal/filewatcher/worker.go b/internal/filewatcher/worker.go index 6ae9c9f77ba..e5ed5e283f4 100644 --- a/internal/filewatcher/worker.go +++ b/internal/filewatcher/worker.go @@ -20,7 +20,7 @@ import ( type worker struct { mu sync.RWMutex - // watcher is an fsnotify watcher that watches the parent + // watcher is a fsnotify watcher that watches the parent // dir of watchedFiles. dirWatcher *fsnotify.Watcher @@ -96,10 +96,9 @@ func (wk *worker) loop() { continue } - sum := getHashSum(path) - if !bytes.Equal(sum, ft.hash) { + sum, isDir := getHashSum(path) + if isDir || !bytes.Equal(sum, ft.hash) { ft.hash = sum - select { case ft.events <- event: // nothing to do @@ -141,7 +140,7 @@ func (wk *worker) loop() { } } -// used only by the worker goroutine +// drainRetiringTrackers used only by the worker goroutine func (wk *worker) drainRetiringTrackers() { // cleanup any trackers that were in the process // of being retired, but didn't get processed due @@ -156,7 +155,7 @@ func (wk *worker) drainRetiringTrackers() { } } -// make a local copy of the set of trackers to avoid contention with callers +// getTrackers make a local copy of the set of trackers to avoid contention with callers // used only by the worker goroutine func (wk *worker) getTrackers() map[string]*fileTracker { wk.mu.RLock() @@ -184,36 +183,34 @@ func (wk *worker) terminate() { func (wk *worker) addPath(path string) error { wk.mu.Lock() + defer wk.mu.Unlock() ft := wk.watchedFiles[path] if ft != nil { - wk.mu.Unlock() return fmt.Errorf("path %s is already being watched", path) } + h, _ := getHashSum(path) ft = &fileTracker{ events: make(chan fsnotify.Event), errors: make(chan error), - hash: getHashSum(path), + hash: h, } - wk.watchedFiles[path] = ft - wk.mu.Unlock() return nil } func (wk *worker) removePath(path string) error { wk.mu.Lock() + defer wk.mu.Unlock() ft := wk.watchedFiles[path] if ft == nil { - wk.mu.Unlock() return fmt.Errorf("path %s not found", path) } delete(wk.watchedFiles, path) - wk.mu.Unlock() wk.retireTrackerCh <- ft return nil @@ -241,16 +238,26 @@ func (wk *worker) errorChannel(path string) chan error { return nil } -// gets the hash of the given file, or nil if there's a problem -func getHashSum(file string) []byte { +// getHashSum return the hash of the given file, or nil if there's a problem, or it's a directory. +func getHashSum(file string) ([]byte, bool) { f, err := os.Open(file) if err != nil { - return nil + return nil, false } - defer f.Close() - r := bufio.NewReader(f) + defer func() { + _ = f.Close() + }() + fi, err := f.Stat() + if err != nil { + return nil, false + } + if fi.IsDir() { + return nil, true + } + + r := bufio.NewReader(f) h := sha256.New() _, _ = io.Copy(h, r) - return h.Sum(nil) + return h.Sum(nil), false } diff --git a/internal/gatewayapi/helpers.go b/internal/gatewayapi/helpers.go index 6ed1d7699a6..2626e1b4be3 100644 --- a/internal/gatewayapi/helpers.go +++ b/internal/gatewayapi/helpers.go @@ -610,20 +610,56 @@ func setIfNil[T any](target **T, value *T) { } } -func getIPFamily(envoyProxy *egv1a1.EnvoyProxy) *ir.IPFamily { +// getServiceIPFamily returns the IP family configuration from a Kubernetes Service +// following the dual-stack service configuration scenarios: +// https://kubernetes.io/docs/concepts/services-networking/dual-stack/#dual-stack-service-configuration-scenarios +// +// The IP family is determined in the following order: +// 1. Service.Spec.IPFamilyPolicy == RequireDualStack -> DualStack +// 2. Service.Spec.IPFamilies length > 1 -> DualStack +// 3. Service.Spec.IPFamilies[0] -> IPv4 or IPv6 +// 4. nil if not specified +func getServiceIPFamily(service *corev1.Service) *egv1a1.IPFamily { + if service == nil { + return nil + } + + // If ipFamilyPolicy is RequireDualStack, return DualStack + if service.Spec.IPFamilyPolicy != nil && + *service.Spec.IPFamilyPolicy == corev1.IPFamilyPolicyRequireDualStack { + return ptr.To(egv1a1.DualStack) + } + + // Check ipFamilies array + if len(service.Spec.IPFamilies) > 0 { + if len(service.Spec.IPFamilies) > 1 { + return ptr.To(egv1a1.DualStack) + } + switch service.Spec.IPFamilies[0] { + case corev1.IPv4Protocol: + return ptr.To(egv1a1.IPv4) + case corev1.IPv6Protocol: + return ptr.To(egv1a1.IPv6) + } + } + + return nil +} + +// getEnvoyIPFamily returns the IPFamily configuration from EnvoyProxy +func getEnvoyIPFamily(envoyProxy *egv1a1.EnvoyProxy) *egv1a1.IPFamily { if envoyProxy == nil || envoyProxy.Spec.IPFamily == nil { return nil } - var result ir.IPFamily + switch *envoyProxy.Spec.IPFamily { case egv1a1.IPv4: - result = ir.IPv4 + return ptr.To(egv1a1.IPv4) case egv1a1.IPv6: - result = ir.IPv6 + return ptr.To(egv1a1.IPv6) case egv1a1.DualStack: - result = ir.DualStack + return ptr.To(egv1a1.DualStack) default: return nil } - return &result } diff --git a/internal/gatewayapi/helpers_test.go b/internal/gatewayapi/helpers_test.go index 5698867c3ca..6403279a5a9 100644 --- a/internal/gatewayapi/helpers_test.go +++ b/internal/gatewayapi/helpers_test.go @@ -15,6 +15,7 @@ import ( "testing" "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" @@ -551,3 +552,67 @@ func TestIsRefToGateway(t *testing.T) { }) } } + +func TestGetServiceIPFamily(t *testing.T) { + testCases := []struct { + name string + service *corev1.Service + expected *egv1a1.IPFamily + }{ + { + name: "nil service", + service: nil, + expected: nil, + }, + { + name: "require dual stack", + service: &corev1.Service{ + Spec: corev1.ServiceSpec{ + IPFamilyPolicy: ptr.To(corev1.IPFamilyPolicyRequireDualStack), + }, + }, + expected: ptr.To(egv1a1.DualStack), + }, + { + name: "multiple ip families", + service: &corev1.Service{ + Spec: corev1.ServiceSpec{ + IPFamilies: []corev1.IPFamily{corev1.IPv4Protocol, corev1.IPv6Protocol}, + }, + }, + expected: ptr.To(egv1a1.DualStack), + }, + { + name: "ipv4 only", + service: &corev1.Service{ + Spec: corev1.ServiceSpec{ + IPFamilies: []corev1.IPFamily{corev1.IPv4Protocol}, + }, + }, + expected: ptr.To(egv1a1.IPv4), + }, + { + name: "ipv6 only", + service: &corev1.Service{ + Spec: corev1.ServiceSpec{ + IPFamilies: []corev1.IPFamily{corev1.IPv6Protocol}, + }, + }, + expected: ptr.To(egv1a1.IPv6), + }, + { + name: "no ip family specified", + service: &corev1.Service{ + Spec: corev1.ServiceSpec{}, + }, + expected: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := getServiceIPFamily(tc.service) + require.Equal(t, tc.expected, result) + }) + } +} diff --git a/internal/gatewayapi/listener.go b/internal/gatewayapi/listener.go index bf369e7b827..75739609609 100644 --- a/internal/gatewayapi/listener.go +++ b/internal/gatewayapi/listener.go @@ -102,8 +102,8 @@ func (t *Translator) ProcessListeners(gateways []*GatewayContext, xdsIR resource } address := net.IPv4ListenerAddress - ipFamily := getIPFamily(gateway.envoyProxy) - if ipFamily != nil && (*ipFamily == ir.IPv6 || *ipFamily == ir.DualStack) { + ipFamily := getEnvoyIPFamily(gateway.envoyProxy) + if ipFamily != nil && (*ipFamily == egv1a1.IPv6 || *ipFamily == egv1a1.DualStack) { address = net.IPv6ListenerAddress } @@ -118,7 +118,7 @@ func (t *Translator) ProcessListeners(gateways []*GatewayContext, xdsIR resource Address: address, Port: uint32(containerPort), Metadata: buildListenerMetadata(listener, gateway), - IPFamily: getIPFamily(gateway.envoyProxy), + IPFamily: ipFamily, }, TLS: irTLSConfigs(listener.tlsSecrets...), Path: ir.PathSettings{ @@ -126,9 +126,6 @@ func (t *Translator) ProcessListeners(gateways []*GatewayContext, xdsIR resource EscapedSlashesAction: ir.UnescapeAndRedirect, }, } - if ipFamily := getIPFamily(gateway.envoyProxy); ipFamily != nil { - irListener.CoreListenerDetails.IPFamily = ipFamily - } if listener.Hostname != nil { irListener.Hostnames = append(irListener.Hostnames, string(*listener.Hostname)) } else { @@ -144,7 +141,7 @@ func (t *Translator) ProcessListeners(gateways []*GatewayContext, xdsIR resource Name: irListenerName(listener), Address: address, Port: uint32(containerPort), - IPFamily: getIPFamily(gateway.envoyProxy), + IPFamily: ipFamily, }, // Gateway is processed firstly, then ClientTrafficPolicy, then xRoute. diff --git a/internal/gatewayapi/route.go b/internal/gatewayapi/route.go index 26627a07285..ddada5f17b6 100644 --- a/internal/gatewayapi/route.go +++ b/internal/gatewayapi/route.go @@ -1232,6 +1232,7 @@ func (t *Translator) processDestination(backendRefContext BackendRefContext, addrType *ir.DestinationAddressType ) protocol := inspectAppProtocolByRouteKind(routeType) + switch KindDerefOr(backendRef.Kind, resource.KindService) { case resource.KindServiceImport: serviceImport := resources.GetServiceImport(backendNamespace, string(backendRef.Name)) @@ -1262,9 +1263,9 @@ func (t *Translator) processDestination(backendRefContext BackendRefContext, Endpoints: endpoints, AddressType: addrType, } + case resource.KindService: ds = t.processServiceDestinationSetting(backendRef.BackendObjectReference, backendNamespace, protocol, resources, envoyProxy) - ds.TLS = t.applyBackendTLSSetting( backendRef.BackendObjectReference, backendNamespace, @@ -1280,6 +1281,7 @@ func (t *Translator) processDestination(backendRefContext BackendRefContext, envoyProxy, ) ds.Filters = t.processDestinationFilters(routeType, backendRefContext, parentRef, route, resources) + ds.IPFamily = getServiceIPFamily(resources.GetService(backendNamespace, string(backendRef.Name))) case egv1a1.KindBackend: ds = t.processBackendDestinationSetting(backendRef.BackendObjectReference, backendNamespace, resources) diff --git a/internal/ir/xds.go b/internal/ir/xds.go index 55afed6e007..00924ed9f32 100644 --- a/internal/ir/xds.go +++ b/internal/ir/xds.go @@ -250,19 +250,12 @@ type CoreListenerDetails struct { ExtensionRefs []*UnstructuredRef `json:"extensionRefs,omitempty" yaml:"extensionRefs,omitempty"` // Metadata is used to enrich envoy resource metadata with user and provider-specific information Metadata *ResourceMetadata `json:"metadata,omitempty" yaml:"metadata,omitempty"` - // IPFamily specifies the IP address family for the gateway. - // It can be IPv4, IPv6, or DualStack. + // IPFamily specifies the IP address family used by the Gateway for its listening ports. IPFamily *IPFamily `json:"ipFamily,omitempty" yaml:"ipFamily,omitempty"` } // IPFamily specifies the IP address family used by the Gateway for its listening ports. -type IPFamily string - -const ( - IPv4 IPFamily = "IPv4" - IPv6 IPFamily = "IPv6" - DualStack IPFamily = "DualStack" -) +type IPFamily = egv1a1.IPFamily func (l CoreListenerDetails) GetName() string { return l.Name @@ -1322,9 +1315,11 @@ type DestinationSetting struct { Endpoints []*DestinationEndpoint `json:"endpoints,omitempty" yaml:"endpoints,omitempty"` // AddressTypeState specifies the state of DestinationEndpoint address type. AddressType *DestinationAddressType `json:"addressType,omitempty" yaml:"addressType,omitempty"` - - TLS *TLSUpstreamConfig `json:"tls,omitempty" yaml:"tls,omitempty"` - Filters *DestinationFilters `json:"filters,omitempty" yaml:"filters,omitempty"` + // IPFamily specifies the IP family (IPv4 or IPv6) to use for this destination's endpoints. + // This is derived from the backend service and endpoint slice information. + IPFamily *IPFamily `json:"ipFamily,omitempty" yaml:"ipFamily,omitempty"` + TLS *TLSUpstreamConfig `json:"tls,omitempty" yaml:"tls,omitempty"` + Filters *DestinationFilters `json:"filters,omitempty" yaml:"filters,omitempty"` } // Validate the fields within the RouteDestination structure @@ -1700,6 +1695,7 @@ func (t TCPListener) Validate() error { func (t TCPRoute) Validate() error { var errs error + if t.Name == "" { errs = errors.Join(errs, ErrRouteNameEmpty) } diff --git a/internal/ir/zz_generated.deepcopy.go b/internal/ir/zz_generated.deepcopy.go index 36ca1c749ba..fbdb444e430 100644 --- a/internal/ir/zz_generated.deepcopy.go +++ b/internal/ir/zz_generated.deepcopy.go @@ -607,7 +607,7 @@ func (in *CoreListenerDetails) DeepCopyInto(out *CoreListenerDetails) { } if in.IPFamily != nil { in, out := &in.IPFamily, &out.IPFamily - *out = new(IPFamily) + *out = new(v1alpha1.IPFamily) **out = **in } } @@ -787,6 +787,11 @@ func (in *DestinationSetting) DeepCopyInto(out *DestinationSetting) { *out = new(DestinationAddressType) **out = **in } + if in.IPFamily != nil { + in, out := &in.IPFamily, &out.IPFamily + *out = new(v1alpha1.IPFamily) + **out = **in + } if in.TLS != nil { in, out := &in.TLS, &out.TLS *out = new(TLSUpstreamConfig) diff --git a/internal/provider/file/file.go b/internal/provider/file/file.go index 79ccd04e763..4dcb2c61842 100644 --- a/internal/provider/file/file.go +++ b/internal/provider/file/file.go @@ -9,36 +9,41 @@ import ( "context" "fmt" "net/http" + "os" + "path/filepath" + "strings" "time" "github.com/fsnotify/fsnotify" "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/util/sets" "sigs.k8s.io/controller-runtime/pkg/healthz" egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" "github.com/envoyproxy/gateway/internal/envoygateway/config" + "github.com/envoyproxy/gateway/internal/filewatcher" "github.com/envoyproxy/gateway/internal/message" + "github.com/envoyproxy/gateway/internal/utils/path" ) type Provider struct { paths []string logger logr.Logger - notifier *Notifier + watcher filewatcher.FileWatcher resourcesStore *resourcesStore } func New(svr *config.Server, resources *message.ProviderResources) (*Provider, error) { logger := svr.Logger.Logger - - notifier, err := NewNotifier(logger) - if err != nil { - return nil, err + paths := sets.New[string]() + if svr.EnvoyGateway.Provider.Custom.Resource.File != nil { + paths.Insert(svr.EnvoyGateway.Provider.Custom.Resource.File.Paths...) } return &Provider{ - paths: svr.EnvoyGateway.Provider.Custom.Resource.File.Paths, + paths: paths.UnsortedList(), logger: logger, - notifier: notifier, + watcher: filewatcher.NewWatcher(), resourcesStore: newResourcesStore(svr.EnvoyGateway.Gateway.ControllerName, resources, logger), }, nil } @@ -48,38 +53,91 @@ func (p *Provider) Type() egv1a1.ProviderType { } func (p *Provider) Start(ctx context.Context) error { - dirs, files, err := getDirsAndFilesForWatcher(p.paths) - if err != nil { - return fmt.Errorf("failed to get directories and files for the watcher: %w", err) - } + defer func() { + _ = p.watcher.Close() + }() // Start runnable servers. go p.startHealthProbeServer(ctx) + initDirs, initFiles := path.ListDirsAndFiles(p.paths) // Initially load resources from paths on host. - if err = p.resourcesStore.LoadAndStore(files.UnsortedList(), dirs.UnsortedList()); err != nil { + if err := p.resourcesStore.LoadAndStore(initFiles.UnsortedList(), initDirs.UnsortedList()); err != nil { return fmt.Errorf("failed to load resources into store: %w", err) } - // Start watchers in notifier. - p.notifier.Watch(ctx, dirs, files) - defer p.notifier.Close() + // Add paths to the watcher, and aggregate all path channels into one. + aggCh := make(chan fsnotify.Event) + for _, path := range p.paths { + if err := p.watcher.Add(path); err != nil { + p.logger.Error(err, "failed to add watch", "path", path) + } else { + p.logger.Info("Watching path added", "path", path) + } + + ch := p.watcher.Events(path) + go func(c chan fsnotify.Event) { + for msg := range c { + aggCh <- msg + } + }(ch) + } + curDirs, curFiles := initDirs.Clone(), initFiles.Clone() for { select { case <-ctx.Done(): return nil - case event := <-p.notifier.Events: + case event := <-aggCh: + // Ignore the irrelevant event. + if event.Has(fsnotify.Chmod) { + continue + } + + // If a file change event is detected, regardless of the event type, it will be processed + // as a Remove event if the file does not exist, and as a Write event if the file exists. + // + // The reason to do so is quite straightforward, for text edit tools like vi/vim etc. + // They always create a temporary file, remove the existing one and replace it with the + // temporary file when file is saved. So the watcher will only receive: + // - Create event, with name "filename~". + // - Remove event, with name "filename", but the file actually exist. + if initFiles.Has(event.Name) { + p.logger.Info("file changed", "op", event.Op, "name", event.Name) + + // For Write event, the file definitely exist. + if event.Has(fsnotify.Write) { + goto handle + } + + _, err := os.Lstat(event.Name) + if err != nil && os.IsNotExist(err) { + curFiles.Delete(event.Name) + } else { + curFiles.Insert(event.Name) + } + goto handle + } + + // Ignore the hidden or temporary file related change event under a directory. + if _, name := filepath.Split(event.Name); strings.HasPrefix(name, ".") || + strings.HasSuffix(name, "~") { + continue + } + p.logger.Info("file changed", "op", event.Op, "name", event.Name) + switch event.Op { - case fsnotify.Create: - dirs.Insert(event.Name) - files.Insert(event.Name) - case fsnotify.Remove: - dirs.Delete(event.Name) - files.Delete(event.Name) + case fsnotify.Create, fsnotify.Write, fsnotify.Remove: + // Since we do not watch any events in the subdirectories, any events involving files + // modifications in current directory will trigger the event handling. + goto handle + default: + // do nothing + continue } - p.resourcesStore.HandleEvent(event, files.UnsortedList(), dirs.UnsortedList()) + handle: + p.resourcesStore.HandleEvent(curFiles.UnsortedList(), curDirs.UnsortedList()) } } } diff --git a/internal/provider/file/notifier.go b/internal/provider/file/notifier.go deleted file mode 100644 index fca8465e3af..00000000000 --- a/internal/provider/file/notifier.go +++ /dev/null @@ -1,316 +0,0 @@ -// Copyright Envoy Gateway Authors -// SPDX-License-Identifier: Apache-2.0 -// The full text of the Apache license is available in the LICENSE file at -// the root of the repo. - -package file - -import ( - "context" - "os" - "path/filepath" - "strings" - "time" - - "github.com/fsnotify/fsnotify" - "github.com/go-logr/logr" - "k8s.io/apimachinery/pkg/util/sets" -) - -const ( - defaultCleanUpRemoveEventsPeriod = 300 * time.Millisecond -) - -type Notifier struct { - // Events record events used to update ResourcesStore, - // which only include two types of events: Write/Remove. - Events chan fsnotify.Event - - filesWatcher *fsnotify.Watcher - dirsWatcher *fsnotify.Watcher - cleanUpRemoveEventsPeriod time.Duration - - logger logr.Logger -} - -func NewNotifier(logger logr.Logger) (*Notifier, error) { - fw, err := fsnotify.NewBufferedWatcher(10) - if err != nil { - return nil, err - } - - dw, err := fsnotify.NewBufferedWatcher(10) - if err != nil { - return nil, err - } - - return &Notifier{ - Events: make(chan fsnotify.Event), - filesWatcher: fw, - dirsWatcher: dw, - cleanUpRemoveEventsPeriod: defaultCleanUpRemoveEventsPeriod, - logger: logger, - }, nil -} - -func (n *Notifier) Watch(ctx context.Context, dirs, files sets.Set[string]) { - n.watchDirs(ctx, dirs) - n.watchFiles(ctx, files) -} - -func (n *Notifier) Close() error { - if err := n.filesWatcher.Close(); err != nil { - return err - } - if err := n.dirsWatcher.Close(); err != nil { - return err - } - return nil -} - -// watchFiles watches one or more files, but instead of watching the file directly, -// it watches its parent directory. This solves various issues where files are -// frequently renamed. -func (n *Notifier) watchFiles(ctx context.Context, files sets.Set[string]) { - if len(files) < 1 { - return - } - - go n.runFilesWatcher(ctx, files) - - for p := range files { - if err := n.filesWatcher.Add(filepath.Dir(p)); err != nil { - n.logger.Error(err, "error adding file to notifier", "path", p) - - continue - } - } -} - -func (n *Notifier) runFilesWatcher(ctx context.Context, files sets.Set[string]) { - var ( - cleanUpTicker = time.NewTicker(n.cleanUpRemoveEventsPeriod) - - // This map records the exact previous Op of one event. - preEventOp = make(map[string]fsnotify.Op) - // This set records the name of event that related to Remove Op. - curRemoveEvents = sets.NewString() - ) - - for { - select { - case <-ctx.Done(): - return - - case err, ok := <-n.filesWatcher.Errors: - if !ok { - return - } - n.logger.Error(err, "error from files watcher in notifier") - - case event, ok := <-n.filesWatcher.Events: - if !ok { - return - } - - // Ignore file and operation the watcher not interested in. - if !files.Has(event.Name) || event.Has(fsnotify.Chmod) { - continue - } - - // This logic is trying to avoid files be removed and then created - // frequently by considering Remove/Rename and the follow Create - // Op as one Write Notifier.Event. - // - // Actually, this approach is also suitable for commands like vi/vim. - // It creates a temporary file, removes the existing one and replace - // it with the temporary file when file is saved. So instead of Write - // Op, the watcher will receive Rename and Create Op. - - var writeEvent bool - switch event.Op { - case fsnotify.Create: - if op, ok := preEventOp[event.Name]; ok && - op.Has(fsnotify.Rename) || op.Has(fsnotify.Remove) { - writeEvent = true - // If the exact previous Op of Create is Rename/Remove, - // then consider them as a Write Notifier.Event instead of Remove. - curRemoveEvents.Delete(event.Name) - } - case fsnotify.Write: - writeEvent = true - case fsnotify.Remove, fsnotify.Rename: - curRemoveEvents.Insert(event.Name) - } - - if writeEvent { - n.logger.Info("sending write event", - "name", event.Name, "watcher", "files") - - n.Events <- fsnotify.Event{ - Name: event.Name, - Op: fsnotify.Write, - } - } - preEventOp[event.Name] = event.Op - - case <-cleanUpTicker.C: - // As for collected Remove Notifier.Event, clean them up - // in a period of time to avoid neglect of dealing with - // Remove/Rename Op. - for e := range curRemoveEvents { - n.logger.Info("sending remove event", - "name", e, "watcher", "files") - - n.Events <- fsnotify.Event{ - Name: e, - Op: fsnotify.Remove, - } - } - curRemoveEvents = sets.NewString() - } - } -} - -// watchDirs watches one or more directories. -func (n *Notifier) watchDirs(ctx context.Context, dirs sets.Set[string]) { - if len(dirs) < 1 { - return - } - - // This map maintains the subdirectories ignored by each directory. - ignoredSubDirs := make(map[string]sets.Set[string]) - - for p := range dirs { - if err := n.dirsWatcher.Add(p); err != nil { - n.logger.Error(err, "error adding dir to notifier", "path", p) - - continue - } - - // Find current exist subdirectories to init ignored subdirectories set. - entries, err := os.ReadDir(p) - if err != nil { - n.logger.Error(err, "error reading dir in notifier", "path", p) - - if err = n.dirsWatcher.Remove(p); err != nil { - n.logger.Error(err, "error removing dir from notifier", "path", p) - } - - continue - } - - ignoredSubDirs[p] = sets.New[string]() - for _, entry := range entries { - if entry.IsDir() { - // The entry name is dir name, not dir path. - ignoredSubDirs[p].Insert(entry.Name()) - } - } - } - - go n.runDirsWatcher(ctx, ignoredSubDirs) -} - -func (n *Notifier) runDirsWatcher(ctx context.Context, ignoredSubDirs map[string]sets.Set[string]) { - var ( - cleanUpTicker = time.NewTicker(n.cleanUpRemoveEventsPeriod) - - // This map records the exact previous Op of one event. - preEventOp = make(map[string]fsnotify.Op) - // This set records the name of event that related to Remove Op. - curRemoveEvents = sets.NewString() - ) - - for { - select { - case <-ctx.Done(): - return - - case err, ok := <-n.dirsWatcher.Errors: - if !ok { - return - } - n.logger.Error(err, "error from dirs watcher in notifier") - - case event, ok := <-n.dirsWatcher.Events: - if !ok { - return - } - - // Ignore the hidden or temporary file related event. - _, name := filepath.Split(event.Name) - if event.Has(fsnotify.Chmod) || - strings.HasPrefix(name, ".") || - strings.HasSuffix(name, "~") { - continue - } - - // Ignore any subdirectory related event. - switch event.Op { - case fsnotify.Create: - if fi, err := os.Lstat(event.Name); err == nil && fi.IsDir() { - parentDir := filepath.Dir(event.Name) - if _, ok := ignoredSubDirs[parentDir]; ok { - ignoredSubDirs[parentDir].Insert(name) - continue - } - } - case fsnotify.Remove, fsnotify.Rename: - parentDir := filepath.Dir(event.Name) - if sub, ok := ignoredSubDirs[parentDir]; ok && sub.Has(name) { - ignoredSubDirs[parentDir].Delete(name) - continue - } - } - - // Share the similar logic as in files watcher. - var writeEvent bool - switch event.Op { - case fsnotify.Create: - if op, ok := preEventOp[event.Name]; ok && - op.Has(fsnotify.Rename) || op.Has(fsnotify.Remove) { - curRemoveEvents.Delete(event.Name) - } - // Since the watcher watches the whole dir, the creation of file - // should also be able to trigger the Write event. - writeEvent = true - case fsnotify.Write: - writeEvent = true - case fsnotify.Remove, fsnotify.Rename: - curRemoveEvents.Insert(event.Name) - } - - if writeEvent { - n.logger.Info("sending write event", - "name", event.Name, "watcher", "dirs") - - n.Events <- fsnotify.Event{ - Name: event.Name, - Op: fsnotify.Write, - } - } - preEventOp[event.Name] = event.Op - - case <-cleanUpTicker.C: - // Merge files to be removed in the same parent directory - // to suppress events, because the file has already been - // removed and is unnecessary to send event for each of them. - parentDirs := sets.NewString() - for e := range curRemoveEvents { - parentDirs.Insert(filepath.Dir(e)) - } - - for parentDir := range parentDirs { - n.logger.Info("sending remove event", - "name", parentDir, "watcher", "dirs") - - n.Events <- fsnotify.Event{ - Name: parentDir, - Op: fsnotify.Remove, - } - } - curRemoveEvents = sets.NewString() - } - } -} diff --git a/internal/provider/file/path.go b/internal/provider/file/path.go deleted file mode 100644 index fe3ad7539f6..00000000000 --- a/internal/provider/file/path.go +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright Envoy Gateway Authors -// SPDX-License-Identifier: Apache-2.0 -// The full text of the Apache license is available in the LICENSE file at -// the root of the repo. - -package file - -import ( - "os" - "path/filepath" - - "k8s.io/apimachinery/pkg/util/sets" -) - -// getDirsAndFilesForWatcher prepares dirs and files for the watcher in notifier. -func getDirsAndFilesForWatcher(paths []string) ( - dirs sets.Set[string], files sets.Set[string], err error, -) { - dirs, files = sets.New[string](), sets.New[string]() - - // Separate paths by whether is a directory or not. - paths = sets.NewString(paths...).List() - for _, path := range paths { - var p os.FileInfo - p, err = os.Lstat(path) - if err != nil { - return - } - - if p.IsDir() { - dirs.Insert(path) - } else { - files.Insert(path) - } - } - - // Ignore filepath if its parent directory is also be watched. - var ignoreFiles []string - for fp := range files { - if dirs.Has(filepath.Dir(fp)) { - ignoreFiles = append(ignoreFiles, fp) - } - } - files.Delete(ignoreFiles...) - - return -} diff --git a/internal/provider/file/store.go b/internal/provider/file/store.go index 90c520564b6..448f1807cf0 100644 --- a/internal/provider/file/store.go +++ b/internal/provider/file/store.go @@ -6,7 +6,6 @@ package file import ( - "github.com/fsnotify/fsnotify" "github.com/go-logr/logr" "github.com/envoyproxy/gateway/internal/gatewayapi/resource" @@ -28,19 +27,15 @@ func newResourcesStore(name string, resources *message.ProviderResources, logger } } -func (r *resourcesStore) HandleEvent(event fsnotify.Event, files, dirs []string) { - r.logger.Info("receive an event", "name", event.Name, "op", event.Op.String()) +// HandleEvent simply removes all the resources and triggers a resources reload from files +// and directories despite of the event type. +// TODO: Enhance this method by respecting the event type, and add support for multiple GatewayClass. +func (r *resourcesStore) HandleEvent(files, dirs []string) { + r.logger.Info("reload all resources") - // TODO(sh2): Support multiple GatewayClass. - switch event.Op { - case fsnotify.Write: - if err := r.LoadAndStore(files, dirs); err != nil { - r.logger.Error(err, "failed to load and store resources") - } - case fsnotify.Remove: - // Under our current assumption, one file only contains one GatewayClass and - // all its other related resources, so we can remove them safely. - r.resources.GatewayAPIResources.Delete(r.name) + r.resources.GatewayAPIResources.Delete(r.name) + if err := r.LoadAndStore(files, dirs); err != nil { + r.logger.Error(err, "failed to load and store resources") } } diff --git a/internal/provider/file/testdata/paths/dir/bar b/internal/provider/file/testdata/paths/dir/bar deleted file mode 100644 index e1878797a7c..00000000000 --- a/internal/provider/file/testdata/paths/dir/bar +++ /dev/null @@ -1 +0,0 @@ -THIS FILE IS FOR TEST ONLY \ No newline at end of file diff --git a/internal/provider/file/testdata/paths/foo b/internal/provider/file/testdata/paths/foo deleted file mode 100644 index e1878797a7c..00000000000 --- a/internal/provider/file/testdata/paths/foo +++ /dev/null @@ -1 +0,0 @@ -THIS FILE IS FOR TEST ONLY \ No newline at end of file diff --git a/internal/provider/kubernetes/controller.go b/internal/provider/kubernetes/controller.go index 28a0eafaa77..f71ebee9520 100644 --- a/internal/provider/kubernetes/controller.go +++ b/internal/provider/kubernetes/controller.go @@ -2088,7 +2088,7 @@ func (r *gatewayAPIReconciler) processEnvoyExtensionPolicyObjectRefs( if backendNamespace != policy.Namespace { from := ObjectKindNamespacedName{ - kind: resource.KindHTTPRoute, + kind: resource.KindEnvoyExtensionPolicy, namespace: policy.Namespace, name: policy.Name, } diff --git a/internal/provider/kubernetes/controller_test.go b/internal/provider/kubernetes/controller_test.go index c1cece27733..d008e7b2f70 100644 --- a/internal/provider/kubernetes/controller_test.go +++ b/internal/provider/kubernetes/controller_test.go @@ -12,8 +12,10 @@ import ( "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" gwapiv1 "sigs.k8s.io/gateway-api/apis/v1" + gwapiv1b1 "sigs.k8s.io/gateway-api/apis/v1beta1" egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" "github.com/envoyproxy/gateway/internal/envoygateway" @@ -288,3 +290,162 @@ func TestProcessGatewayClassParamsRef(t *testing.T) { }) } } + +func TestProcessEnvoyExtensionPolicyObjectRefs(t *testing.T) { + testCases := []struct { + name string + envoyExtensionPolicy *egv1a1.EnvoyExtensionPolicy + backend *egv1a1.Backend + referenceGrant *gwapiv1b1.ReferenceGrant + shouldBeAdded bool + }{ + { + name: "valid envoy extension policy with proper ref grant to backend", + envoyExtensionPolicy: &egv1a1.EnvoyExtensionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "ns-1", + Name: "test-policy", + }, + Spec: egv1a1.EnvoyExtensionPolicySpec{ + ExtProc: []egv1a1.ExtProc{ + { + BackendCluster: egv1a1.BackendCluster{ + BackendRefs: []egv1a1.BackendRef{ + { + BackendObjectReference: gwapiv1.BackendObjectReference{ + Namespace: gatewayapi.NamespacePtr("ns-2"), + Name: "test-backend", + Kind: gatewayapi.KindPtr(resource.KindBackend), + Group: gatewayapi.GroupPtr(egv1a1.GroupName), + }, + }, + }, + }, + }, + }, + }, + }, + backend: &egv1a1.Backend{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "ns-2", + Name: "test-backend", + }, + }, + referenceGrant: &gwapiv1b1.ReferenceGrant{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "ns-2", + Name: "test-grant", + }, + Spec: gwapiv1b1.ReferenceGrantSpec{ + From: []gwapiv1b1.ReferenceGrantFrom{ + { + Namespace: gwapiv1.Namespace("ns-1"), + Kind: gwapiv1.Kind(resource.KindEnvoyExtensionPolicy), + Group: gwapiv1.Group(egv1a1.GroupName), + }, + }, + To: []gwapiv1b1.ReferenceGrantTo{ + { + Name: gatewayapi.ObjectNamePtr("test-backend"), + Kind: gwapiv1.Kind(resource.KindBackend), + Group: gwapiv1.Group(egv1a1.GroupName), + }, + }, + }, + }, + shouldBeAdded: true, + }, + { + name: "valid envoy extension policy with wrong from kind in ref grant to backend", + envoyExtensionPolicy: &egv1a1.EnvoyExtensionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "ns-1", + Name: "test-policy", + }, + Spec: egv1a1.EnvoyExtensionPolicySpec{ + ExtProc: []egv1a1.ExtProc{ + { + BackendCluster: egv1a1.BackendCluster{ + BackendRefs: []egv1a1.BackendRef{ + { + BackendObjectReference: gwapiv1.BackendObjectReference{ + Namespace: gatewayapi.NamespacePtr("ns-2"), + Name: "test-backend", + Kind: gatewayapi.KindPtr(resource.KindBackend), + Group: gatewayapi.GroupPtr(egv1a1.GroupName), + }, + }, + }, + }, + }, + }, + }, + }, + backend: &egv1a1.Backend{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "ns-2", + Name: "test-backend", + }, + }, + referenceGrant: &gwapiv1b1.ReferenceGrant{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "ns-2", + Name: "test-grant", + }, + Spec: gwapiv1b1.ReferenceGrantSpec{ + From: []gwapiv1b1.ReferenceGrantFrom{ + { + Namespace: gwapiv1.Namespace("ns-1"), + Kind: gwapiv1.Kind(resource.KindHTTPRoute), + Group: gwapiv1.Group(gwapiv1.GroupName), + }, + }, + To: []gwapiv1b1.ReferenceGrantTo{ + { + Name: gatewayapi.ObjectNamePtr("test-backend"), + Kind: gwapiv1.Kind(resource.KindBackend), + Group: gwapiv1.Group(egv1a1.GroupName), + }, + }, + }, + }, + shouldBeAdded: false, + }, + } + + for i := range testCases { + tc := testCases[i] + // Run the test cases. + t.Run(tc.name, func(t *testing.T) { + // Add objects referenced by test cases. + objs := []client.Object{tc.envoyExtensionPolicy, tc.backend, tc.referenceGrant} + + // Create the reconciler. + logger := logging.DefaultLogger(egv1a1.LogLevelInfo) + + ctx := context.Background() + + r := &gatewayAPIReconciler{ + log: logger, + classController: "some-gateway-class", + } + + r.client = fakeclient.NewClientBuilder(). + WithScheme(envoygateway.GetScheme()). + WithObjects(objs...). + WithIndex(&gwapiv1b1.ReferenceGrant{}, targetRefGrantRouteIndex, getReferenceGrantIndexerFunc()). + Build() + + resourceTree := resource.NewResources() + resourceMap := newResourceMapping() + + err := r.processEnvoyExtensionPolicies(ctx, resourceTree, resourceMap) + require.NoError(t, err) + if tc.shouldBeAdded { + require.Contains(t, resourceTree.ReferenceGrants, tc.referenceGrant) + } else { + require.NotContains(t, resourceTree.ReferenceGrants, tc.referenceGrant) + } + }) + } +} diff --git a/internal/provider/kubernetes/indexers.go b/internal/provider/kubernetes/indexers.go index 031a2657a9c..7626ea32d52 100644 --- a/internal/provider/kubernetes/indexers.go +++ b/internal/provider/kubernetes/indexers.go @@ -52,17 +52,21 @@ const ( ) func addReferenceGrantIndexers(ctx context.Context, mgr manager.Manager) error { - if err := mgr.GetFieldIndexer().IndexField(ctx, &gwapiv1b1.ReferenceGrant{}, targetRefGrantRouteIndex, func(rawObj client.Object) []string { + if err := mgr.GetFieldIndexer().IndexField(ctx, &gwapiv1b1.ReferenceGrant{}, targetRefGrantRouteIndex, getReferenceGrantIndexerFunc()); err != nil { + return err + } + return nil +} + +func getReferenceGrantIndexerFunc() func(rawObj client.Object) []string { + return func(rawObj client.Object) []string { refGrant := rawObj.(*gwapiv1b1.ReferenceGrant) var referredServices []string for _, target := range refGrant.Spec.To { referredServices = append(referredServices, string(target.Kind)) } return referredServices - }); err != nil { - return err } - return nil } // addHTTPRouteIndexers adds indexing on HTTPRoute. diff --git a/internal/utils/path/path.go b/internal/utils/path/path.go index e333a7f5971..4291dd58848 100644 --- a/internal/utils/path/path.go +++ b/internal/utils/path/path.go @@ -8,6 +8,8 @@ package path import ( "os" "path/filepath" + + "k8s.io/apimachinery/pkg/util/sets" ) // ValidateOutputPath takes an output file path and returns it as an absolute path. @@ -22,3 +24,35 @@ func ValidateOutputPath(outputPath string) (string, error) { } return outputPath, nil } + +// ListDirsAndFiles return a list of directories and files from a list of paths recursively. +func ListDirsAndFiles(paths []string) (dirs sets.Set[string], files sets.Set[string]) { + dirs, files = sets.New[string](), sets.New[string]() + // Separate paths by whether is a directory or not. + paths = sets.NewString(paths...).UnsortedList() + for _, path := range paths { + var p os.FileInfo + p, err := os.Lstat(path) + if err != nil { + // skip + continue + } + + if p.IsDir() { + dirs.Insert(path) + } else { + files.Insert(path) + } + } + + // Ignore filepath if its parent directory is also be watched. + var ignoreFiles []string + for fp := range files { + if dirs.Has(filepath.Dir(fp)) { + ignoreFiles = append(ignoreFiles, fp) + } + } + files.Delete(ignoreFiles...) + + return +} diff --git a/internal/provider/file/path_test.go b/internal/utils/path/path_test.go similarity index 51% rename from internal/provider/file/path_test.go rename to internal/utils/path/path_test.go index 183c24efa97..8b3db14784d 100644 --- a/internal/provider/file/path_test.go +++ b/internal/utils/path/path_test.go @@ -3,17 +3,28 @@ // The full text of the Apache license is available in the LICENSE file at // the root of the repo. -package file +package path import ( + "os" "path" "testing" "github.com/stretchr/testify/require" ) -func TestGetDirsAndFilesForWatcher(t *testing.T) { - testPath := path.Join("testdata", "paths") +func TestListDirsAndFiles(t *testing.T) { + basePath, _ := os.MkdirTemp(os.TempDir(), "list-test") + defer func() { + _ = os.RemoveAll(basePath) + }() + paths, err := os.MkdirTemp(basePath, "paths") + require.NoError(t, err) + dirPath, err := os.MkdirTemp(paths, "dir") + require.NoError(t, err) + require.NoError(t, os.WriteFile(path.Join(paths, "foo"), []byte("foo"), 0o700)) // nolint: gosec + require.NoError(t, os.WriteFile(path.Join(dirPath, "bar"), []byte("bar"), 0o700)) // nolint: gosec + testCases := []struct { name string paths []string @@ -23,22 +34,23 @@ func TestGetDirsAndFilesForWatcher(t *testing.T) { { name: "get file and dir path", paths: []string{ - path.Join(testPath, "dir"), path.Join(testPath, "foo"), + dirPath, + path.Join(paths, "foo"), }, expectDirs: []string{ - path.Join(testPath, "dir"), + dirPath, }, expectFiles: []string{ - path.Join(testPath, "foo"), + path.Join(paths, "foo"), }, }, { name: "overlap file path will be ignored", paths: []string{ - path.Join(testPath, "dir"), path.Join(testPath, "dir", "bar"), + dirPath, path.Join(dirPath, "bar"), }, expectDirs: []string{ - path.Join(testPath, "dir"), + dirPath, }, expectFiles: []string{}, }, @@ -46,9 +58,9 @@ func TestGetDirsAndFilesForWatcher(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - dirs, paths, _ := getDirsAndFilesForWatcher(tc.paths) + dirs, files := ListDirsAndFiles(tc.paths) require.ElementsMatch(t, dirs.UnsortedList(), tc.expectDirs) - require.ElementsMatch(t, paths.UnsortedList(), tc.expectFiles) + require.ElementsMatch(t, files.UnsortedList(), tc.expectFiles) }) } } diff --git a/internal/xds/translator/cluster.go b/internal/xds/translator/cluster.go index c5064c29eef..10792bae24b 100644 --- a/internal/xds/translator/cluster.go +++ b/internal/xds/translator/cluster.go @@ -698,6 +698,7 @@ type ExtraArgs struct { metrics *ir.Metrics http1Settings *ir.HTTP1Settings http2Settings *ir.HTTP2Settings + ipFamily *egv1a1.IPFamily } type clusterArgs interface { @@ -716,6 +717,7 @@ func (route *UDPRouteTranslator) asClusterArgs(extra *ExtraArgs) *xdsClusterArgs endpointType: buildEndpointType(route.Destination.Settings), metrics: extra.metrics, dns: route.DNS, + ipFamily: extra.ipFamily, } } @@ -737,6 +739,7 @@ func (route *TCPRouteTranslator) asClusterArgs(extra *ExtraArgs) *xdsClusterArgs metrics: extra.metrics, backendConnection: route.BackendConnection, dns: route.DNS, + ipFamily: extra.ipFamily, } } @@ -754,6 +757,7 @@ func (httpRoute *HTTPRouteTranslator) asClusterArgs(extra *ExtraArgs) *xdsCluste http1Settings: extra.http1Settings, http2Settings: extra.http2Settings, useClientProtocol: ptr.Deref(httpRoute.UseClientProtocol, false), + ipFamily: extra.ipFamily, } // Populate traffic features. diff --git a/internal/xds/translator/listener.go b/internal/xds/translator/listener.go index 1568ed3e570..36cf9a8953b 100644 --- a/internal/xds/translator/listener.go +++ b/internal/xds/translator/listener.go @@ -180,7 +180,7 @@ func buildXdsTCPListener( }, } - if ipFamily != nil && *ipFamily == ir.DualStack { + if ipFamily != nil && *ipFamily == egv1a1.DualStack { socketAddress := listener.Address.GetSocketAddress() socketAddress.Ipv4Compat = true } @@ -224,7 +224,7 @@ func buildXdsQuicListener(name, address string, port uint32, ipFamily *ir.IPFami DrainType: listenerv3.Listener_MODIFY_ONLY, } - if ipFamily != nil && *ipFamily == ir.DualStack { + if ipFamily != nil && *ipFamily == egv1a1.DualStack { socketAddress := xdsListener.Address.GetSocketAddress() socketAddress.Ipv4Compat = true } @@ -869,7 +869,7 @@ func buildXdsUDPListener(clusterName string, udpListener *ir.UDPListener, access }}, } - if udpListener.IPFamily != nil && *udpListener.IPFamily == ir.DualStack { + if udpListener.IPFamily != nil && *udpListener.IPFamily == egv1a1.DualStack { socketAddress := xdsListener.Address.GetSocketAddress() socketAddress.Ipv4Compat = true } diff --git a/internal/xds/translator/translator.go b/internal/xds/translator/translator.go index 1e0ae77e915..79f16d5d1b5 100644 --- a/internal/xds/translator/translator.go +++ b/internal/xds/translator/translator.go @@ -464,6 +464,7 @@ func (t *Translator) addRouteToRouteConfig( ea := &ExtraArgs{ metrics: metrics, http1Settings: httpListener.HTTP1, + ipFamily: determineIPFamily(httpRoute.Destination.Settings), } if httpRoute.Traffic != nil && httpRoute.Traffic.HTTP2 != nil { diff --git a/internal/xds/translator/utils.go b/internal/xds/translator/utils.go index 882d9b1e926..30c2f771ef0 100644 --- a/internal/xds/translator/utils.go +++ b/internal/xds/translator/utils.go @@ -196,3 +196,43 @@ func addClusterFromURL(url string, tCtx *types.ResourceVersionTable) error { return addXdsCluster(tCtx, clusterArgs) } + +// determineIPFamily determines the IP family based on multiple destination settings +func determineIPFamily(settings []*ir.DestinationSetting) *egv1a1.IPFamily { + // If there's only one setting, return its IPFamily directly + if len(settings) == 1 { + return settings[0].IPFamily + } + + hasIPv4 := false + hasIPv6 := false + hasDualStack := false + + for _, setting := range settings { + if setting.IPFamily == nil { + continue + } + + switch *setting.IPFamily { + case egv1a1.IPv4: + hasIPv4 = true + case egv1a1.IPv6: + hasIPv6 = true + case egv1a1.DualStack: + hasDualStack = true + } + } + + switch { + case hasDualStack: + return ptr.To(egv1a1.DualStack) + case hasIPv4 && hasIPv6: + return ptr.To(egv1a1.DualStack) + case hasIPv4: + return ptr.To(egv1a1.IPv4) + case hasIPv6: + return ptr.To(egv1a1.IPv6) + default: + return nil + } +} diff --git a/internal/xds/translator/utils_test.go b/internal/xds/translator/utils_test.go new file mode 100644 index 00000000000..588c68690b6 --- /dev/null +++ b/internal/xds/translator/utils_test.go @@ -0,0 +1,112 @@ +// Copyright Envoy Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +package translator + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "k8s.io/utils/ptr" + + egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" + "github.com/envoyproxy/gateway/internal/ir" +) + +func TestDetermineIPFamily(t *testing.T) { + tests := []struct { + name string + settings []*ir.DestinationSetting + want *egv1a1.IPFamily + }{ + { + name: "nil settings should return nil", + settings: nil, + want: nil, + }, + { + name: "empty settings should return nil", + settings: []*ir.DestinationSetting{}, + want: nil, + }, + { + name: "single IPv4 setting", + settings: []*ir.DestinationSetting{ + {IPFamily: ptr.To(egv1a1.IPv4)}, + }, + want: ptr.To(egv1a1.IPv4), + }, + { + name: "single IPv6 setting", + settings: []*ir.DestinationSetting{ + {IPFamily: ptr.To(egv1a1.IPv6)}, + }, + want: ptr.To(egv1a1.IPv6), + }, + { + name: "single DualStack setting", + settings: []*ir.DestinationSetting{ + {IPFamily: ptr.To(egv1a1.DualStack)}, + }, + want: ptr.To(egv1a1.DualStack), + }, + { + name: "mixed IPv4 and IPv6 should return DualStack", + settings: []*ir.DestinationSetting{ + {IPFamily: ptr.To(egv1a1.IPv4)}, + {IPFamily: ptr.To(egv1a1.IPv6)}, + }, + want: ptr.To(egv1a1.DualStack), + }, + { + name: "DualStack with IPv4 should return DualStack", + settings: []*ir.DestinationSetting{ + {IPFamily: ptr.To(egv1a1.DualStack)}, + {IPFamily: ptr.To(egv1a1.IPv4)}, + }, + want: ptr.To(egv1a1.DualStack), + }, + { + name: "DualStack with IPv6 should return DualStack", + settings: []*ir.DestinationSetting{ + {IPFamily: ptr.To(egv1a1.DualStack)}, + {IPFamily: ptr.To(egv1a1.IPv6)}, + }, + want: ptr.To(egv1a1.DualStack), + }, + { + name: "mixed with nil IPFamily should be ignored", + settings: []*ir.DestinationSetting{ + {IPFamily: ptr.To(egv1a1.IPv4)}, + {IPFamily: nil}, + {IPFamily: ptr.To(egv1a1.IPv6)}, + }, + want: ptr.To(egv1a1.DualStack), + }, + { + name: "multiple IPv4 settings should return IPv4", + settings: []*ir.DestinationSetting{ + {IPFamily: ptr.To(egv1a1.IPv4)}, + {IPFamily: ptr.To(egv1a1.IPv4)}, + }, + want: ptr.To(egv1a1.IPv4), + }, + { + name: "multiple IPv6 settings should return IPv6", + settings: []*ir.DestinationSetting{ + {IPFamily: ptr.To(egv1a1.IPv6)}, + {IPFamily: ptr.To(egv1a1.IPv6)}, + }, + want: ptr.To(egv1a1.IPv6), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := determineIPFamily(tt.settings) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/release-notes/current.yaml b/release-notes/current.yaml index d1c6dd95c06..6ba9c2ee5cb 100644 --- a/release-notes/current.yaml +++ b/release-notes/current.yaml @@ -17,6 +17,7 @@ new features: | # Fixes for bugs identified in previous versions. bug fixes: | + Fixed reference grant from EnvoyExtensionPolicy to referenced ext-proc backend not respected # Enhancements that improve performance. performance improvements: | diff --git a/test/e2e/testdata/httproute-dualstack.yaml b/test/e2e/testdata/httproute-dualstack.yaml index e1289dac50e..97a79c78ac3 100644 --- a/test/e2e/testdata/httproute-dualstack.yaml +++ b/test/e2e/testdata/httproute-dualstack.yaml @@ -95,6 +95,30 @@ spec: selector: app: infra-backend-v1 --- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: infra-backend-v1-httproute-all-stacks + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: dualstack-gateway + rules: + - backendRefs: + - name: infra-backend-v1-service-ipv4 + port: 8080 + weight: 30 + - name: infra-backend-v1-service-ipv6 + port: 8080 + weight: 30 + - name: infra-backend-v1-service-dualstack + port: 8080 + weight: 40 + matches: + - path: + type: PathPrefix + value: /all-stacks +--- apiVersion: gateway.envoyproxy.io/v1alpha1 kind: EnvoyProxy metadata: @@ -119,3 +143,11 @@ spec: - name: http port: 80 protocol: HTTP +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: GatewayClass +metadata: + name: envoy-gateway + namespace: gateway-conformance-infra +spec: + controllerName: gateway.envoyproxy.io/gatewayclass-controller diff --git a/test/e2e/tests/httproute_dualstack.go b/test/e2e/tests/httproute_dualstack.go index b01fc392a12..f765b08cd1a 100644 --- a/test/e2e/tests/httproute_dualstack.go +++ b/test/e2e/tests/httproute_dualstack.go @@ -38,6 +38,9 @@ var HTTPRouteDualStackTest = suite.ConformanceTest{ t.Run("HTTPRoute to IPv4 only service", func(t *testing.T) { runHTTPRouteTest(t, suite, ns, gwNN, "infra-backend-v1-httproute-ipv4", "/ipv4-only") }) + t.Run("HTTPRoute to All-stacks services", func(t *testing.T) { + runHTTPRouteTest(t, suite, ns, gwNN, "infra-backend-v1-httproute-all-stacks", "/all-stacks") + }) }, } diff --git a/tools/src/golangci-lint/go.mod b/tools/src/golangci-lint/go.mod index d7d2cdce1f2..f84bb3bc267 100644 --- a/tools/src/golangci-lint/go.mod +++ b/tools/src/golangci-lint/go.mod @@ -2,7 +2,7 @@ module local go 1.23.3 -require github.com/golangci/golangci-lint v1.62.0 +require github.com/golangci/golangci-lint v1.62.2 require ( 4d63.com/gocheckcompilerdirectives v1.2.1 // indirect @@ -11,9 +11,9 @@ require ( github.com/Abirdcfly/dupword v0.1.3 // indirect github.com/Antonboom/errname v1.0.0 // indirect github.com/Antonboom/nilnil v1.0.0 // indirect - github.com/Antonboom/testifylint v1.5.0 // indirect + github.com/Antonboom/testifylint v1.5.2 // indirect github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect - github.com/Crocmagnon/fatcontext v0.5.2 // indirect + github.com/Crocmagnon/fatcontext v0.5.3 // indirect github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 // indirect github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.0 // indirect github.com/Masterminds/semver/v3 v3.3.0 // indirect @@ -103,19 +103,19 @@ require ( github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect - github.com/mgechev/revive v1.5.0 // indirect + github.com/mgechev/revive v1.5.1 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/moricho/tparallel v0.3.2 // indirect github.com/nakabonne/nestif v0.3.1 // indirect github.com/nishanths/exhaustive v0.12.0 // indirect github.com/nishanths/predeclared v0.2.2 // indirect - github.com/nunnatsa/ginkgolinter v0.18.0 // indirect + github.com/nunnatsa/ginkgolinter v0.18.3 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/polyfloyd/go-errorlint v1.6.0 // indirect + github.com/polyfloyd/go-errorlint v1.7.0 // indirect github.com/prometheus/client_golang v1.12.1 // indirect github.com/prometheus/client_model v0.2.0 // indirect github.com/prometheus/common v0.32.1 // indirect @@ -150,7 +150,7 @@ require ( github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect github.com/stbenjam/no-sprintf-host-port v0.1.1 // indirect github.com/stretchr/objx v0.5.2 // indirect - github.com/stretchr/testify v1.9.0 // indirect + github.com/stretchr/testify v1.10.0 // indirect github.com/subosito/gotenv v1.4.1 // indirect github.com/tdakkota/asciicheck v0.2.0 // indirect github.com/tetafro/godot v1.4.18 // indirect @@ -161,7 +161,7 @@ require ( github.com/ultraware/funlen v0.1.0 // indirect github.com/ultraware/whitespace v0.1.1 // indirect github.com/uudashr/gocognit v1.1.3 // indirect - github.com/uudashr/iface v1.2.0 // indirect + github.com/uudashr/iface v1.2.1 // indirect github.com/xen0n/gosmopolitan v1.2.2 // indirect github.com/yagipy/maintidx v1.0.0 // indirect github.com/yeya24/promlinter v0.3.0 // indirect @@ -174,7 +174,7 @@ require ( go.uber.org/multierr v1.6.0 // indirect go.uber.org/zap v1.24.0 // indirect golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect - golang.org/x/exp/typeparams v0.0.0-20240909161429-701f63a606c0 // indirect + golang.org/x/exp/typeparams v0.0.0-20241108190413-2d47ceb2692f // indirect golang.org/x/mod v0.22.0 // indirect golang.org/x/sync v0.9.0 // indirect golang.org/x/sys v0.27.0 // indirect diff --git a/tools/src/golangci-lint/go.sum b/tools/src/golangci-lint/go.sum index c8205a75d73..20fd8682dca 100644 --- a/tools/src/golangci-lint/go.sum +++ b/tools/src/golangci-lint/go.sum @@ -43,14 +43,14 @@ github.com/Antonboom/errname v1.0.0 h1:oJOOWR07vS1kRusl6YRSlat7HFnb3mSfMl6sDMRoT github.com/Antonboom/errname v1.0.0/go.mod h1:gMOBFzK/vrTiXN9Oh+HFs+e6Ndl0eTFbtsRTSRdXyGI= github.com/Antonboom/nilnil v1.0.0 h1:n+v+B12dsE5tbAqRODXmEKfZv9j2KcTBrp+LkoM4HZk= github.com/Antonboom/nilnil v1.0.0/go.mod h1:fDJ1FSFoLN6yoG65ANb1WihItf6qt9PJVTn/s2IrcII= -github.com/Antonboom/testifylint v1.5.0 h1:dlUIsDMtCrZWUnvkaCz3quJCoIjaGi41GzjPBGkkJ8A= -github.com/Antonboom/testifylint v1.5.0/go.mod h1:wqaJbu0Blb5Wag2wv7Z5xt+CIV+eVLxtGZrlK13z3AE= +github.com/Antonboom/testifylint v1.5.2 h1:4s3Xhuv5AvdIgbd8wOOEeo0uZG7PbDKQyKY5lGoQazk= +github.com/Antonboom/testifylint v1.5.2/go.mod h1:vxy8VJ0bc6NavlYqjZfmp6EfqXMtBgQ4+mhCojwC1P8= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs= github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/Crocmagnon/fatcontext v0.5.2 h1:vhSEg8Gqng8awhPju2w7MKHqMlg4/NI+gSDHtR3xgwA= -github.com/Crocmagnon/fatcontext v0.5.2/go.mod h1:87XhRMaInHP44Q7Tlc7jkgKKB7kZAOPiDkFMdKCC+74= +github.com/Crocmagnon/fatcontext v0.5.3 h1:zCh/wjc9oyeF+Gmp+V60wetm8ph2tlsxocgg/J0hOps= +github.com/Crocmagnon/fatcontext v0.5.3/go.mod h1:XoCQYY1J+XTfyv74qLXvNw4xFunr3L1wkopIIKG7wGM= github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 h1:sHglBQTwgx+rWPdisA5ynNEsoARbiCBOyGcJM4/OzsM= github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs= github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.0 h1:/fTUt5vmbkAcMBt4YQiuC23cV0kEsN1MVMNqeOW43cU= @@ -230,8 +230,8 @@ github.com/golangci/go-printf-func-name v0.1.0 h1:dVokQP+NMTO7jwO4bwsRwLWeudOVUP github.com/golangci/go-printf-func-name v0.1.0/go.mod h1:wqhWFH5mUdJQhweRnldEywnR5021wTdZSNgwYceV14s= github.com/golangci/gofmt v0.0.0-20240816233607-d8596aa466a9 h1:/1322Qns6BtQxUZDTAT4SdcoxknUki7IAoK4SAXr8ME= github.com/golangci/gofmt v0.0.0-20240816233607-d8596aa466a9/go.mod h1:Oesb/0uFAyWoaw1U1qS5zyjCg5NP9C9iwjnI4tIsXEE= -github.com/golangci/golangci-lint v1.62.0 h1:/G0g+bi1BhmGJqLdNQkKBWjcim8HjOPc4tsKuHDOhcI= -github.com/golangci/golangci-lint v1.62.0/go.mod h1:jtoOhQcKTz8B6dGNFyfQV3WZkQk+YvBDewDtNpiAJts= +github.com/golangci/golangci-lint v1.62.2 h1:b8K5K9PN+rZN1+mKLtsZHz2XXS9aYKzQ9i25x3Qnxxw= +github.com/golangci/golangci-lint v1.62.2/go.mod h1:ILWWyeFUrctpHVGMa1dg2xZPKoMUTc5OIMgW7HZr34g= github.com/golangci/misspell v0.6.0 h1:JCle2HUTNWirNlDIAUO44hUsKhOFqGPoC4LZxlaSXDs= github.com/golangci/misspell v0.6.0/go.mod h1:keMNyY6R9isGaSAu+4Q8NMBwMPkh15Gtc8UCVoDtAWo= github.com/golangci/modinfo v0.3.4 h1:oU5huX3fbxqQXdfspamej74DFX0kyGLkw1ppvXoJ8GA= @@ -369,8 +369,8 @@ github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6T github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/mgechev/revive v1.5.0 h1:oaSmjA7rP8+HyoRuCgC531VHwnLH1AlJdjj+1AnQceQ= -github.com/mgechev/revive v1.5.0/go.mod h1:L6T3H8EoerRO86c7WuGpvohIUmiploGiyoYbtIWFmV8= +github.com/mgechev/revive v1.5.1 h1:hE+QPeq0/wIzJwOphdVyUJ82njdd8Khp4fUIHGZHW3M= +github.com/mgechev/revive v1.5.1/go.mod h1:lC9AhkJIBs5zwx8wkudyHrU+IJkrEKmpCmGMnIJPk4o= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= @@ -390,8 +390,8 @@ github.com/nishanths/exhaustive v0.12.0 h1:vIY9sALmw6T/yxiASewa4TQcFsVYZQQRUQJhK github.com/nishanths/exhaustive v0.12.0/go.mod h1:mEZ95wPIZW+x8kC4TgC+9YCUgiST7ecevsVDTgc2obs= github.com/nishanths/predeclared v0.2.2 h1:V2EPdZPliZymNAn79T8RkNApBjMmVKh5XRpLm/w98Vk= github.com/nishanths/predeclared v0.2.2/go.mod h1:RROzoN6TnGQupbC+lqggsOlcgysk3LMK/HI84Mp280c= -github.com/nunnatsa/ginkgolinter v0.18.0 h1:ZXO1wKhPg3A6LpbN5dMuqwhfOjN5c3ous8YdKOuqk9k= -github.com/nunnatsa/ginkgolinter v0.18.0/go.mod h1:vPrWafSULmjMGCMsfGA908if95VnHQNAahvSBOjTuWs= +github.com/nunnatsa/ginkgolinter v0.18.3 h1:WgS7X3zzmni3vwHSBhvSgqrRgUecN6PQUcfB0j1noDw= +github.com/nunnatsa/ginkgolinter v0.18.3/go.mod h1:BE1xyB/PNtXXG1azrvrqJW5eFH0hSRylNzFy8QHPwzs= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onsi/ginkgo/v2 v2.20.2 h1:7NVCeyIWROIAheY21RLS+3j2bb52W0W82tkberYytp4= @@ -415,8 +415,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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/polyfloyd/go-errorlint v1.6.0 h1:tftWV9DE7txiFzPpztTAwyoRLKNj9gpVm2cg8/OwcYY= -github.com/polyfloyd/go-errorlint v1.6.0/go.mod h1:HR7u8wuP1kb1NeN1zqTd1ZMlqUKPPHF+Id4vIPvDqVw= +github.com/polyfloyd/go-errorlint v1.7.0 h1:Zp6lzCK4hpBDj8y8a237YK4EPrMXQWvOe3nGoH4pFrU= +github.com/polyfloyd/go-errorlint v1.7.0/go.mod h1:dGWKu85mGHnegQ2SWpEybFityCg3j7ZbwsVUxAOk9gY= github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= @@ -521,8 +521,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/tdakkota/asciicheck v0.2.0 h1:o8jvnUANo0qXtnslk2d3nMKTFNlOnJjRrNcj0j9qkHM= @@ -547,8 +547,8 @@ github.com/ultraware/whitespace v0.1.1 h1:bTPOGejYFulW3PkcrqkeQwOd6NKOOXvmGD9bo/ github.com/ultraware/whitespace v0.1.1/go.mod h1:XcP1RLD81eV4BW8UhQlpaR+SDc2givTvyI8a586WjW8= github.com/uudashr/gocognit v1.1.3 h1:l+a111VcDbKfynh+airAy/DJQKaXh2m9vkoysMPSZyM= github.com/uudashr/gocognit v1.1.3/go.mod h1:aKH8/e8xbTRBwjbCkwZ8qt4l2EpKXl31KMHgSS+lZ2U= -github.com/uudashr/iface v1.2.0 h1:ECJjh5q/1Zmnv/2yFpWV6H3oMg5+Mo+vL0aqw9Gjazo= -github.com/uudashr/iface v1.2.0/go.mod h1:Ux/7d/rAF3owK4m53cTVXL4YoVHKNqnoOeQHn2xrlp0= +github.com/uudashr/iface v1.2.1 h1:vHHyzAUmWZ64Olq6NZT3vg/z1Ws56kyPdBOd5kTXDF8= +github.com/uudashr/iface v1.2.1/go.mod h1:4QvspiRd3JLPAEXBQ9AiZpLbJlrWWgRChOKDJEuQTdg= github.com/xen0n/gosmopolitan v1.2.2 h1:/p2KTnMzwRexIW8GlKawsTWOxn7UHA+jCMF/V8HHtvU= github.com/xen0n/gosmopolitan v1.2.2/go.mod h1:7XX7Mj61uLYrj0qmeN0zi7XDon9JRAEhYQqAPLVNTeg= github.com/yagipy/maintidx v1.0.0 h1:h5NvIsCz+nRDapQ0exNv4aJ0yXSI0420omVANTv3GJM= @@ -609,8 +609,8 @@ golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWB golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= -golang.org/x/exp/typeparams v0.0.0-20240909161429-701f63a606c0 h1:bVwtbF629Xlyxk6xLQq2TDYmqP0uiWaet5LwRebuY0k= -golang.org/x/exp/typeparams v0.0.0-20240909161429-701f63a606c0/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= +golang.org/x/exp/typeparams v0.0.0-20241108190413-2d47ceb2692f h1:WTyX8eCCyfdqiPYkRGm0MqElSfYFH3yR1+rl/mct9sA= +golang.org/x/exp/typeparams v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=