Skip to content

Support sending chunked files to agent #3390

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 22, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build/Dockerfile.nginx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ WORKDIR /tmp
RUN apk add --no-cache git make \
&& git clone https://github.com/nginx/agent.git \
&& cd agent \
&& git checkout e745a3236e0f02a579461a5a435b3bcd410a686c \
&& git checkout 9b574fa90848c9a7c123e6e7e6153ccd602ae724 \
&& make build

FROM nginx:1.28.0-alpine-otel
Expand Down
2 changes: 1 addition & 1 deletion build/Dockerfile.nginxplus
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ WORKDIR /tmp
RUN apk add --no-cache git make \
&& git clone https://github.com/nginx/agent.git \
&& cd agent \
&& git checkout e745a3236e0f02a579461a5a435b3bcd410a686c \
&& git checkout 9b574fa90848c9a7c123e6e7e6153ccd602ae724 \
&& make build

FROM alpine:3.21
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ require (
github.com/go-logr/logr v1.4.2
github.com/google/go-cmp v0.7.0
github.com/google/uuid v1.6.0
github.com/nginx/agent/v3 v3.0.0-20250513105855-e745a3236e0f
github.com/nginx/agent/v3 v3.0.0-20250520100419-9b574fa90848
github.com/nginx/telemetry-exporter v0.1.4
github.com/onsi/ginkgo/v2 v2.23.4
github.com/onsi/gomega v1.37.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nginx/agent/v3 v3.0.0-20250513105855-e745a3236e0f h1:fSUAaR1AxmmbmGMRkvKGY2+LhuVpBp7tbBFLLgDMjNQ=
github.com/nginx/agent/v3 v3.0.0-20250513105855-e745a3236e0f/go.mod h1:O/31aKtii/mpiZmFGMcTNDoLtKzwTyTXOBMSRkMaPvs=
github.com/nginx/agent/v3 v3.0.0-20250520100419-9b574fa90848 h1:BZ5WY30Ojw/+/SmmvsdbM7SXuEpUy9zgBkuMSVet540=
github.com/nginx/agent/v3 v3.0.0-20250520100419-9b574fa90848/go.mod h1:O/31aKtii/mpiZmFGMcTNDoLtKzwTyTXOBMSRkMaPvs=
github.com/nginx/telemetry-exporter v0.1.4 h1:3ikgKlyz/O57oaBLkxCInMjr74AhGTKr9rHdRAkkl/w=
github.com/nginx/telemetry-exporter v0.1.4/go.mod h1:bl6qmsxgk4a9D0X8R5E3sUNXN2iECPEK1JNbRLhN5C4=
github.com/nginxinc/nginx-plus-go-client/v2 v2.0.1 h1:5VVK38bnELMDWnwfF6dSv57ResXh9AUzeDa72ENj94o=
Expand Down
6 changes: 3 additions & 3 deletions internal/mode/static/nginx/agent/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -522,9 +522,9 @@ func getNginxInstanceID(instances []*pb.Instance) string {
}

// UpdateDataPlaneHealth includes full health information about the data plane as reported by the agent.
func (cs *commandService) UpdateDataPlaneHealth(
_ context.Context,
_ *pb.UpdateDataPlaneHealthRequest,
func (*commandService) UpdateDataPlaneHealth(
context.Context,
*pb.UpdateDataPlaneHealthRequest,
) (*pb.UpdateDataPlaneHealthResponse, error) {
return &pb.UpdateDataPlaneHealthResponse{}, nil
}
121 changes: 99 additions & 22 deletions internal/mode/static/nginx/agent/file.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package agent

import (
"bytes"
"context"
"math"

"github.com/go-logr/logr"
pb "github.com/nginx/agent/v3/api/grpc/mpi/v1"
"github.com/nginx/agent/v3/pkg/files"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
Expand All @@ -13,6 +16,8 @@
grpcContext "github.com/nginx/nginx-gateway-fabric/internal/mode/static/nginx/agent/grpc/context"
)

const defaultChunkSize uint32 = 2097152 // 2MB

// File is an nginx configuration file that the nginx agent gets from the control plane
// after a ConfigApplyRequest.
type File struct {
Expand Down Expand Up @@ -51,15 +56,83 @@
ctx context.Context,
req *pb.GetFileRequest,
) (*pb.GetFileResponse, error) {
filename := req.GetFileMeta().GetName()
hash := req.GetFileMeta().GetHash()

gi, ok := grpcContext.GrpcInfoFromContext(ctx)
if !ok {
return nil, agentgrpc.ErrStatusInvalidConnection
}

conn := fs.connTracker.GetConnection(gi.IPAddress)
if req.GetFileMeta() == nil {
return nil, status.Error(codes.InvalidArgument, "invalid request")
}

contents, err := fs.getFileContents(req, gi.IPAddress)
if err != nil {
return nil, err
}

return &pb.GetFileResponse{
Contents: &pb.FileContents{
Contents: contents,
},
}, nil
}

// GetFileStream is called by the agent when it needs to download a file in chunks for a ConfigApplyRequest.
// The deployment object used to get the files is already LOCKED when this function is called,
// before the ConfigApply transaction is started.
func (fs *fileService) GetFileStream(
req *pb.GetFileRequest,
server grpc.ServerStreamingServer[pb.FileDataChunk],
) error {
gi, ok := grpcContext.GrpcInfoFromContext(server.Context())
if !ok {
return agentgrpc.ErrStatusInvalidConnection
}

if req.GetFileMeta() == nil || req.GetMessageMeta() == nil {
return status.Error(codes.InvalidArgument, "invalid request")
}

contents, err := fs.getFileContents(req, gi.IPAddress)
if err != nil {
return err
}

Check warning on line 99 in internal/mode/static/nginx/agent/file.go

View check run for this annotation

Codecov / codecov/patch

internal/mode/static/nginx/agent/file.go#L98-L99

Added lines #L98 - L99 were not covered by tests

size := req.GetFileMeta().GetSize()
var sizeUint32 uint32
if size > math.MaxUint32 {
return status.Error(codes.Internal, "file size is too large and cannot be converted to uint32")
}

Check warning on line 105 in internal/mode/static/nginx/agent/file.go

View check run for this annotation

Codecov / codecov/patch

internal/mode/static/nginx/agent/file.go#L104-L105

Added lines #L104 - L105 were not covered by tests
sizeUint32 = uint32(size) //nolint:gosec // validation check performed on previous line
hash := req.GetFileMeta().GetHash()

fs.logger.V(1).Info("Sending chunked file to agent", "file", req.GetFileMeta().GetName())

if err := files.SendChunkedFile(
req.GetMessageMeta(),
pb.FileDataChunk_Header{
Header: &pb.FileDataChunkHeader{
ChunkSize: defaultChunkSize,
Chunks: calculateChunks(sizeUint32, defaultChunkSize),
FileMeta: &pb.FileMeta{
Name: req.GetFileMeta().GetName(),
Hash: hash,
Permissions: req.GetFileMeta().GetPermissions(),
Size: size,
},
},
},
bytes.NewReader(contents),
server,
); err != nil {
return status.Error(codes.Aborted, err.Error())
}

Check warning on line 129 in internal/mode/static/nginx/agent/file.go

View check run for this annotation

Codecov / codecov/patch

internal/mode/static/nginx/agent/file.go#L128-L129

Added lines #L128 - L129 were not covered by tests

return nil
}

func (fs *fileService) getFileContents(req *pb.GetFileRequest, connKey string) ([]byte, error) {
conn := fs.connTracker.GetConnection(connKey)
if conn.PodName == "" {
return nil, status.Errorf(codes.NotFound, "connection not found")
}
Expand All @@ -69,43 +142,47 @@
return nil, status.Errorf(codes.NotFound, "deployment not found in store")
}

contents := deployment.GetFile(filename, hash)
filename := req.GetFileMeta().GetName()
contents := deployment.GetFile(filename, req.GetFileMeta().GetHash())
if len(contents) == 0 {
return nil, status.Errorf(codes.NotFound, "file not found")
}

fs.logger.V(1).Info("Getting file for agent", "file", filename)

return &pb.GetFileResponse{
Contents: &pb.FileContents{
Contents: contents,
},
}, nil
return contents, nil
}

func calculateChunks(fileSize uint32, chunkSize uint32) uint32 {
remainder, divide := fileSize%chunkSize, fileSize/chunkSize
if remainder > 0 {
return divide + 1
}
// if fileSize is divisible by chunkSize without remainder
// then we don't need the extra chunk for the remainder
return divide

Check warning on line 163 in internal/mode/static/nginx/agent/file.go

View check run for this annotation

Codecov / codecov/patch

internal/mode/static/nginx/agent/file.go#L163

Added line #L163 was not covered by tests
}

// GetOverview gets the overview of files for a particular configuration version of an instance.
// At the moment it doesn't appear to be used by the agent.
func (fs *fileService) GetOverview(
_ context.Context,
_ *pb.GetOverviewRequest,
) (*pb.GetOverviewResponse, error) {
func (*fileService) GetOverview(context.Context, *pb.GetOverviewRequest) (*pb.GetOverviewResponse, error) {
return &pb.GetOverviewResponse{}, nil
}

// UpdateOverview is called by agent on startup and whenever any files change on the instance.
// Since directly changing nginx configuration on the instance is not supported, this is a no-op for NGF.
func (fs *fileService) UpdateOverview(
_ context.Context,
_ *pb.UpdateOverviewRequest,
) (*pb.UpdateOverviewResponse, error) {
func (*fileService) UpdateOverview(context.Context, *pb.UpdateOverviewRequest) (*pb.UpdateOverviewResponse, error) {
return &pb.UpdateOverviewResponse{}, nil
}

// UpdateFile is called by agent whenever any files change on the instance.
// Since directly changing nginx configuration on the instance is not supported, this is a no-op for NGF.
func (fs *fileService) UpdateFile(
_ context.Context,
_ *pb.UpdateFileRequest,
) (*pb.UpdateFileResponse, error) {
func (*fileService) UpdateFile(context.Context, *pb.UpdateFileRequest) (*pb.UpdateFileResponse, error) {
return &pb.UpdateFileResponse{}, nil
}

// UpdateFileStream is called by agent whenever any files change on the instance.
// Since directly changing nginx configuration on the instance is not supported, this is a no-op for NGF.
func (*fileService) UpdateFileStream(grpc.ClientStreamingServer[pb.FileDataChunk, pb.UpdateFileResponse]) error {
return nil
}
Loading
Loading