Skip to content

Commit 3068c6e

Browse files
authored
Fix bug where user mounted files were being removed by agent (#4178)
Fix bug where user mounted files were being removed by agent. Problem: User mounted files are being removed by nginx agent. Solution: Mark user mounted files as unmanaged so nginx agent doesn't remove them. To do so we use agent's UpdateOverview function to get all files referenced in the nginx conf. We compare that with the user added volumeMounts and mark any files that were user mounted as unmanaged, ensuring nginx agent doesn't remove them. Testing: Added unit tests and manual testing.
1 parent 97ebb04 commit 3068c6e

File tree

10 files changed

+446
-27
lines changed

10 files changed

+446
-27
lines changed

internal/controller/handler.go

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -241,8 +241,20 @@ func (h *eventHandlerImpl) sendNginxConfig(ctx context.Context, logger logr.Logg
241241

242242
h.setLatestConfiguration(gw, &cfg)
243243

244+
vm := []v1.VolumeMount{}
245+
if gw.EffectiveNginxProxy != nil &&
246+
gw.EffectiveNginxProxy.Kubernetes != nil {
247+
if gw.EffectiveNginxProxy.Kubernetes.Deployment != nil {
248+
vm = gw.EffectiveNginxProxy.Kubernetes.Deployment.Container.VolumeMounts
249+
}
250+
251+
if gw.EffectiveNginxProxy.Kubernetes.DaemonSet != nil {
252+
vm = gw.EffectiveNginxProxy.Kubernetes.DaemonSet.Container.VolumeMounts
253+
}
254+
}
255+
244256
deployment.FileLock.Lock()
245-
h.updateNginxConf(deployment, cfg)
257+
h.updateNginxConf(deployment, cfg, vm)
246258
deployment.FileLock.Unlock()
247259

248260
configErr := deployment.GetLatestConfigError()
@@ -454,9 +466,10 @@ func (h *eventHandlerImpl) parseAndCaptureEvent(ctx context.Context, logger logr
454466
func (h *eventHandlerImpl) updateNginxConf(
455467
deployment *agent.Deployment,
456468
conf dataplane.Configuration,
469+
volumeMounts []v1.VolumeMount,
457470
) {
458471
files := h.cfg.generator.Generate(conf)
459-
h.cfg.nginxUpdater.UpdateConfig(deployment, files)
472+
h.cfg.nginxUpdater.UpdateConfig(deployment, files, volumeMounts)
460473

461474
// If using NGINX Plus, update upstream servers using the API.
462475
if h.cfg.plus {

internal/controller/handler_test.go

Lines changed: 100 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
2323

2424
ngfAPI "github.com/nginx/nginx-gateway-fabric/v2/apis/v1alpha1"
25+
"github.com/nginx/nginx-gateway-fabric/v2/apis/v1alpha2"
2526
"github.com/nginx/nginx-gateway-fabric/v2/internal/controller/config"
2627
"github.com/nginx/nginx-gateway-fabric/v2/internal/controller/licensing/licensingfakes"
2728
"github.com/nginx/nginx-gateway-fabric/v2/internal/controller/metrics/collectors"
@@ -66,7 +67,7 @@ var _ = Describe("eventHandler", func() {
6667
Expect(fakeGenerator.GenerateArgsForCall(0)).Should(Equal(expectedConf))
6768

6869
Expect(fakeNginxUpdater.UpdateConfigCallCount()).Should(Equal(1))
69-
_, files := fakeNginxUpdater.UpdateConfigArgsForCall(0)
70+
_, files, _ := fakeNginxUpdater.UpdateConfigArgsForCall(0)
7071
Expect(expectedFiles).To(Equal(files))
7172

7273
Eventually(
@@ -642,6 +643,104 @@ var _ = Describe("eventHandler", func() {
642643

643644
Expect(handler.GetLatestConfiguration()).To(BeEmpty())
644645
})
646+
647+
It("should process events with volume mounts from Deployment", func() {
648+
// Create a gateway with EffectiveNginxProxy containing Deployment VolumeMounts
649+
gatewayWithVolumeMounts := &graph.Graph{
650+
Gateways: map[types.NamespacedName]*graph.Gateway{
651+
{Namespace: "test", Name: "gateway"}: {
652+
Valid: true,
653+
Source: &gatewayv1.Gateway{
654+
ObjectMeta: metav1.ObjectMeta{
655+
Name: "gateway",
656+
Namespace: "test",
657+
},
658+
},
659+
DeploymentName: types.NamespacedName{
660+
Namespace: "test",
661+
Name: controller.CreateNginxResourceName("gateway", "nginx"),
662+
},
663+
EffectiveNginxProxy: &graph.EffectiveNginxProxy{
664+
Kubernetes: &v1alpha2.KubernetesSpec{
665+
Deployment: &v1alpha2.DeploymentSpec{
666+
Container: v1alpha2.ContainerSpec{
667+
VolumeMounts: []v1.VolumeMount{
668+
{
669+
Name: "test-volume",
670+
MountPath: "/etc/test",
671+
},
672+
},
673+
},
674+
},
675+
},
676+
},
677+
},
678+
},
679+
}
680+
681+
fakeProcessor.ProcessReturns(gatewayWithVolumeMounts)
682+
683+
e := &events.UpsertEvent{Resource: &gatewayv1.HTTPRoute{}}
684+
batch := []interface{}{e}
685+
686+
handler.HandleEventBatch(context.Background(), logr.Discard(), batch)
687+
688+
// Verify that UpdateConfig was called with the volume mounts
689+
Expect(fakeNginxUpdater.UpdateConfigCallCount()).Should(Equal(1))
690+
_, _, volumeMounts := fakeNginxUpdater.UpdateConfigArgsForCall(0)
691+
Expect(volumeMounts).To(HaveLen(1))
692+
Expect(volumeMounts[0].Name).To(Equal("test-volume"))
693+
Expect(volumeMounts[0].MountPath).To(Equal("/etc/test"))
694+
})
695+
696+
It("should process events with volume mounts from DaemonSet", func() {
697+
// Create a gateway with EffectiveNginxProxy containing DaemonSet VolumeMounts
698+
gatewayWithVolumeMounts := &graph.Graph{
699+
Gateways: map[types.NamespacedName]*graph.Gateway{
700+
{Namespace: "test", Name: "gateway"}: {
701+
Valid: true,
702+
Source: &gatewayv1.Gateway{
703+
ObjectMeta: metav1.ObjectMeta{
704+
Name: "gateway",
705+
Namespace: "test",
706+
},
707+
},
708+
DeploymentName: types.NamespacedName{
709+
Namespace: "test",
710+
Name: controller.CreateNginxResourceName("gateway", "nginx"),
711+
},
712+
EffectiveNginxProxy: &graph.EffectiveNginxProxy{
713+
Kubernetes: &v1alpha2.KubernetesSpec{
714+
DaemonSet: &v1alpha2.DaemonSetSpec{
715+
Container: v1alpha2.ContainerSpec{
716+
VolumeMounts: []v1.VolumeMount{
717+
{
718+
Name: "daemon-volume",
719+
MountPath: "/var/daemon",
720+
},
721+
},
722+
},
723+
},
724+
},
725+
},
726+
},
727+
},
728+
}
729+
730+
fakeProcessor.ProcessReturns(gatewayWithVolumeMounts)
731+
732+
e := &events.UpsertEvent{Resource: &gatewayv1.HTTPRoute{}}
733+
batch := []interface{}{e}
734+
735+
handler.HandleEventBatch(context.Background(), logr.Discard(), batch)
736+
737+
// Verify that UpdateConfig was called with the volume mounts
738+
Expect(fakeNginxUpdater.UpdateConfigCallCount()).Should(Equal(1))
739+
_, _, volumeMounts := fakeNginxUpdater.UpdateConfigArgsForCall(0)
740+
Expect(volumeMounts).To(HaveLen(1))
741+
Expect(volumeMounts[0].Name).To(Equal("daemon-volume"))
742+
Expect(volumeMounts[0].MountPath).To(Equal("/var/daemon"))
743+
})
645744
})
646745

647746
var _ = Describe("getGatewayAddresses", func() {

internal/controller/nginx/agent/agent.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/go-logr/logr"
1111
pb "github.com/nginx/agent/v3/api/grpc/mpi/v1"
1212
"google.golang.org/protobuf/types/known/structpb"
13+
v1 "k8s.io/api/core/v1"
1314
"k8s.io/apimachinery/pkg/util/wait"
1415
"sigs.k8s.io/controller-runtime/pkg/client"
1516

@@ -29,7 +30,7 @@ const retryUpstreamTimeout = 5 * time.Second
2930

3031
// NginxUpdater is an interface for updating NGINX using the NGINX agent.
3132
type NginxUpdater interface {
32-
UpdateConfig(deployment *Deployment, files []File)
33+
UpdateConfig(deployment *Deployment, files []File, volumeMounts []v1.VolumeMount)
3334
UpdateUpstreamServers(deployment *Deployment, conf dataplane.Configuration)
3435
}
3536

@@ -87,8 +88,9 @@ func NewNginxUpdater(
8788
func (n *NginxUpdaterImpl) UpdateConfig(
8889
deployment *Deployment,
8990
files []File,
91+
volumeMounts []v1.VolumeMount,
9092
) {
91-
msg := deployment.SetFiles(files)
93+
msg := deployment.SetFiles(files, volumeMounts)
9294
if msg == nil {
9395
n.logger.V(1).Info("No changes to nginx configuration files, not sending to agent")
9496
return

internal/controller/nginx/agent/agent_test.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
pb "github.com/nginx/agent/v3/api/grpc/mpi/v1"
1010
. "github.com/onsi/gomega"
1111
"google.golang.org/protobuf/types/known/structpb"
12+
v1 "k8s.io/api/core/v1"
1213
"sigs.k8s.io/controller-runtime/pkg/client/fake"
1314

1415
"github.com/nginx/nginx-gateway-fabric/v2/internal/controller/nginx/agent/broadcast/broadcastfakes"
@@ -63,7 +64,7 @@ func TestUpdateConfig(t *testing.T) {
6364
deployment.SetPodErrorStatus("pod1", testErr)
6465
}
6566

66-
updater.UpdateConfig(deployment, []File{file})
67+
updater.UpdateConfig(deployment, []File{file}, []v1.VolumeMount{})
6768

6869
g.Expect(fakeBroadcaster.SendCallCount()).To(Equal(1))
6970
fileContents, _ := deployment.GetFile(file.Meta.Name, file.Meta.Hash)
@@ -74,7 +75,7 @@ func TestUpdateConfig(t *testing.T) {
7475
// ensure that the error is cleared after the next config is applied
7576
deployment.SetPodErrorStatus("pod1", nil)
7677
file.Meta.Hash = "5678"
77-
updater.UpdateConfig(deployment, []File{file})
78+
updater.UpdateConfig(deployment, []File{file}, []v1.VolumeMount{})
7879
g.Expect(deployment.GetLatestConfigError()).ToNot(HaveOccurred())
7980
} else {
8081
g.Expect(deployment.GetLatestConfigError()).ToNot(HaveOccurred())
@@ -105,10 +106,10 @@ func TestUpdateConfig_NoChange(t *testing.T) {
105106
}
106107

107108
// Set the initial files on the deployment
108-
deployment.SetFiles([]File{file})
109+
deployment.SetFiles([]File{file}, []v1.VolumeMount{})
109110

110111
// Call UpdateConfig with the same files
111-
updater.UpdateConfig(deployment, []File{file})
112+
updater.UpdateConfig(deployment, []File{file}, []v1.VolumeMount{})
112113

113114
// Verify that no new configuration was sent
114115
g.Expect(fakeBroadcaster.SendCallCount()).To(Equal(0))

internal/controller/nginx/agent/agentfakes/fake_nginx_updater.go

Lines changed: 16 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/controller/nginx/agent/command_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -342,7 +342,7 @@ func TestSubscribe(t *testing.T) {
342342
Contents: []byte("file contents"),
343343
},
344344
}
345-
deployment.SetFiles(files)
345+
deployment.SetFiles(files, []v1.VolumeMount{})
346346
deployment.SetImageVersion("nginx:v1.0.0")
347347

348348
initialAction := &pb.NGINXPlusAction{
@@ -488,7 +488,7 @@ func TestSubscribe_Reset(t *testing.T) {
488488
Contents: []byte("file contents"),
489489
},
490490
}
491-
deployment.SetFiles(files)
491+
deployment.SetFiles(files, []v1.VolumeMount{})
492492
deployment.SetImageVersion("nginx:v1.0.0")
493493

494494
ctx, cancel := createGrpcContextWithCancel()

internal/controller/nginx/agent/deployment.go

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ import (
44
"context"
55
"errors"
66
"fmt"
7+
"strings"
78
"sync"
89

910
pb "github.com/nginx/agent/v3/api/grpc/mpi/v1"
1011
filesHelper "github.com/nginx/agent/v3/pkg/files"
12+
v1 "k8s.io/api/core/v1"
1113
"k8s.io/apimachinery/pkg/types"
1214

1315
"github.com/nginx/nginx-gateway-fabric/v2/internal/controller/nginx/agent/broadcast"
@@ -58,6 +60,8 @@ type Deployment struct {
5860
fileOverviews []*pb.File
5961
files []File
6062

63+
latestFileNames []string
64+
6165
FileLock sync.RWMutex
6266
errLock sync.RWMutex
6367
}
@@ -187,16 +191,31 @@ func (d *Deployment) GetFile(name, hash string) ([]byte, string) {
187191

188192
// SetFiles updates the nginx files and fileOverviews for the deployment and returns the message to send.
189193
// The deployment FileLock MUST already be locked before calling this function.
190-
func (d *Deployment) SetFiles(files []File) *broadcast.NginxAgentMessage {
194+
func (d *Deployment) SetFiles(files []File, volumeMounts []v1.VolumeMount) *broadcast.NginxAgentMessage {
191195
d.files = files
192196

193197
fileOverviews := make([]*pb.File, 0, len(files))
194198
for _, file := range files {
195199
fileOverviews = append(fileOverviews, &pb.File{FileMeta: file.Meta})
196200
}
197201

202+
// To avoid duplicates, use a set for volume ignore files
203+
volumeIgnoreSet := make(map[string]struct{}, len(d.latestFileNames))
204+
for _, vm := range volumeMounts {
205+
for _, f := range d.latestFileNames {
206+
if strings.HasPrefix(f, vm.MountPath) {
207+
volumeIgnoreSet[f] = struct{}{}
208+
}
209+
}
210+
}
211+
212+
volumeIgnoreFiles := make([]string, 0, len(volumeIgnoreSet))
213+
for f := range volumeIgnoreSet {
214+
volumeIgnoreFiles = append(volumeIgnoreFiles, f)
215+
}
216+
198217
// add ignored files to the overview as 'unmanaged' so agent doesn't touch them
199-
for _, f := range ignoreFiles {
218+
for _, f := range append(ignoreFiles, volumeIgnoreFiles...) {
200219
meta := &pb.FileMeta{
201220
Name: f,
202221
Permissions: fileMode,

0 commit comments

Comments
 (0)