diff --git a/api/grpc/mpi/v1/command.pb.go b/api/grpc/mpi/v1/command.pb.go index 31e94ea7d..3770775cc 100644 --- a/api/grpc/mpi/v1/command.pb.go +++ b/api/grpc/mpi/v1/command.pb.go @@ -8,7 +8,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.10 +// protoc-gen-go v1.36.11 // protoc (unknown) // source: mpi/v1/command.proto diff --git a/api/grpc/mpi/v1/command_grpc.pb.go b/api/grpc/mpi/v1/command_grpc.pb.go index dbf61a337..ba20831d9 100644 --- a/api/grpc/mpi/v1/command_grpc.pb.go +++ b/api/grpc/mpi/v1/command_grpc.pb.go @@ -8,7 +8,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: -// - protoc-gen-go-grpc v1.5.1 +// - protoc-gen-go-grpc v1.6.0 // - protoc (unknown) // source: mpi/v1/command.proto @@ -144,16 +144,16 @@ type CommandServiceServer interface { type UnimplementedCommandServiceServer struct{} func (UnimplementedCommandServiceServer) CreateConnection(context.Context, *CreateConnectionRequest) (*CreateConnectionResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method CreateConnection not implemented") + return nil, status.Error(codes.Unimplemented, "method CreateConnection not implemented") } func (UnimplementedCommandServiceServer) UpdateDataPlaneStatus(context.Context, *UpdateDataPlaneStatusRequest) (*UpdateDataPlaneStatusResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method UpdateDataPlaneStatus not implemented") + return nil, status.Error(codes.Unimplemented, "method UpdateDataPlaneStatus not implemented") } func (UnimplementedCommandServiceServer) UpdateDataPlaneHealth(context.Context, *UpdateDataPlaneHealthRequest) (*UpdateDataPlaneHealthResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method UpdateDataPlaneHealth not implemented") + return nil, status.Error(codes.Unimplemented, "method UpdateDataPlaneHealth not implemented") } func (UnimplementedCommandServiceServer) Subscribe(grpc.BidiStreamingServer[DataPlaneResponse, ManagementPlaneRequest]) error { - return status.Errorf(codes.Unimplemented, "method Subscribe not implemented") + return status.Error(codes.Unimplemented, "method Subscribe not implemented") } func (UnimplementedCommandServiceServer) testEmbeddedByValue() {} @@ -165,7 +165,7 @@ type UnsafeCommandServiceServer interface { } func RegisterCommandServiceServer(s grpc.ServiceRegistrar, srv CommandServiceServer) { - // If the following call pancis, it indicates UnimplementedCommandServiceServer was + // If the following call panics, it indicates UnimplementedCommandServiceServer was // embedded by pointer and is nil. This will cause panics if an // unimplemented method is ever invoked, so we test this at initialization // time to prevent it from happening at runtime later due to I/O. diff --git a/api/grpc/mpi/v1/common.pb.go b/api/grpc/mpi/v1/common.pb.go index b59f47b5a..a4fc36217 100644 --- a/api/grpc/mpi/v1/common.pb.go +++ b/api/grpc/mpi/v1/common.pb.go @@ -5,7 +5,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.10 +// protoc-gen-go v1.36.11 // protoc (unknown) // source: mpi/v1/common.proto diff --git a/api/grpc/mpi/v1/files.pb.go b/api/grpc/mpi/v1/files.pb.go index 0223bae3a..57fdff253 100644 --- a/api/grpc/mpi/v1/files.pb.go +++ b/api/grpc/mpi/v1/files.pb.go @@ -5,7 +5,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.10 +// protoc-gen-go v1.36.11 // protoc (unknown) // source: mpi/v1/files.proto diff --git a/api/grpc/mpi/v1/files_grpc.pb.go b/api/grpc/mpi/v1/files_grpc.pb.go index 69efda491..80ed54f75 100644 --- a/api/grpc/mpi/v1/files_grpc.pb.go +++ b/api/grpc/mpi/v1/files_grpc.pb.go @@ -5,7 +5,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: -// - protoc-gen-go-grpc v1.5.1 +// - protoc-gen-go-grpc v1.6.0 // - protoc (unknown) // source: mpi/v1/files.proto @@ -174,22 +174,22 @@ type FileServiceServer interface { type UnimplementedFileServiceServer struct{} func (UnimplementedFileServiceServer) GetOverview(context.Context, *GetOverviewRequest) (*GetOverviewResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method GetOverview not implemented") + return nil, status.Error(codes.Unimplemented, "method GetOverview not implemented") } func (UnimplementedFileServiceServer) UpdateOverview(context.Context, *UpdateOverviewRequest) (*UpdateOverviewResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method UpdateOverview not implemented") + return nil, status.Error(codes.Unimplemented, "method UpdateOverview not implemented") } func (UnimplementedFileServiceServer) GetFile(context.Context, *GetFileRequest) (*GetFileResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method GetFile not implemented") + return nil, status.Error(codes.Unimplemented, "method GetFile not implemented") } func (UnimplementedFileServiceServer) UpdateFile(context.Context, *UpdateFileRequest) (*UpdateFileResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method UpdateFile not implemented") + return nil, status.Error(codes.Unimplemented, "method UpdateFile not implemented") } func (UnimplementedFileServiceServer) GetFileStream(*GetFileRequest, grpc.ServerStreamingServer[FileDataChunk]) error { - return status.Errorf(codes.Unimplemented, "method GetFileStream not implemented") + return status.Error(codes.Unimplemented, "method GetFileStream not implemented") } func (UnimplementedFileServiceServer) UpdateFileStream(grpc.ClientStreamingServer[FileDataChunk, UpdateFileResponse]) error { - return status.Errorf(codes.Unimplemented, "method UpdateFileStream not implemented") + return status.Error(codes.Unimplemented, "method UpdateFileStream not implemented") } func (UnimplementedFileServiceServer) testEmbeddedByValue() {} @@ -201,7 +201,7 @@ type UnsafeFileServiceServer interface { } func RegisterFileServiceServer(s grpc.ServiceRegistrar, srv FileServiceServer) { - // If the following call pancis, it indicates UnimplementedFileServiceServer was + // If the following call panics, it indicates UnimplementedFileServiceServer was // embedded by pointer and is nil. This will cause panics if an // unimplemented method is ever invoked, so we test this at initialization // time to prevent it from happening at runtime later due to I/O. diff --git a/go.mod b/go.mod index edac1a238..e1517e0c2 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,10 @@ toolchain go1.24.10 require ( buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.9-20250912141014-52f32327d4b0.1 buf.build/go/protovalidate v1.0.0 + cloud.google.com/go/storage v1.58.0 + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.0 + github.com/aws/aws-sdk-go-v2/config v1.31.15 + github.com/aws/aws-sdk-go-v2/service/s3 v1.94.0 github.com/cenkalti/backoff/v4 v4.3.0 github.com/docker/docker v28.4.0+incompatible github.com/fsnotify/fsnotify v1.9.0 @@ -81,24 +85,30 @@ require ( go.uber.org/multierr v1.11.0 go.uber.org/zap v1.27.0 golang.org/x/mod v0.29.0 - golang.org/x/sync v0.17.0 + golang.org/x/sync v0.18.0 + google.golang.org/api v0.256.0 google.golang.org/protobuf v1.36.10 ) require ( cel.dev/expr v0.24.0 // indirect - cloud.google.com/go/auth v0.16.5 // indirect + cloud.google.com/go v0.123.0 // indirect + cloud.google.com/go/auth v0.17.0 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect + cloud.google.com/go/iam v1.5.3 // indirect + cloud.google.com/go/monitoring v1.24.2 // indirect dario.cat/mergo v1.0.2 // indirect github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 // indirect - github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.12.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0 // indirect github.com/DataDog/datadog-agent/pkg/obfuscate v0.73.0-devel.0.20251030121902-cd89eab046d6 // indirect github.com/DataDog/datadog-go/v5 v5.8.1 // indirect github.com/DataDog/go-sqllexer v0.1.9 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 // indirect github.com/HdrHistogram/hdrhistogram-go v1.1.2 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/alecthomas/participle/v2 v2.1.4 // indirect @@ -108,19 +118,22 @@ require ( github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/apache/arrow-go/v18 v18.2.0 // indirect github.com/apache/thrift v0.22.0 // indirect - github.com/aws/aws-sdk-go-v2 v1.39.4 // indirect - github.com/aws/aws-sdk-go-v2/config v1.31.15 // indirect + github.com/aws/aws-sdk-go-v2 v1.41.0 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.18.19 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.11 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.11 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.11 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.2 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.11 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.29.8 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.3 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.38.9 // indirect - github.com/aws/smithy-go v1.23.1 // indirect + github.com/aws/smithy-go v1.24.0 // indirect github.com/axiomhq/hyperloglog v0.2.5 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bmatcuk/doublestar/v4 v4.9.1 // indirect @@ -129,6 +142,7 @@ require ( github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect + github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/log v0.1.0 // indirect @@ -144,12 +158,15 @@ require ( github.com/ebitengine/purego v0.9.0 // indirect github.com/elastic/go-grok v0.3.1 // indirect github.com/elastic/lunes v0.1.0 // indirect + github.com/envoyproxy/go-control-plane/envoy v1.35.0 // indirect + github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect github.com/expr-lang/expr v1.17.6 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/foxboron/go-tpm-keyfiles v0.0.0-20250903184740-5d135037bd4d // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/gabriel-vasile/mimetype v1.4.8 // indirect github.com/gin-contrib/sse v1.1.0 // indirect + github.com/go-jose/go-jose/v4 v4.1.2 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect @@ -168,7 +185,7 @@ require ( github.com/google/flatbuffers v25.2.10+incompatible // indirect github.com/google/go-tpm v0.9.6 // indirect github.com/google/s2a-go v0.1.9 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect github.com/googleapis/gax-go/v2 v2.15.0 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/grafana/clusterurl v0.2.1 // indirect @@ -248,6 +265,7 @@ require ( github.com/pierrec/lz4/v4 v4.1.22 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/prometheus/otlptranslator v1.0.0 // indirect @@ -260,6 +278,7 @@ require ( github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect + github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect github.com/splunk/stef/go/grpc v0.0.8 // indirect github.com/splunk/stef/go/otel v0.0.8 // indirect github.com/splunk/stef/go/pdata v0.0.8 // indirect @@ -278,6 +297,7 @@ require ( github.com/valyala/fastjson v1.6.4 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect + github.com/zeebo/errs v1.4.0 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/collector v0.139.0 // indirect @@ -326,6 +346,7 @@ require ( go.opentelemetry.io/collector/service v0.139.0 // indirect go.opentelemetry.io/collector/service/hostcapabilities v0.139.0 // indirect go.opentelemetry.io/contrib/bridges/otelzap v0.13.0 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.36.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect go.opentelemetry.io/contrib/otelconf v0.18.0 // indirect @@ -352,15 +373,15 @@ require ( go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/arch v0.20.0 // indirect golang.org/x/exp v0.0.0-20251009144603-d2f985daa21b // indirect - golang.org/x/oauth2 v0.32.0 // indirect + golang.org/x/oauth2 v0.33.0 // indirect golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8 // indirect golang.org/x/time v0.14.0 // indirect golang.org/x/tools v0.38.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect gonum.org/v1/gonum v0.16.0 // indirect - google.golang.org/api v0.250.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4 // indirect + google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 0ecee6728..7a446475e 100644 --- a/go.sum +++ b/go.sum @@ -5,12 +5,26 @@ buf.build/go/protovalidate v1.0.0/go.mod h1:KQmEUrcQuC99hAw+juzOEAmILScQiKBP1Oc3 cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go/auth v0.16.5 h1:mFWNQ2FEVWAliEQWpAdH80omXFokmrnbDhUS9cBywsI= -cloud.google.com/go/auth v0.16.5/go.mod h1:utzRfHMP+Vv0mpOkTRQoWD2q3BatTOoWbA7gCc2dUhQ= +cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= +cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= +cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4= +cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= +cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc= +cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU= +cloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc= +cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA= +cloud.google.com/go/longrunning v0.7.0 h1:FV0+SYF1RIj59gyoWDRi45GiYUMM3K1qO51qoboQT1E= +cloud.google.com/go/longrunning v0.7.0/go.mod h1:ySn2yXmjbK9Ba0zsQqunhDkYi0+9rlXIwnoAf+h+TPY= +cloud.google.com/go/monitoring v1.24.2 h1:5OTsoJ1dXYIiMiuL+sYscLc9BumrL3CarVLL7dd7lHM= +cloud.google.com/go/monitoring v1.24.2/go.mod h1:x7yzPWcgDRnPEv3sI+jJGBkwl5qINf+6qY4eq0I9B4U= +cloud.google.com/go/storage v1.58.0 h1:PflFXlmFJjG/nBeR9B7pKddLQWaFaRWx4uUi/LyNxxo= +cloud.google.com/go/storage v1.58.0/go.mod h1:cMWbtM+anpC74gn6qjLh+exqYcfmB9Hqe5z6adx+CLI= +cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4= +cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= @@ -18,8 +32,8 @@ github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8af github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 h1:5YTBM8QDVIBN3sxBil89WfdAAqDZbyJTgh688DSxX5w= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.12.0 h1:wL5IEG5zb7BVv1Kv0Xm92orq+5hB5Nipn3B5tn4Rqfk= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.12.0/go.mod h1:J7MUC/wtRpfGVbQ5sIItY5/FuVWmvzlY21WAOfQnq/I= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.0 h1:KpMC6LFL7mqpExyMC9jVOYRiVhLmamjeZfRsUpB7l4s= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.0/go.mod h1:J7MUC/wtRpfGVbQ5sIItY5/FuVWmvzlY21WAOfQnq/I= github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY= github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8= github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA= @@ -44,6 +58,14 @@ github.com/DataDog/datadog-go/v5 v5.8.1 h1:+GOES5W9zpKlhwHptZVW2C0NLVf7ilr7pHkDc github.com/DataDog/datadog-go/v5 v5.8.1/go.mod h1:K9kcYBlxkcPP8tvvjZZKs/m1edNAUFzBbdpTUKfCsuw= github.com/DataDog/go-sqllexer v0.1.9 h1:0R8FnSHGXRtZo70UxgCneL7Yu4PurswUAMb5N3kOIrI= github.com/DataDog/go-sqllexer v0.1.9/go.mod h1:vOw7Ia7z+z6nl3zGZlLIZe0vQlPtCPR906WIPBJadxc= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 h1:UQUsRi8WTzhZntp5313l+CHIAT95ojUI2lpP/ExlZa4= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 h1:lhhYARPUu3LmHysQ/igznQphfzynnqI3D75oUyw1HXk= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0/go.mod h1:l9rva3ApbBpEJxSNYnwT9N4CDLrWgtq3u8736C5hyJw= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0 h1:xfK3bbi6F2RDtaZFtUdKO3osOBIhNb+xTs8lFW6yx9o= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 h1:s0WlVbf9qpvkh1c/uDAPElam0WrL7fHRIidgZJ7UqZI= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc= github.com/HdrHistogram/hdrhistogram-go v1.1.2 h1:5IcZpTvzydCQeHzK4Ef/D5rrSqwxob0t8PQPMybUNFM= github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= @@ -74,36 +96,46 @@ github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJ github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= -github.com/aws/aws-sdk-go-v2 v1.39.4 h1:qTsQKcdQPHnfGYBBs+Btl8QwxJeoWcOcPcixK90mRhg= -github.com/aws/aws-sdk-go-v2 v1.39.4/go.mod h1:yWSxrnioGUZ4WVv9TgMrNUeLV3PFESn/v+6T/Su8gnM= +github.com/aws/aws-sdk-go-v2 v1.41.0 h1:tNvqh1s+v0vFYdA1xq0aOJH+Y5cRyZ5upu6roPgPKd4= +github.com/aws/aws-sdk-go-v2 v1.41.0/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4= github.com/aws/aws-sdk-go-v2/config v1.31.15 h1:gE3M4xuNXfC/9bG4hyowGm/35uQTi7bUKeYs5e/6uvU= github.com/aws/aws-sdk-go-v2/config v1.31.15/go.mod h1:HvnvGJoE2I95KAIW8kkWVPJ4XhdrlvwJpV6pEzFQa8o= github.com/aws/aws-sdk-go-v2/credentials v1.18.19 h1:Jc1zzwkSY1QbkEcLujwqRTXOdvW8ppND3jRBb/VhBQc= github.com/aws/aws-sdk-go-v2/credentials v1.18.19/go.mod h1:DIfQ9fAk5H0pGtnqfqkbSIzky82qYnGvh06ASQXXg6A= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.11 h1:X7X4YKb+c0rkI6d4uJ5tEMxXgCZ+jZ/D6mvkno8c8Uw= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.11/go.mod h1:EqM6vPZQsZHYvC4Cai35UDg/f5NCEU+vp0WfbVqVcZc= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.11 h1:7AANQZkF3ihM8fbdftpjhken0TP9sBzFbV/Ze/Y4HXA= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.11/go.mod h1:NTF4QCGkm6fzVwncpkFQqoquQyOolcyXfbpC98urj+c= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.11 h1:ShdtWUZT37LCAA4Mw2kJAJtzaszfSHFb5n25sdcv4YE= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.11/go.mod h1:7bUb2sSr2MZ3M/N+VyETLTQtInemHXb/Fl3s8CLzm0Y= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 h1:rgGwPzb82iBYSvHMHXc8h9mRoOUBZIGFgKb9qniaZZc= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16/go.mod h1:L/UxsGeKpGoIj6DxfhOWHWQ/kGKcd4I1VncE4++IyKA= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 h1:1jtGzuV7c82xnqOVfx2F0xmJcOw5374L7N6juGW6x6U= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16/go.mod h1:M2E5OQf+XLe+SZGmmpaI2yy+J326aFf6/+54PoxSANc= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16 h1:CjMzUs78RDDv4ROu3JnJn/Ig1r6ZD7/T2DXLLRpejic= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16/go.mod h1:uVW4OLBqbJXSHJYA9svT9BluSvvwbzLQ2Crf6UPzR3c= github.com/aws/aws-sdk-go-v2/service/ec2 v1.259.0 h1:0BwB+z9JX7fleVvaZaUuzIHvGWiWn2BQLJIW2riEzDQ= github.com/aws/aws-sdk-go-v2/service/ec2 v1.259.0/go.mod h1:DT0XByGaNaOff3CtLVmj3jKcMeVDfOj5DkLD39UPJY0= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.2 h1:xtuxji5CS0JknaXoACOunXOYOQzgfTvGAc9s2QdCJA4= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.2/go.mod h1:zxwi0DIR0rcRcgdbl7E2MSOvxDyyXGBlScvBkARFaLQ= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.11 h1:GpMf3z2KJa4RnJ0ew3Hac+hRFYLZ9DDjfgXjuW+pB54= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.11/go.mod h1:6MZP3ZI4QQsgUCFTwMZA2V0sEriNQ8k2hmoHF3qjimQ= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7 h1:DIBqIrJ7hv+e4CmIk2z3pyKT+3B6qVMgRsawHiR3qso= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7/go.mod h1:vLm00xmBke75UmpNvOcZQ/Q30ZFjbczeLFqGx5urmGo= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 h1:oHjJHeUy0ImIV0bsrX0X91GkV5nJAyv1l1CC9lnO0TI= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16/go.mod h1:iRSNGgOYmiYwSCXxXaKb9HfOEj40+oTKn8pTxMlYkRM= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 h1:NSbvS17MlI2lurYgXnCOLvCFX38sBW4eiVER7+kkgsU= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16/go.mod h1:SwT8Tmqd4sA6G1qaGdzWCJN99bUmPGHfRwwq3G5Qb+A= github.com/aws/aws-sdk-go-v2/service/lightsail v1.49.1 h1:J1A0VJlt5HgUX6s11Obe9zrBDECeE2uhQc7Dwhdei9o= github.com/aws/aws-sdk-go-v2/service/lightsail v1.49.1/go.mod h1:WEOSRNyfIfvgrD9MuSIGrogKyuFahaVMziVq1pHI0NQ= +github.com/aws/aws-sdk-go-v2/service/s3 v1.94.0 h1:SWTxh/EcUCDVqi/0s26V6pVUq0BBG7kx0tDTmF/hCgA= +github.com/aws/aws-sdk-go-v2/service/s3 v1.94.0/go.mod h1:79S2BdqCJpScXZA2y+cpZuocWsjGjJINyXnOsf5DTz8= github.com/aws/aws-sdk-go-v2/service/sso v1.29.8 h1:M5nimZmugcZUO9wG7iVtROxPhiqyZX6ejS1lxlDPbTU= github.com/aws/aws-sdk-go-v2/service/sso v1.29.8/go.mod h1:mbef/pgKhtKRwrigPPs7SSSKZgytzP8PQ6P6JAAdqyM= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.3 h1:S5GuJZpYxE0lKeMHKn+BRTz6PTFpgThyJ+5mYfux7BM= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.3/go.mod h1:X4OF+BTd7HIb3L+tc4UlWHVrpgwZZIVENU15pRDVTI0= github.com/aws/aws-sdk-go-v2/service/sts v1.38.9 h1:Ekml5vGg6sHSZLZJQJagefnVe6PmqC2oiRkBq4F7fU0= github.com/aws/aws-sdk-go-v2/service/sts v1.38.9/go.mod h1:/e15V+o1zFHWdH3u7lpI3rVBcxszktIKuHKCY2/py+k= -github.com/aws/smithy-go v1.23.1 h1:sLvcH6dfAFwGkHLZ7dGiYF7aK6mg4CgKA/iDKjLDt9M= -github.com/aws/smithy-go v1.23.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= +github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk= +github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= github.com/axiomhq/hyperloglog v0.2.5 h1:Hefy3i8nAs8zAI/tDp+wE7N+Ltr8JnwiW3875pvl0N8= github.com/axiomhq/hyperloglog v0.2.5/go.mod h1:DLUK9yIzpU5B6YFLjxTIcbHu1g4Y1WQb1m5RH3radaM= github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3 h1:6df1vn4bBlDDo4tARvBm7l6KA9iVMnE3NWizDeWSrps= @@ -187,8 +219,11 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= +github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA= github.com/envoyproxy/go-control-plane/envoy v1.35.0 h1:ixjkELDE+ru6idPxcHLj8LBVc2bFP7iBytj353BoHUo= github.com/envoyproxy/go-control-plane/envoy v1.35.0/go.mod h1:09qwbGVuSWWAyN5t/b3iyVfz5+z8QWGrzkoqm/8SbEs= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= @@ -219,6 +254,8 @@ github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fq github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI= +github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= @@ -309,12 +346,14 @@ github.com/google/go-tpm v0.9.6/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u github.com/google/go-tpm-tools v0.4.4 h1:oiQfAIkc6xTy9Fl5NKTeTJkBTlXdHsxAofmQyxBKY98= github.com/google/go-tpm-tools v0.4.4/go.mod h1:T8jXkp2s+eltnCDIsXR84/MTcVU9Ja7bh3Mit0pa4AY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= +github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= -github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= +github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ= +github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= github.com/gophercloud/gophercloud/v2 v2.8.0 h1:of2+8tT6+FbEYHfYC8GBu8TXJNsXYSNm9KuvpX7Neqo= @@ -680,6 +719,8 @@ github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= +github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE= +github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= github.com/splunk/stef/go/grpc v0.0.8 h1:suVHFhpa4b28b80wxPSfCSXg1JjCDrJ4W6/8EQlPYrk= github.com/splunk/stef/go/grpc v0.0.8/go.mod h1:PxAdLa77jC/wSYr5T91Aqo1l9cg8oaPp6jc6UY3kxHE= github.com/splunk/stef/go/otel v0.0.8 h1:FbvVBMKLsoRvVAT2tB6lXqRQdrJAibeUAp9GSQTaU/8= @@ -747,6 +788,8 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/zeebo/assert v1.3.1 h1:vukIABvugfNMZMQO1ABsyQDJDTVQbn+LWSMy1ol1h6A= github.com/zeebo/assert v1.3.1/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= +github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= +github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= go.mongodb.org/mongo-driver v1.17.4 h1:jUorfmVzljjr0FLzYQsGP8cgN/qzzxlY9Vh0C9KFXVw= @@ -915,6 +958,8 @@ go.opentelemetry.io/collector/service/telemetry/telemetrytest v0.139.0 h1:b+b0U1 go.opentelemetry.io/collector/service/telemetry/telemetrytest v0.139.0/go.mod h1:fadcF+Cx45GEg+lNWGfpJNTVu4pAxIdq9+DbNrAs7T8= go.opentelemetry.io/contrib/bridges/otelzap v0.13.0 h1:aBKdhLVieqvwWe9A79UHI/0vgp2t/s2euY8X59pGRlw= go.opentelemetry.io/contrib/bridges/otelzap v0.13.0/go.mod h1:SYqtxLQE7iINgh6WFuVi2AI70148B8EI35DSk0Wr8m4= +go.opentelemetry.io/contrib/detectors/gcp v1.36.0 h1:F7q2tNlCaHY9nMKHR6XH9/qkp8FktLnIcy6jJNyOCQw= +go.opentelemetry.io/contrib/detectors/gcp v1.36.0/go.mod h1:IbBN8uAIIx734PTonTPxAxnjc2pQTxWNkwfstZ+6H2k= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0 h1:2pn7OzMewmYRiNtv1doZnLo3gONcnMHlFnmOR8Vgt+8= @@ -1056,8 +1101,8 @@ golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= -golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= +golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1070,8 +1115,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= -golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1160,17 +1205,19 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= -google.golang.org/api v0.250.0 h1:qvkwrf/raASj82UegU2RSDGWi/89WkLckn4LuO4lVXM= -google.golang.org/api v0.250.0/go.mod h1:Y9Uup8bDLJJtMzJyQnu+rLRJLA0wn+wTtc6vTlOvfXo= +google.golang.org/api v0.256.0 h1:u6Khm8+F9sxbCTYNoBHg6/Hwv0N/i+V94MvkOSor6oI= +google.golang.org/api v0.256.0/go.mod h1:KIgPhksXADEKJlnEoRa9qAII4rXcy40vfI8HRqcU964= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4 h1:8XJ4pajGwOlasW+L13MnEGA8W4115jJySQtVfS2/IBU= -google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4/go.mod h1:NnuHhy+bxcg30o7FnVAZbXsPHUDQ9qKWAQKCD7VxFtk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4 h1:i8QOKZfYg6AbGVZzUAY3LrNWCKF8O6zFisU9Wl9RER4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= +google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9 h1:LvZVVaPE0JSqL+ZWb6ErZfnEOKIqqFWUJE2D0fObSmc= +google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9/go.mod h1:QFOrLhdAe2PsTp3vQY4quuLKTi9j3XG3r6JPPaw7MSc= +google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba h1:B14OtaXuMaCQsl2deSvNkyPKIzq3BjfxQp8d00QyWx4= +google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:G5IanEx8/PgI9w6CFcYQf7jMtHQhZruvfM1i3qOqk5U= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= diff --git a/internal/config/config.go b/internal/config/config.go index 75c829e64..d765bdcf0 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -50,6 +50,10 @@ const ( regexLabelPattern = "^[a-zA-Z0-9]([a-zA-Z0-9-_]{0,254}[a-zA-Z0-9])?$" ) +var domainRegex = regexp.MustCompile( + `^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$`, +) + var viperInstance = viper.NewWithOptions(viper.KeyDelimiter(KeyDelimiter)) func RegisterRunner(r func(cmd *cobra.Command, args []string)) { @@ -157,6 +161,7 @@ func ResolveConfig() (*Config, error) { Features: viperInstance.GetStringSlice(FeaturesKey), Labels: resolveLabels(), LibDir: viperInstance.GetString(LibDirPathKey), + ExternalDataSource: resolveExternalDataSource(), SyslogServer: resolveSyslogServer(), } @@ -475,6 +480,7 @@ func registerFlags() { registerCollectorFlags(fs) registerClientFlags(fs) registerDataPlaneFlags(fs) + registerExternalDataSourceFlags(fs) fs.SetNormalizeFunc(normalizeFunc) @@ -489,6 +495,24 @@ func registerFlags() { }) } +func registerExternalDataSourceFlags(fs *flag.FlagSet) { + fs.String( + ExternalDataSourceProxyUrlKey, + DefExternalDataSourceProxyUrl, + "Url to the proxy service for fetching external files.", + ) + fs.StringSlice( + ExternalDataSourceAllowDomainsKey, + []string{}, + "List of allowed domains for external data sources.", + ) + fs.Int64( + ExternalDataSourceMaxBytesKey, + DefExternalDataSourceMaxBytes, + "Maximum size in bytes for external data sources.", + ) +} + func registerDataPlaneFlags(fs *flag.FlagSet) { fs.Duration( NginxReloadMonitoringPeriodKey, @@ -628,6 +652,11 @@ func registerClientFlags(fs *flag.FlagSet) { DefMaxFileSize, "Max file size in bytes.", ) + fs.Duration( + ClientFileDownloadTimeoutKey, + DefClientFileDownloadTimeout, + "Timeout value in seconds, for downloading a file during a config apply.", + ) fs.Int( ClientGRPCMaxParallelFileOperationsKey, @@ -1120,6 +1149,7 @@ func resolveClient() *Client { RandomizationFactor: viperInstance.GetFloat64(ClientBackoffRandomizationFactorKey), Multiplier: viperInstance.GetFloat64(ClientBackoffMultiplierKey), }, + FileDownloadTimeout: viperInstance.GetDuration(ClientFileDownloadTimeoutKey), } } @@ -1560,3 +1590,36 @@ func areCommandServerProxyTLSSettingsSet() bool { viperInstance.IsSet(CommandServerProxyTLSSkipVerifyKey) || viperInstance.IsSet(CommandServerProxyTLSServerNameKey) } + +func resolveExternalDataSource() *ExternalDataSource { + proxyURLStruct := ProxyURL{ + URL: viperInstance.GetString(ExternalDataSourceProxyUrlKey), + } + externalDataSource := &ExternalDataSource{ + ProxyURL: proxyURLStruct, + AllowedDomains: viperInstance.GetStringSlice(ExternalDataSourceAllowDomainsKey), + MaxBytes: viperInstance.GetInt64(ExternalDataSourceMaxBytesKey), + } + + if err := validateAllowedDomains(externalDataSource.AllowedDomains); err != nil { + return nil + } + + return externalDataSource +} + +func validateAllowedDomains(domains []string) error { + if len(domains) == 0 { + return nil + } + + for _, domain := range domains { + // Validating syntax using the RFC-compliant regex + if !domainRegex.MatchString(domain) || domain == "" { + slog.Error("domain specified in allowed_domains is invalid", "domain", domain) + return errors.New("invalid domain found in allowed_domains") + } + } + + return nil +} diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 5c4384ba8..8fd3dd1fe 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -1425,6 +1425,13 @@ func createConfig() *Config { config.FeatureCertificates, config.FeatureFileWatcher, config.FeatureMetrics, config.FeatureAPIAction, config.FeatureLogsNap, }, + ExternalDataSource: &ExternalDataSource{ + ProxyURL: ProxyURL{ + URL: "http://proxy.example.com", + }, + AllowedDomains: []string{"example.com", "api.example.com"}, + MaxBytes: 1048576, + }, } } @@ -1569,3 +1576,73 @@ func TestValidateLabel(t *testing.T) { }) } } + +func TestValidateAllowedDomains(t *testing.T) { + tests := []struct { + name string + domains []string + wantErr bool + }{ + { + name: "Test 1: Success: Empty slice", + domains: []string{}, + wantErr: false, + }, + { + name: "Test 2: Success: Nil slice", + domains: nil, + wantErr: false, + }, + { + name: "Test 3: Success: Valid domains", + domains: []string{"example.com", "api.nginx.com", "sub.domain.io"}, + wantErr: false, + }, + { + name: "Test 4: Failure: Domain contains space", + domains: []string{"valid.com", "bad domain.com"}, + wantErr: true, + }, + { + name: "Test 5: Failure: Empty string domain", + domains: []string{"valid.com", ""}, + wantErr: true, + }, + { + name: "Test 6: Failure: Domain contains forward slash /", + domains: []string{"domain.com/path"}, + wantErr: true, + }, + { + name: "Test 7: Failure: Domain contains backward slash \\", + domains: []string{"domain.com\\path"}, + wantErr: true, + }, + { + name: "Test 8: Failure: Mixed valid and invalid (first is invalid)", + domains: []string{" only.com", "good.com"}, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var logBuffer bytes.Buffer + logHandler := slog.NewTextHandler(&logBuffer, &slog.HandlerOptions{Level: slog.LevelError}) + + originalLogger := slog.Default() + slog.SetDefault(slog.New(logHandler)) + defer slog.SetDefault(originalLogger) + + actualErr := validateAllowedDomains(tt.domains) + + if tt.wantErr { + require.Error(t, actualErr, "Expected an error but got nil.") + assert.Contains(t, logBuffer.String(), "domain specified in allowed_domains is invalid", + "Expected the error log message to be present in the output.") + } else { + assert.NoError(t, actualErr, "Did not expect an error but got one: %v", actualErr) + } + }) + } +} diff --git a/internal/config/defaults.go b/internal/config/defaults.go index 0f1e08075..70b32fbd6 100644 --- a/internal/config/defaults.go +++ b/internal/config/defaults.go @@ -82,6 +82,8 @@ const ( DefBackoffMaxInterval = 20 * time.Second DefBackoffMaxElapsedTime = 1 * time.Minute + DefClientFileDownloadTimeout = 60 * time.Second + // Watcher defaults DefInstanceWatcherMonitoringFrequency = 5 * time.Second DefInstanceHealthWatcherMonitoringFrequency = 5 * time.Second @@ -114,6 +116,9 @@ const ( // File defaults DefLibDir = "/var/lib/nginx-agent" + + DefExternalDataSourceProxyUrl = "" + DefExternalDataSourceMaxBytes = 100 * 1024 * 1024 // default 100MB ) func DefaultFeatures() []string { diff --git a/internal/config/flags.go b/internal/config/flags.go index d0f664540..135cacb71 100644 --- a/internal/config/flags.go +++ b/internal/config/flags.go @@ -25,6 +25,7 @@ const ( InstanceHealthWatcherMonitoringFrequencyKey = "watchers_instance_health_watcher_monitoring_frequency" FileWatcherKey = "watchers_file_watcher" LibDirPathKey = "lib_dir" + ExternalDataSourceRootKey = "external_data_source" ) var ( @@ -47,6 +48,7 @@ var ( ClientBackoffMaxElapsedTimeKey = pre(ClientRootKey) + "backoff_max_elapsed_time" ClientBackoffRandomizationFactorKey = pre(ClientRootKey) + "backoff_randomization_factor" ClientBackoffMultiplierKey = pre(ClientRootKey) + "backoff_multiplier" + ClientFileDownloadTimeoutKey = pre(ClientRootKey) + "file_download_timeout" CollectorConfigPathKey = pre(CollectorRootKey) + "config_path" CollectorAdditionalConfigPathsKey = pre(CollectorRootKey) + "additional_config_paths" @@ -141,6 +143,11 @@ var ( FileWatcherMonitoringFrequencyKey = pre(FileWatcherKey) + "monitoring_frequency" NginxExcludeFilesKey = pre(FileWatcherKey) + "exclude_files" + + ExternalDataSourceProxyKey = pre(ExternalDataSourceRootKey) + "proxy" + ExternalDataSourceProxyUrlKey = pre(ExternalDataSourceProxyKey) + "url" + ExternalDataSourceMaxBytesKey = pre(ExternalDataSourceRootKey) + "max_bytes" + ExternalDataSourceAllowDomainsKey = pre(ExternalDataSourceRootKey) + "allowed_domains" ) func pre(prefixes ...string) string { diff --git a/internal/config/testdata/nginx-agent.conf b/internal/config/testdata/nginx-agent.conf index 2ac87b9ee..d7892c4c6 100644 --- a/internal/config/testdata/nginx-agent.conf +++ b/internal/config/testdata/nginx-agent.conf @@ -183,3 +183,11 @@ collector: log: level: "INFO" path: "/var/log/nginx-agent/opentelemetry-collector-agent.log" + +external_data_source: + proxy: + url: "http://proxy.example.com" + allowed_domains: + - example.com + - api.example.com + max_bytes: 1048576 diff --git a/internal/config/types.go b/internal/config/types.go index 72eda1369..e553e0d40 100644 --- a/internal/config/types.go +++ b/internal/config/types.go @@ -36,21 +36,22 @@ func parseServerType(str string) (ServerType, bool) { type ( Config struct { - Command *Command `yaml:"command" mapstructure:"command"` - AuxiliaryCommand *Command `yaml:"auxiliary_command" mapstructure:"auxiliary_command"` - Log *Log `yaml:"log" mapstructure:"log"` - DataPlaneConfig *DataPlaneConfig `yaml:"data_plane_config" mapstructure:"data_plane_config"` - Client *Client `yaml:"client" mapstructure:"client"` - Collector *Collector `yaml:"collector" mapstructure:"collector"` - Watchers *Watchers `yaml:"watchers" mapstructure:"watchers"` - SyslogServer *SyslogServer `yaml:"syslog_server" mapstructure:"syslog_server"` - Labels map[string]any `yaml:"labels" mapstructure:"labels"` - Version string `yaml:"-"` - Path string `yaml:"-"` - UUID string `yaml:"-"` - LibDir string `yaml:"-"` - AllowedDirectories []string `yaml:"allowed_directories" mapstructure:"allowed_directories"` - Features []string `yaml:"features" mapstructure:"features"` + Command *Command `yaml:"command" mapstructure:"command"` + AuxiliaryCommand *Command `yaml:"auxiliary_command" mapstructure:"auxiliary_command"` + Log *Log `yaml:"log" mapstructure:"log"` + DataPlaneConfig *DataPlaneConfig `yaml:"data_plane_config" mapstructure:"data_plane_config"` + Client *Client `yaml:"client" mapstructure:"client"` + Collector *Collector `yaml:"collector" mapstructure:"collector"` + Watchers *Watchers `yaml:"watchers" mapstructure:"watchers"` + SyslogServer *SyslogServer `yaml:"syslog_server" mapstructure:"syslog_server"` + ExternalDataSource *ExternalDataSource `yaml:"external_data_source" mapstructure:"external_data_source"` + Labels map[string]any `yaml:"labels" mapstructure:"labels"` + Version string `yaml:"-"` + Path string `yaml:"-"` + UUID string `yaml:"-"` + LibDir string `yaml:"-"` + AllowedDirectories []string `yaml:"allowed_directories" mapstructure:"allowed_directories"` + Features []string `yaml:"features" mapstructure:"features"` } Log struct { @@ -74,9 +75,10 @@ type ( } Client struct { - HTTP *HTTP `yaml:"http" mapstructure:"http"` - Grpc *GRPC `yaml:"grpc" mapstructure:"grpc"` - Backoff *BackOff `yaml:"backoff" mapstructure:"backoff"` + HTTP *HTTP `yaml:"http" mapstructure:"http"` + Grpc *GRPC `yaml:"grpc" mapstructure:"grpc"` + Backoff *BackOff `yaml:"backoff" mapstructure:"backoff"` + FileDownloadTimeout time.Duration `yaml:"file_download_timeout" mapstructure:"file_download_timeout"` } HTTP struct { @@ -358,6 +360,16 @@ type ( Token string `yaml:"token,omitempty" mapstructure:"token"` Timeout time.Duration `yaml:"timeout" mapstructure:"timeout"` } + + ProxyURL struct { + URL string `yaml:"url" mapstructure:"url"` + } + + ExternalDataSource struct { + ProxyURL ProxyURL `yaml:"proxy" mapstructure:"proxy"` + AllowedDomains []string `yaml:"allowed_domains" mapstructure:"allowed_domains"` + MaxBytes int64 `yaml:"max_bytes" mapstructure:"max_bytes"` + } ) func (col *Collector) Validate(allowedDirectories []string) error { diff --git a/internal/file/file_manager_service.go b/internal/file/file_manager_service.go index af0e67a91..d6fd47ecd 100644 --- a/internal/file/file_manager_service.go +++ b/internal/file/file_manager_service.go @@ -11,15 +11,24 @@ import ( "encoding/json" "errors" "fmt" + "io" "log/slog" + "net/http" + "net/url" "os" "path/filepath" "strconv" + "strings" "sync" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" "golang.org/x/sync/errgroup" + "google.golang.org/api/option" "google.golang.org/grpc" + gcpstorage "cloud.google.com/go/storage" + awsconfig "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/nginx/agent/v3/internal/model" mpi "github.com/nginx/agent/v3/api/grpc/mpi/v1" @@ -33,6 +42,9 @@ import ( //go:generate go run github.com/maxbrunsfeld/counterfeiter/v6@v6.8.1 -generate //counterfeiter:generate . fileManagerServiceInterface +//go:generate go run github.com/maxbrunsfeld/counterfeiter/v6@v6.8.1 -generate +//counterfeiter:generate . fileServiceOperatorInterface + const ( maxAttempts = 5 dirPerm = 0o755 @@ -40,6 +52,78 @@ const ( executePerm = 0o111 ) +type HTTPDownloader struct { + fms *FileManagerService +} +type AWSDownloader struct { + fms *FileManagerService + s3Client *s3.Client +} + +//nolint:ireturn,revive,staticcheck // Just a POC +func NewAWSDownloader(fms *FileManagerService) *AWSDownloader { + // 1. Load AWS Configuration (this handles finding credentials automatically) + cfg, err := awsconfig.LoadDefaultConfig(context.Background()) + if err != nil { + // Handle fatal configuration error (e.g., no credentials found) + // You would typically log this or panic if necessary. + } + + return &AWSDownloader{ + fms: fms, + s3Client: s3.NewFromConfig(cfg), + } +} + +type GCPDownloader struct { + fms *FileManagerService + storageClient *gcpstorage.Client +} + +//nolint:ireturn,revive,staticcheck // Just a POC +func NewGCPDownloader(fms *FileManagerService) *GCPDownloader { + // 1. Initialize the client, using the 'gcp_storage' alias. + client, err := gcpstorage.NewClient(context.Background(), option.WithScopes(gcpstorage.ScopeReadOnly)) + if err != nil { + // Handle error + } + + return &GCPDownloader{ + fms: fms, + storageClient: client, + } +} + +type AzureDownloader struct { + fms *FileManagerService + // containerClient *container.Client +} + +//nolint:ireturn,revive,staticcheck // Just a POC +func NewAzureDownloader(fms *FileManagerService) *AzureDownloader { + // 1. Use the Default Azure Credential chain + _, err := azidentity.NewDefaultAzureCredential(nil) + if err != nil { + // Handle error + } + + // Need to construct the URL for the Blob Storage service, + // and create the client using the credential object. (URL is required here) + // storageURL := "https://youraccount.blob.core.windows.net" + + // client, err := container.NewClient(storageURL, cred, nil) + + return &AzureDownloader{ + fms: fms, + } +} + +// Downloader defines the contract for downloading content from various sources. +type Downloader interface { + Download(ctx context.Context, location string, fileMeta *mpi.FileMeta) (content []byte, + headers model.DownloadHeader, err error) +} + type ( fileOperator interface { Write(ctx context.Context, fileContent []byte, fileName, filePermissions string) error @@ -73,7 +157,8 @@ type ( fileToUpdate *mpi.File, ) error SetIsConnected(isConnected bool) - RenameFile(ctx context.Context, hash, fileName, tempDir string) error + RenameFile(ctx context.Context, fileName, tempDir string) error + ValidateFileHash(ctx context.Context, fileName, expectedHash string) error UpdateClient(ctx context.Context, fileServiceClient mpi.FileServiceClient) } @@ -93,6 +178,8 @@ type ( IsConnected() bool SetIsConnected(isConnected bool) ResetClient(ctx context.Context, fileServiceClient mpi.FileServiceClient) + Download(ctx context.Context, location string, fileMeta *mpi.FileMeta) (content []byte, + headers model.DownloadHeader, err error) } ) @@ -106,6 +193,7 @@ type FileManagerService struct { // map of the files currently on disk, used to determine the file action during config apply currentFilesOnDisk map[string]*mpi.File // key is file path previousManifestFiles map[string]*model.ManifestFile + externalFileHeaders map[string]model.DownloadHeader manifestFilePath string rollbackManifest bool filesMutex sync.RWMutex @@ -121,6 +209,7 @@ func NewFileManagerService(fileServiceClient mpi.FileServiceClient, agentConfig fileActions: make(map[string]*model.FileCache), currentFilesOnDisk: make(map[string]*mpi.File), previousManifestFiles: make(map[string]*model.ManifestFile), + externalFileHeaders: make(map[string]model.DownloadHeader), rollbackManifest: true, manifestFilePath: agentConfig.LibDir + "/manifest.json", manifestLock: manifestLock, @@ -233,7 +322,7 @@ func (fms *FileManagerService) Rollback(ctx context.Context, instanceID string) delete(fms.currentFilesOnDisk, fileAction.File.GetFileMeta().GetName()) continue - case model.Delete, model.Update: + case model.Delete, model.Update, model.ExternalFile: content, err := fms.restoreFiles(fileAction) if err != nil { return err @@ -390,6 +479,17 @@ func (fms *FileManagerService) DetermineFileActions( slog.DebugContext(ctx, "Skipping unmanaged file updates", "file_name", fileName) continue } + + // If either the modified file or the current file is an external data source, + // treat this as an ExternalFile and skip the regular Add/Update checks. This + // ensures external files are downloaded/validated every single time. + if modifiedFile.File.GetExternalDataSource() != nil || (ok && currentFile.GetExternalDataSource() != nil) { + modifiedFile.Action = model.ExternalFile + fileDiff[fileName] = modifiedFile + + continue + } + // if file doesn't exist in the current files, file has been added // set file action if _, statErr := os.Stat(fileName); errors.Is(statErr, os.ErrNotExist) { @@ -481,6 +581,240 @@ func (fms *FileManagerService) UpdateManifestFile(ctx context.Context, return fms.fileOperator.WriteManifestFile(ctx, updatedFiles, fms.agentConfig.LibDir, fms.manifestFilePath) } +// ConvertToMapOfFiles converts a list of files to a map of file caches (file and action) with the file name as the key +func ConvertToMapOfFileCache(convertFiles []*mpi.File) map[string]*model.FileCache { + filesMap := make(map[string]*model.FileCache) + for _, convertFile := range convertFiles { + filesMap[convertFile.GetFileMeta().GetName()] = &model.FileCache{ + File: convertFile, + } + } + + return filesMap +} + +func tempFilePath(fileName string) string { + tempFileName := "." + filepath.Base(fileName) + ".agent.tmp" + return filepath.Join(filepath.Dir(fileName), tempFileName) +} + +func tempBackupFilePath(fileName string) string { + tempFileName := "." + filepath.Base(fileName) + ".agent.backup" + return filepath.Join(filepath.Dir(fileName), tempFileName) +} + +func NewHTTPDownloader(fms *FileManagerService) *HTTPDownloader { + return &HTTPDownloader{fms: fms} +} + +// Download implements the Downloader interface for standard HTTP. +// This preserves the original file downloading logic. +func (h *HTTPDownloader) Download( + ctx context.Context, + downloadURL string, + fileMeta *mpi.FileMeta, +) (content []byte, headers model.DownloadHeader, err error) { + fileName := fileMeta.GetName() + externalConfig := h.fms.agentConfig.ExternalDataSource + + if !isDomainAllowed(downloadURL, externalConfig.AllowedDomains) { + return nil, model.DownloadHeader{}, fmt.Errorf("download URL %s is not in the allowed domains list", + downloadURL) + } + + httpClient, err := h.fms.setupHTTPClient(ctx, externalConfig.ProxyURL.URL) + if err != nil { + return nil, model.DownloadHeader{}, err + } + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, downloadURL, nil) + if err != nil { + return nil, model.DownloadHeader{}, fmt.Errorf("failed to create request for %s: %w", downloadURL, err) + } + + if externalConfig.ProxyURL.URL != "" { + h.fms.addConditionalHeaders(ctx, req, fileName) + } else { + slog.DebugContext(ctx, "No proxy configured; sending plain HTTP request without caching headers.") + } + + resp, err := httpClient.Do(req) + if err != nil { + return nil, model.DownloadHeader{}, fmt.Errorf("failed to execute download request for %s: %w", + downloadURL, err) + } + defer resp.Body.Close() + + switch resp.StatusCode { + case http.StatusOK: + headers.ETag = resp.Header.Get("ETag") + headers.LastModified = resp.Header.Get("Last-Modified") + case http.StatusNotModified: + slog.DebugContext(ctx, "File content unchanged (304 Not Modified)", "file_name", fileName) + return nil, model.DownloadHeader{}, nil + default: + return nil, model.DownloadHeader{}, fmt.Errorf("download failed with status code %d", resp.StatusCode) + } + + reader := io.Reader(resp.Body) + if h.fms.agentConfig.ExternalDataSource.MaxBytes > 0 { + reader = io.LimitReader(resp.Body, h.fms.agentConfig.ExternalDataSource.MaxBytes) + } + + content, err = io.ReadAll(reader) + if err != nil { + return nil, model.DownloadHeader{}, fmt.Errorf("failed to read content from response body: %w", err) + } + + slog.InfoContext(ctx, "Successfully downloaded file content", "file_name", fileName, "size", len(content)) + + return content, headers, nil +} + +func (a *AWSDownloader) Download( + ctx context.Context, + location string, + fileMeta *mpi.FileMeta, +) (content []byte, headers model.DownloadHeader, err error) { + // --- AWS IMPLEMENTATION GOES HERE --- + return nil, model.DownloadHeader{}, fmt.Errorf("AWS download from %s is not yet fully implemented", location) +} + +func (g *GCPDownloader) Download( + ctx context.Context, + location string, + fileMeta *mpi.FileMeta, +) (content []byte, headers model.DownloadHeader, err error) { + // --- GCP IMPLEMENTATION GOES HERE --- + return nil, model.DownloadHeader{}, fmt.Errorf("GCP download from %s is not yet fully implemented", location) +} + +func (az *AzureDownloader) Download( + ctx context.Context, + location string, + fileMeta *mpi.FileMeta, +) (content []byte, headers model.DownloadHeader, err error) { + // --- AZURE IMPLEMENTATION GOES HERE --- + return nil, model.DownloadHeader{}, fmt.Errorf("azure download from %s is not yet fully implemented", location) +} + +//nolint:ireturn // Just a POC for cloud downloaders +func (fms *FileManagerService) GetDownloader(location string) Downloader { + u, err := url.Parse(location) + if err != nil { + // Fallback to HTTP if parsing fails + return NewHTTPDownloader(fms) + } + + // Check for specific cloud schemes (s3://, gs://) + switch u.Scheme { + case "s3": + return NewAWSDownloader(fms) // <--- AWS S3 Implementation Trigger + case "gs": + return NewGCPDownloader(fms) // <--- GCP Cloud Storage Implementation Trigger + } + + // Check for specific cloud domains (Secrets Manager, Key Vault, etc.) + hostname := u.Hostname() + + // AWS Secret/Service URLs + if strings.Contains(hostname, "amazonaws.com") { + return NewAWSDownloader(fms) // <--- AWS Secrets Manager/Service Implementation Trigger + } + + // Azure Key Vault/Service URLs + if strings.Contains(hostname, ".vault.azure.net") || strings.Contains(hostname, ".blob.core.windows.net") { + return NewAzureDownloader(fms) // <--- Azure Key Vault/Blob Implementation Trigger + } + + // GCP Secrets Manager/Service URLs + if strings.Contains(hostname, "googleapis.com") { + return NewGCPDownloader(fms) // <--- GCP Secret Manager/Service Implementation Trigger + } + + // Default to standard HTTP/HTTPS download if no cloud provider matches + return NewHTTPDownloader(fms) +} + +func (fms *FileManagerService) Download( + ctx context.Context, + location string, + fileMeta *mpi.FileMeta, +) (content []byte, headers model.DownloadHeader, err error) { + // Created a temporary mpi.File structure to satisfy the Downloader interface + tempFile := &mpi.File{ + FileMeta: fileMeta, + ExternalDataSource: &mpi.ExternalDataSource{ + Location: location, + }, + } + + return fms.downloadFileContent(ctx, tempFile) +} + +// downloadFileContent performs an HTTP GET request to the given URL and returns the file content as a byte slice. +func (fms *FileManagerService) downloadFileContent( + ctx context.Context, + file *mpi.File, +) (content []byte, headers model.DownloadHeader, err error) { + fileName := file.GetFileMeta().GetName() + downloadURL := file.GetExternalDataSource().GetLocation() + externalConfig := fms.agentConfig.ExternalDataSource + + if !isDomainAllowed(downloadURL, externalConfig.AllowedDomains) { + return nil, model.DownloadHeader{}, fmt.Errorf("download URL %s is not in the allowed domains list", + downloadURL) + } + + httpClient, err := fms.setupHTTPClient(ctx, externalConfig.ProxyURL.URL) + if err != nil { + return nil, model.DownloadHeader{}, err + } + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, downloadURL, nil) + if err != nil { + return nil, model.DownloadHeader{}, fmt.Errorf("failed to create request for %s: %w", downloadURL, err) + } + + if externalConfig.ProxyURL.URL != "" { + fms.addConditionalHeaders(ctx, req, fileName) + } else { + slog.DebugContext(ctx, "No proxy configured; sending plain HTTP request without caching headers.") + } + + resp, err := httpClient.Do(req) + if err != nil { + return nil, model.DownloadHeader{}, fmt.Errorf("failed to execute download request for %s: %w", + downloadURL, err) + } + defer resp.Body.Close() + + switch resp.StatusCode { + case http.StatusOK: + headers.ETag = resp.Header.Get("ETag") + headers.LastModified = resp.Header.Get("Last-Modified") + case http.StatusNotModified: + slog.DebugContext(ctx, "File content unchanged (304 Not Modified)", "file_name", fileName) + return nil, model.DownloadHeader{}, nil + default: + return nil, model.DownloadHeader{}, fmt.Errorf("download failed with status code %d", resp.StatusCode) + } + + reader := io.Reader(resp.Body) + if fms.agentConfig.ExternalDataSource.MaxBytes > 0 { + reader = io.LimitReader(resp.Body, fms.agentConfig.ExternalDataSource.MaxBytes) + } + + content, err = io.ReadAll(reader) + if err != nil { + return nil, model.DownloadHeader{}, fmt.Errorf("failed to read content from response body: %w", err) + } + + slog.InfoContext(ctx, "Successfully downloaded file content", "file_name", fileName, "size", len(content)) + + return content, headers, nil +} + func (fms *FileManagerService) backupFiles(ctx context.Context) error { for _, file := range fms.fileActions { if file.Action == model.Add || file.Action == model.Unchanged { @@ -581,7 +915,8 @@ func (fms *FileManagerService) executeFileActions(ctx context.Context) (actionEr func (fms *FileManagerService) downloadUpdatedFilesToTempLocation(ctx context.Context) (updateError error) { var downloadFiles []*model.FileCache for _, fileAction := range fms.fileActions { - if fileAction.Action == model.Add || fileAction.Action == model.Update { + if fileAction.Action == model.ExternalFile || fileAction.Action == model.Add || + fileAction.Action == model.Update { downloadFiles = append(downloadFiles, fileAction) } } @@ -590,7 +925,6 @@ func (fms *FileManagerService) downloadUpdatedFilesToTempLocation(ctx context.Co slog.DebugContext(ctx, "No updated files to download") return nil } - errGroup, errGroupCtx := errgroup.WithContext(ctx) errGroup.SetLimit(fms.agentConfig.Client.Grpc.MaxParallelFileOperations) @@ -598,45 +932,62 @@ func (fms *FileManagerService) downloadUpdatedFilesToTempLocation(ctx context.Co errGroup.Go(func() error { tempFilePath := tempFilePath(fileAction.File.GetFileMeta().GetName()) - slog.DebugContext( - errGroupCtx, - "Downloading file to temp location", - "file", tempFilePath, - ) + switch fileAction.Action { + case model.ExternalFile: + return fms.downloadExternalFile(errGroupCtx, fileAction, tempFilePath) + case model.Add, model.Update: + slog.DebugContext( + errGroupCtx, + "Downloading file to temp location", + "file", tempFilePath, + ) - return fms.fileUpdate(errGroupCtx, fileAction.File, tempFilePath) + return fms.fileUpdate(errGroupCtx, fileAction.File, tempFilePath) + case model.Delete, model.Unchanged: // had to add for linter + return nil + default: + return nil + } }) } return errGroup.Wait() } +//nolint:revive // cognitive-complexity of 14 max is 12, loop is needed cant be broken up func (fms *FileManagerService) moveOrDeleteFiles(ctx context.Context, actionError error) error { actionsLoop: for _, fileAction := range fms.fileActions { + var err error + fileMeta := fileAction.File.GetFileMeta() + tempFilePath := tempFilePath(fileMeta.GetName()) switch fileAction.Action { case model.Delete: - slog.DebugContext(ctx, "Deleting file", "file", fileAction.File.GetFileMeta().GetName()) - if err := os.Remove(fileAction.File.GetFileMeta().GetName()); err != nil && !os.IsNotExist(err) { + slog.DebugContext(ctx, "Deleting file", "file", fileMeta.GetName()) + if err = os.Remove(fileMeta.GetName()); err != nil && !os.IsNotExist(err) { actionError = fmt.Errorf("error deleting file: %s error: %w", - fileAction.File.GetFileMeta().GetName(), err) + fileMeta.GetName(), err) break actionsLoop } continue case model.Add, model.Update: - fileMeta := fileAction.File.GetFileMeta() - tempFilePath := tempFilePath(fileAction.File.GetFileMeta().GetName()) - err := fms.fileServiceOperator.RenameFile(ctx, fileMeta.GetHash(), tempFilePath, fileMeta.GetName()) + err = fms.fileServiceOperator.RenameFile(ctx, tempFilePath, fileMeta.GetName()) if err != nil { actionError = err - break actionsLoop } + err = fms.fileServiceOperator.ValidateFileHash(ctx, fileMeta.GetName(), fileMeta.GetHash()) + case model.ExternalFile: + err = fms.fileServiceOperator.RenameFile(ctx, tempFilePath, fileMeta.GetName()) case model.Unchanged: slog.DebugContext(ctx, "File unchanged") } + if err != nil { + actionError = err + break actionsLoop + } } return actionError @@ -771,24 +1122,146 @@ func (fms *FileManagerService) convertToFile(manifestFile *model.ManifestFile) * } } -// ConvertToMapOfFiles converts a list of files to a map of file caches (file and action) with the file name as the key -func ConvertToMapOfFileCache(convertFiles []*mpi.File) map[string]*model.FileCache { - filesMap := make(map[string]*model.FileCache) - for _, convertFile := range convertFiles { - filesMap[convertFile.GetFileMeta().GetName()] = &model.FileCache{ - File: convertFile, +//nolint:ireturn // Just a POC +func (fms *FileManagerService) downloadExternalFile(ctx context.Context, fileAction *model.FileCache, + filePath string, +) error { + location := fileAction.File.GetExternalDataSource().GetLocation() + permission := fileAction.File.GetFileMeta().GetPermissions() + + slog.InfoContext(ctx, "Downloading external file from", "location", location) + + var contentToWrite []byte + var downloadErr, updateError error + var headers model.DownloadHeader + + contentToWrite, headers, downloadErr = fms.downloadFileContent(ctx, fileAction.File) + + if downloadErr != nil { + updateError = fmt.Errorf("failed to download file %s from %s: %w", + fileAction.File.GetFileMeta().GetName(), location, downloadErr) + + return updateError + } + + if contentToWrite == nil { + slog.DebugContext(ctx, "External file unchanged (304), skipping disk write.", + "file", fileAction.File.GetFileMeta().GetName()) + + fileAction.Action = model.Unchanged + + return nil + } + + fileName := fileAction.File.GetFileMeta().GetName() + fms.externalFileHeaders[fileName] = headers + + writeErr := fms.fileOperator.Write( + ctx, + contentToWrite, + filePath, + permission, + ) + + if writeErr != nil { + return fmt.Errorf("failed to write downloaded content to temp file %s: %w", filePath, writeErr) + } + + return nil +} + +func isDomainAllowed(downloadURL string, allowedDomains []string) bool { + u, err := url.Parse(downloadURL) + if err != nil { + slog.Debug("Failed to parse download URL for domain check", "url", downloadURL, "error", err) + return false + } + + hostname := u.Hostname() + if hostname == "" { + return false + } + + for _, domain := range allowedDomains { + if domain == "" { + continue + } + + if domain == hostname || isMatchesWildcardDomain(hostname, domain) { + return true } } - return filesMap + return false } -func tempFilePath(fileName string) string { - tempFileName := "." + filepath.Base(fileName) + ".agent.tmp" - return filepath.Join(filepath.Dir(fileName), tempFileName) +func (fms *FileManagerService) setupHTTPClient(ctx context.Context, proxyURLString string) (*http.Client, error) { + var transport *http.Transport + + if proxyURLString != "" { + proxyURL, err := url.Parse(proxyURLString) + if err != nil { + return nil, fmt.Errorf("invalid proxy URL configured: %w", err) + } + slog.DebugContext(ctx, "Configuring HTTP client to use proxy", "proxy_url", proxyURLString) + transport = &http.Transport{ + Proxy: http.ProxyURL(proxyURL), + } + } else { + slog.DebugContext(ctx, "Configuring HTTP client for direct connection (no proxy)") + transport = &http.Transport{ + Proxy: nil, + } + } + + httpClient := &http.Client{ + Transport: transport, + Timeout: fms.agentConfig.Client.FileDownloadTimeout, + } + + return httpClient, nil } -func tempBackupFilePath(fileName string) string { - tempFileName := "." + filepath.Base(fileName) + ".agent.backup" - return filepath.Join(filepath.Dir(fileName), tempFileName) +func (fms *FileManagerService) addConditionalHeaders(ctx context.Context, req *http.Request, fileName string) { + slog.DebugContext(ctx, "Proxy configured; adding headers to GET request.") + + manifestFiles, _, manifestFileErr := fms.manifestFile() + + if manifestFileErr != nil && !errors.Is(manifestFileErr, os.ErrNotExist) { + slog.WarnContext(ctx, "Error reading manifest file for headers", "error", manifestFileErr) + } + + manifestFile, ok := manifestFiles[fileName] + + if ok && manifestFile != nil && manifestFile.ManifestFileMeta != nil { + fileMeta := manifestFile.ManifestFileMeta + + if fileMeta.ETag != "" { + req.Header.Set("If-None-Match", fileMeta.ETag) + } + if fileMeta.LastModified != "" { + req.Header.Set("If-Modified-Since", fileMeta.LastModified) + } + } else { + slog.DebugContext(ctx, "File not found in manifest or missing metadata; skipping conditional headers.", + "file", fileName) + } +} + +func isMatchesWildcardDomain(hostname, pattern string) bool { + if !strings.HasPrefix(pattern, "*.") { + return false + } + + baseDomain := pattern[2:] + if strings.HasSuffix(hostname, baseDomain) { + // Check to ensure it's a true subdomain match (e.g., must have a '.' + // before baseDomain unless it IS the baseDomain) + // This handles cases like preventing 'foo.com' matching '*.oo.com' + if hostname == baseDomain || hostname[len(hostname)-len(baseDomain)-1] == '.' { + return true + } + } + + return false } diff --git a/internal/file/file_manager_service_test.go b/internal/file/file_manager_service_test.go index e42128063..904b58086 100644 --- a/internal/file/file_manager_service_test.go +++ b/internal/file/file_manager_service_test.go @@ -10,11 +10,17 @@ import ( "encoding/json" "errors" "fmt" + "net/http" + "net/http/httptest" + "net/url" "os" "path/filepath" + "strings" "sync" "testing" + "time" + "github.com/nginx/agent/v3/internal/config" "github.com/nginx/agent/v3/internal/model" "github.com/nginx/agent/v3/pkg/files" @@ -1173,3 +1179,378 @@ rQHX6DP4w6IwZY8JB8LS }) } } + +func TestFileManagerService_DetermineFileActions_ExternalFile(t *testing.T) { + ctx := context.Background() + tempDir := t.TempDir() + fileName := filepath.Join(tempDir, "external.conf") + + modifiedFiles := map[string]*model.FileCache{ + fileName: { + File: &mpi.File{ + FileMeta: &mpi.FileMeta{ + Name: fileName, + }, + ExternalDataSource: &mpi.ExternalDataSource{Location: "http://example.com/file"}, + }, + }, + } + + fakeFileServiceClient := &v1fakes.FakeFileServiceClient{} + fileManagerService := NewFileManagerService(fakeFileServiceClient, types.AgentConfig(), &sync.RWMutex{}) + fileManagerService.agentConfig.AllowedDirectories = []string{tempDir} + + diff, err := fileManagerService.DetermineFileActions(ctx, make(map[string]*mpi.File), modifiedFiles) + require.NoError(t, err) + + fc, ok := diff[fileName] + require.True(t, ok, "expected file to be present in diff") + assert.Equal(t, model.ExternalFile, fc.Action) +} + +//nolint:gocognit,revive,govet // cognitive complexity is 22 +func TestFileManagerService_downloadExternalFiles(t *testing.T) { + type tc struct { + allowedDomains []string + expectContent []byte + name string + expectHeaderETag string + expectHeaderLastMod string + expectErrContains string + handler http.HandlerFunc + maxBytes int + expectError bool + expectTempFile bool + } + + tests := []tc{ + { + name: "Test 1: Success", + handler: func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("ETag", "test-etag") + w.Header().Set("Last-Modified", time.RFC1123) + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("external file content")) + }, + allowedDomains: nil, + maxBytes: 0, + expectError: false, + expectTempFile: true, + expectContent: []byte("external file content"), + expectHeaderETag: "test-etag", + expectHeaderLastMod: time.RFC1123, + }, + { + name: "Test 2: NotModified", + handler: func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotModified) + }, + allowedDomains: nil, + maxBytes: 0, + expectError: false, + expectTempFile: false, + expectContent: nil, + expectHeaderETag: "", + expectHeaderLastMod: "", + }, + { + name: "Test 3: NotAllowedDomain", + handler: func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("external file content")) + }, + allowedDomains: []string{"not-the-host"}, + maxBytes: 0, + expectError: true, + expectErrContains: "not in the allowed domains", + expectTempFile: false, + }, + { + name: "Test 4: NotFound", + handler: func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + }, + allowedDomains: nil, + maxBytes: 0, + expectError: true, + expectErrContains: "status code 404", + expectTempFile: false, + }, + { + name: "Test 5: ProxyWithConditionalHeaders", + handler: func(w http.ResponseWriter, r *http.Request) { + // verify conditional headers from manifest are added + if r.Header.Get("If-None-Match") != "manifest-test-etag" { + http.Error(w, "missing If-None-Match", http.StatusBadRequest) + return + } + if r.Header.Get("If-Modified-Since") != time.RFC1123 { + http.Error(w, "missing If-Modified-Since", http.StatusBadRequest) + return + } + w.Header().Set("ETag", "resp-etag") + w.Header().Set("Last-Modified", time.RFC1123) + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("external file via proxy")) + }, + allowedDomains: nil, + maxBytes: 0, + expectError: false, + expectTempFile: true, + expectContent: []byte("external file via proxy"), + expectHeaderETag: "resp-etag", + expectHeaderLastMod: time.RFC1123, + expectErrContains: "", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ctx := context.Background() + tempDir := t.TempDir() + fileName := filepath.Join(tempDir, "external.conf") + + ts := httptest.NewServer(test.handler) + defer ts.Close() + + u, err := url.Parse(ts.URL) + require.NoError(t, err) + host := u.Hostname() + + fakeFileServiceClient := &v1fakes.FakeFileServiceClient{} + fileManagerService := NewFileManagerService(fakeFileServiceClient, types.AgentConfig(), &sync.RWMutex{}) + + eds := &config.ExternalDataSource{ + ProxyURL: config.ProxyURL{URL: ""}, + AllowedDomains: []string{host}, + MaxBytes: int64(test.maxBytes), + } + + if test.allowedDomains != nil { + eds.AllowedDomains = test.allowedDomains + } + + if test.name == "Test 5: ProxyWithConditionalHeaders" { + manifestFiles := map[string]*model.ManifestFile{ + fileName: { + ManifestFileMeta: &model.ManifestFileMeta{ + Name: fileName, + ETag: "manifest-test-etag", + LastModified: time.RFC1123, + }, + }, + } + manifestJSON, mErr := json.MarshalIndent(manifestFiles, "", " ") + require.NoError(t, mErr) + + manifestFile, mErr := os.CreateTemp(tempDir, "manifest.json") + require.NoError(t, mErr) + _, mErr = manifestFile.Write(manifestJSON) + require.NoError(t, mErr) + _ = manifestFile.Close() + + fileManagerService.agentConfig.LibDir = tempDir + fileManagerService.manifestFilePath = manifestFile.Name() + + eds.ProxyURL = config.ProxyURL{URL: ts.URL} + } + + fileManagerService.agentConfig.ExternalDataSource = eds + + fileManagerService.fileActions = map[string]*model.FileCache{ + fileName: { + File: &mpi.File{ + FileMeta: &mpi.FileMeta{Name: fileName}, + ExternalDataSource: &mpi.ExternalDataSource{Location: ts.URL}, + }, + Action: model.ExternalFile, + }, + } + + err = fileManagerService.downloadUpdatedFilesToTempLocation(ctx) + + if test.expectError { + require.Error(t, err) + if test.expectErrContains != "" { + assert.Contains(t, err.Error(), test.expectErrContains) + } + _, statErr := os.Stat(tempFilePath(fileName)) + assert.True(t, os.IsNotExist(statErr)) + + return + } + + require.NoError(t, err) + + if test.expectTempFile { + b, readErr := os.ReadFile(tempFilePath(fileName)) + require.NoError(t, readErr) + assert.Equal(t, test.expectContent, b) + + h, ok := fileManagerService.externalFileHeaders[fileName] + require.True(t, ok) + assert.Equal(t, test.expectHeaderETag, h.ETag) + assert.Equal(t, test.expectHeaderLastMod, h.LastModified) + + _ = os.Remove(tempFilePath(fileName)) + } else { + _, statErr := os.Stat(tempFilePath(fileName)) + assert.True(t, os.IsNotExist(statErr)) + } + }) + } +} + +func TestFileManagerService_DownloadFileContent_MaxBytesLimit(t *testing.T) { + ctx := context.Background() + fms := NewFileManagerService(nil, types.AgentConfig(), &sync.RWMutex{}) + + // test server returns 10 bytes, we set MaxBytes to 4 and expect only 4 bytes returned + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("ETag", "etag-1") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("0123456789")) + })) + defer ts.Close() + + u, err := url.Parse(ts.URL) + require.NoError(t, err) + + fms.agentConfig.ExternalDataSource = &config.ExternalDataSource{ + AllowedDomains: []string{u.Hostname()}, + MaxBytes: 4, + } + + fileName := filepath.Join(t.TempDir(), "external.conf") + file := &mpi.File{ + FileMeta: &mpi.FileMeta{Name: fileName}, + ExternalDataSource: &mpi.ExternalDataSource{Location: ts.URL}, + } + + content, headers, err := fms.downloadFileContent(ctx, file) + require.NoError(t, err) + assert.Len(t, content, 4) + assert.Equal(t, "etag-1", headers.ETag) +} + +func TestFileManagerService_TestDownloadFileContent_InvalidProxyURL(t *testing.T) { + ctx := context.Background() + fms := NewFileManagerService(nil, types.AgentConfig(), &sync.RWMutex{}) + + downURL := "http://example.com/file" + fms.agentConfig.ExternalDataSource = &config.ExternalDataSource{ + AllowedDomains: []string{"example.com"}, + ProxyURL: config.ProxyURL{URL: "http://:"}, + } + + file := &mpi.File{ + FileMeta: &mpi.FileMeta{Name: "/tmp/file"}, + ExternalDataSource: &mpi.ExternalDataSource{Location: downURL}, + } + + _, _, err := fms.downloadFileContent(ctx, file) + require.Error(t, err) + if !strings.Contains(err.Error(), "invalid proxy URL configured") && + !strings.Contains(err.Error(), "failed to execute download request") && + !strings.Contains(err.Error(), "proxyconnect") { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestFileManagerService_IsDomainAllowed(t *testing.T) { + type testCase struct { + name string + url string + allowedDomains []string + expected bool + } + + tests := []testCase{ + { + name: "Invalid URL (Percent)", + url: "http://%", + allowedDomains: []string{"example.com"}, + expected: false, + }, + { + name: "Invalid URL (Empty Host)", + url: "http://", + allowedDomains: []string{"example.com"}, + expected: false, + }, + { + name: "Empty Allowed List", + url: "http://example.com/path", + allowedDomains: []string{""}, + expected: false, + }, + { + name: "Basic Match", + url: "http://example.com/path", + allowedDomains: []string{"example.com"}, + expected: true, + }, + { + name: "Wildcard Subdomain Match", + url: "http://sub.example.com/path", + allowedDomains: []string{"*.example.com"}, + expected: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + actual := isDomainAllowed(tc.url, tc.allowedDomains) + assert.Equal(t, tc.expected, actual, "for URL: %s and domains: %v", tc.url, tc.allowedDomains) + }) + } +} + +func TestFileManagerService_IsMatchesWildcardDomain(t *testing.T) { + type testCase struct { + name string + hostname string + pattern string + expected bool + } + + tests := []testCase{ + { + name: "True Match - Subdomain", + hostname: "sub.example.com", + pattern: "*.example.com", + expected: true, + }, + { + name: "True Match - Exact Base Domain", + hostname: "example.com", + pattern: "*.example.com", + expected: true, + }, + { + name: "False Match - Bad Domain Suffix", + hostname: "badexample.com", + pattern: "*.example.com", + expected: false, + }, + { + name: "False Match - No Wildcard Prefix", + hostname: "test.com", + pattern: "google.com", + expected: false, + }, + { + name: "False Match - Different Suffix", + hostname: "sub.anotherexample.com", + pattern: "*.example.com", + expected: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + actual := isMatchesWildcardDomain(tc.hostname, tc.pattern) + assert.Equal(t, tc.expected, actual, "Hostname: %s, Pattern: %s", tc.hostname, tc.pattern) + }) + } +} diff --git a/internal/file/file_service_operator.go b/internal/file/file_service_operator.go index 19211c600..35e4731da 100644 --- a/internal/file/file_service_operator.go +++ b/internal/file/file_service_operator.go @@ -79,7 +79,10 @@ func (fso *FileServiceOperator) File( defer backoffCancel() getFile := func() (*mpi.GetFileResponse, error) { - return fso.fileServiceClient.GetFile(ctx, &mpi.GetFileRequest{ + grpcCtx, cancel := context.WithTimeout(ctx, fso.agentConfig.Client.FileDownloadTimeout) + defer cancel() + + return fso.fileServiceClient.GetFile(grpcCtx, &mpi.GetFileRequest{ MessageMeta: &mpi.MessageMeta{ MessageId: id.GenerateMessageID(), CorrelationId: logger.CorrelationID(ctx), @@ -107,7 +110,7 @@ func (fso *FileServiceOperator) File( return writeErr } - return fso.validateFileHash(tempFilePath, expectedHash) + return fso.ValidateFileHash(ctx, tempFilePath, expectedHash) } func (fso *FileServiceOperator) UpdateOverview( @@ -225,7 +228,10 @@ func (fso *FileServiceOperator) ChunkedFile( ) error { slog.DebugContext(ctx, "Getting chunked file", "file", file.GetFileMeta().GetName()) - stream, err := fso.fileServiceClient.GetFileStream(ctx, &mpi.GetFileRequest{ + grpcCtx, cancel := context.WithTimeout(ctx, fso.agentConfig.Client.FileDownloadTimeout) + defer cancel() + + stream, err := fso.fileServiceClient.GetFileStream(grpcCtx, &mpi.GetFileRequest{ MessageMeta: &mpi.MessageMeta{ MessageId: id.GenerateMessageID(), CorrelationId: logger.CorrelationID(ctx), @@ -254,7 +260,7 @@ func (fso *FileServiceOperator) ChunkedFile( return writeChunkedFileError } - return fso.validateFileHash(tempFilePath, expectedHash) + return fso.ValidateFileHash(ctx, tempFilePath, expectedHash) } func (fso *FileServiceOperator) UpdateFile( @@ -278,7 +284,7 @@ func (fso *FileServiceOperator) UpdateFile( // renameFile, renames (moves) file from tempDir to new location to update file. func (fso *FileServiceOperator) RenameFile( - ctx context.Context, hash, source, desination string, + ctx context.Context, source, desination string, ) error { slog.DebugContext(ctx, fmt.Sprintf("Renaming file %s to %s", source, desination)) @@ -292,10 +298,11 @@ func (fso *FileServiceOperator) RenameFile( return fmt.Errorf("failed to rename file: %w", moveErr) } - return fso.validateFileHash(desination, hash) + return nil } -func (fso *FileServiceOperator) validateFileHash(filePath, expectedHash string) error { +func (fso *FileServiceOperator) ValidateFileHash(ctx context.Context, filePath, expectedHash string) error { + slog.DebugContext(ctx, "Validating file hash for file ", "file_path", filePath) content, err := os.ReadFile(filePath) if err != nil { return err @@ -371,12 +378,15 @@ func (fso *FileServiceOperator) sendUpdateFileRequest( return nil, errors.New("CreateConnection rpc has not being called yet") } - response, updateError := fso.fileServiceClient.UpdateFile(ctx, request) + grpcCtx, cancel := context.WithTimeout(ctx, fso.agentConfig.Client.FileDownloadTimeout) + defer cancel() + + response, updateError := fso.fileServiceClient.UpdateFile(grpcCtx, request) validatedError := internalgrpc.ValidateGrpcError(updateError) if validatedError != nil { - slog.ErrorContext(ctx, "Failed to send update file", "error", validatedError) + slog.ErrorContext(grpcCtx, "Failed to send update file", "error", validatedError) return nil, validatedError } @@ -406,7 +416,10 @@ func (fso *FileServiceOperator) sendUpdateFileStream( return errors.New("file chunk size must be greater than zero") } - updateFileStreamClient, err := fso.fileServiceClient.UpdateFileStream(ctx) + grpcCtx, cancel := context.WithTimeout(ctx, fso.agentConfig.Client.FileDownloadTimeout) + defer cancel() + + updateFileStreamClient, err := fso.fileServiceClient.UpdateFileStream(grpcCtx) if err != nil { return err } diff --git a/internal/file/filefakes/fake_file_manager_service_interface.go b/internal/file/filefakes/fake_file_manager_service_interface.go index f2af670fe..ec52d62d1 100644 --- a/internal/file/filefakes/fake_file_manager_service_interface.go +++ b/internal/file/filefakes/fake_file_manager_service_interface.go @@ -61,6 +61,23 @@ type FakeFileManagerServiceInterface struct { result1 map[string]*model.FileCache result2 error } + DownloadStub func(context.Context, string, *v1.FileMeta) ([]byte, model.DownloadHeader, error) + downloadMutex sync.RWMutex + downloadArgsForCall []struct { + arg1 context.Context + arg2 string + arg3 *v1.FileMeta + } + downloadReturns struct { + result1 []byte + result2 model.DownloadHeader + result3 error + } + downloadReturnsOnCall map[int]struct { + result1 []byte + result2 model.DownloadHeader + result3 error + } IsConnectedStub func() bool isConnectedMutex sync.RWMutex isConnectedArgsForCall []struct { @@ -361,6 +378,75 @@ func (fake *FakeFileManagerServiceInterface) DetermineFileActionsReturnsOnCall(i }{result1, result2} } +func (fake *FakeFileManagerServiceInterface) Download(arg1 context.Context, arg2 string, arg3 *v1.FileMeta) ([]byte, model.DownloadHeader, error) { + fake.downloadMutex.Lock() + ret, specificReturn := fake.downloadReturnsOnCall[len(fake.downloadArgsForCall)] + fake.downloadArgsForCall = append(fake.downloadArgsForCall, struct { + arg1 context.Context + arg2 string + arg3 *v1.FileMeta + }{arg1, arg2, arg3}) + stub := fake.DownloadStub + fakeReturns := fake.downloadReturns + fake.recordInvocation("Download", []interface{}{arg1, arg2, arg3}) + fake.downloadMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3) + } + if specificReturn { + return ret.result1, ret.result2, ret.result3 + } + return fakeReturns.result1, fakeReturns.result2, fakeReturns.result3 +} + +func (fake *FakeFileManagerServiceInterface) DownloadCallCount() int { + fake.downloadMutex.RLock() + defer fake.downloadMutex.RUnlock() + return len(fake.downloadArgsForCall) +} + +func (fake *FakeFileManagerServiceInterface) DownloadCalls(stub func(context.Context, string, *v1.FileMeta) ([]byte, model.DownloadHeader, error)) { + fake.downloadMutex.Lock() + defer fake.downloadMutex.Unlock() + fake.DownloadStub = stub +} + +func (fake *FakeFileManagerServiceInterface) DownloadArgsForCall(i int) (context.Context, string, *v1.FileMeta) { + fake.downloadMutex.RLock() + defer fake.downloadMutex.RUnlock() + argsForCall := fake.downloadArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *FakeFileManagerServiceInterface) DownloadReturns(result1 []byte, result2 model.DownloadHeader, result3 error) { + fake.downloadMutex.Lock() + defer fake.downloadMutex.Unlock() + fake.DownloadStub = nil + fake.downloadReturns = struct { + result1 []byte + result2 model.DownloadHeader + result3 error + }{result1, result2, result3} +} + +func (fake *FakeFileManagerServiceInterface) DownloadReturnsOnCall(i int, result1 []byte, result2 model.DownloadHeader, result3 error) { + fake.downloadMutex.Lock() + defer fake.downloadMutex.Unlock() + fake.DownloadStub = nil + if fake.downloadReturnsOnCall == nil { + fake.downloadReturnsOnCall = make(map[int]struct { + result1 []byte + result2 model.DownloadHeader + result3 error + }) + } + fake.downloadReturnsOnCall[i] = struct { + result1 []byte + result2 model.DownloadHeader + result3 error + }{result1, result2, result3} +} + func (fake *FakeFileManagerServiceInterface) IsConnected() bool { fake.isConnectedMutex.Lock() ret, specificReturn := fake.isConnectedReturnsOnCall[len(fake.isConnectedArgsForCall)] @@ -617,6 +703,8 @@ func (fake *FakeFileManagerServiceInterface) Invocations() map[string][][]interf defer fake.configUploadMutex.RUnlock() fake.determineFileActionsMutex.RLock() defer fake.determineFileActionsMutex.RUnlock() + fake.downloadMutex.RLock() + defer fake.downloadMutex.RUnlock() fake.isConnectedMutex.RLock() defer fake.isConnectedMutex.RUnlock() fake.resetClientMutex.RLock() diff --git a/internal/file/filefakes/fake_file_service_operator_interface.go b/internal/file/filefakes/fake_file_service_operator_interface.go new file mode 100644 index 000000000..dc559e41d --- /dev/null +++ b/internal/file/filefakes/fake_file_service_operator_interface.go @@ -0,0 +1,662 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package filefakes + +import ( + "context" + "sync" + + v1 "github.com/nginx/agent/v3/api/grpc/mpi/v1" +) + +type FakeFileServiceOperatorInterface struct { + ChunkedFileStub func(context.Context, *v1.File, string, string) error + chunkedFileMutex sync.RWMutex + chunkedFileArgsForCall []struct { + arg1 context.Context + arg2 *v1.File + arg3 string + arg4 string + } + chunkedFileReturns struct { + result1 error + } + chunkedFileReturnsOnCall map[int]struct { + result1 error + } + FileStub func(context.Context, *v1.File, string, string) error + fileMutex sync.RWMutex + fileArgsForCall []struct { + arg1 context.Context + arg2 *v1.File + arg3 string + arg4 string + } + fileReturns struct { + result1 error + } + fileReturnsOnCall map[int]struct { + result1 error + } + IsConnectedStub func() bool + isConnectedMutex sync.RWMutex + isConnectedArgsForCall []struct { + } + isConnectedReturns struct { + result1 bool + } + isConnectedReturnsOnCall map[int]struct { + result1 bool + } + RenameFileStub func(context.Context, string, string) error + renameFileMutex sync.RWMutex + renameFileArgsForCall []struct { + arg1 context.Context + arg2 string + arg3 string + } + renameFileReturns struct { + result1 error + } + renameFileReturnsOnCall map[int]struct { + result1 error + } + SetIsConnectedStub func(bool) + setIsConnectedMutex sync.RWMutex + setIsConnectedArgsForCall []struct { + arg1 bool + } + UpdateClientStub func(context.Context, v1.FileServiceClient) + updateClientMutex sync.RWMutex + updateClientArgsForCall []struct { + arg1 context.Context + arg2 v1.FileServiceClient + } + UpdateFileStub func(context.Context, string, *v1.File) error + updateFileMutex sync.RWMutex + updateFileArgsForCall []struct { + arg1 context.Context + arg2 string + arg3 *v1.File + } + updateFileReturns struct { + result1 error + } + updateFileReturnsOnCall map[int]struct { + result1 error + } + UpdateOverviewStub func(context.Context, string, []*v1.File, string, int) error + updateOverviewMutex sync.RWMutex + updateOverviewArgsForCall []struct { + arg1 context.Context + arg2 string + arg3 []*v1.File + arg4 string + arg5 int + } + updateOverviewReturns struct { + result1 error + } + updateOverviewReturnsOnCall map[int]struct { + result1 error + } + ValidateFileHashStub func(context.Context, string, string) error + validateFileHashMutex sync.RWMutex + validateFileHashArgsForCall []struct { + arg1 context.Context + arg2 string + arg3 string + } + validateFileHashReturns struct { + result1 error + } + validateFileHashReturnsOnCall map[int]struct { + result1 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeFileServiceOperatorInterface) ChunkedFile(arg1 context.Context, arg2 *v1.File, arg3 string, arg4 string) error { + fake.chunkedFileMutex.Lock() + ret, specificReturn := fake.chunkedFileReturnsOnCall[len(fake.chunkedFileArgsForCall)] + fake.chunkedFileArgsForCall = append(fake.chunkedFileArgsForCall, struct { + arg1 context.Context + arg2 *v1.File + arg3 string + arg4 string + }{arg1, arg2, arg3, arg4}) + stub := fake.ChunkedFileStub + fakeReturns := fake.chunkedFileReturns + fake.recordInvocation("ChunkedFile", []interface{}{arg1, arg2, arg3, arg4}) + fake.chunkedFileMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3, arg4) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeFileServiceOperatorInterface) ChunkedFileCallCount() int { + fake.chunkedFileMutex.RLock() + defer fake.chunkedFileMutex.RUnlock() + return len(fake.chunkedFileArgsForCall) +} + +func (fake *FakeFileServiceOperatorInterface) ChunkedFileCalls(stub func(context.Context, *v1.File, string, string) error) { + fake.chunkedFileMutex.Lock() + defer fake.chunkedFileMutex.Unlock() + fake.ChunkedFileStub = stub +} + +func (fake *FakeFileServiceOperatorInterface) ChunkedFileArgsForCall(i int) (context.Context, *v1.File, string, string) { + fake.chunkedFileMutex.RLock() + defer fake.chunkedFileMutex.RUnlock() + argsForCall := fake.chunkedFileArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4 +} + +func (fake *FakeFileServiceOperatorInterface) ChunkedFileReturns(result1 error) { + fake.chunkedFileMutex.Lock() + defer fake.chunkedFileMutex.Unlock() + fake.ChunkedFileStub = nil + fake.chunkedFileReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeFileServiceOperatorInterface) ChunkedFileReturnsOnCall(i int, result1 error) { + fake.chunkedFileMutex.Lock() + defer fake.chunkedFileMutex.Unlock() + fake.ChunkedFileStub = nil + if fake.chunkedFileReturnsOnCall == nil { + fake.chunkedFileReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.chunkedFileReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeFileServiceOperatorInterface) File(arg1 context.Context, arg2 *v1.File, arg3 string, arg4 string) error { + fake.fileMutex.Lock() + ret, specificReturn := fake.fileReturnsOnCall[len(fake.fileArgsForCall)] + fake.fileArgsForCall = append(fake.fileArgsForCall, struct { + arg1 context.Context + arg2 *v1.File + arg3 string + arg4 string + }{arg1, arg2, arg3, arg4}) + stub := fake.FileStub + fakeReturns := fake.fileReturns + fake.recordInvocation("File", []interface{}{arg1, arg2, arg3, arg4}) + fake.fileMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3, arg4) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeFileServiceOperatorInterface) FileCallCount() int { + fake.fileMutex.RLock() + defer fake.fileMutex.RUnlock() + return len(fake.fileArgsForCall) +} + +func (fake *FakeFileServiceOperatorInterface) FileCalls(stub func(context.Context, *v1.File, string, string) error) { + fake.fileMutex.Lock() + defer fake.fileMutex.Unlock() + fake.FileStub = stub +} + +func (fake *FakeFileServiceOperatorInterface) FileArgsForCall(i int) (context.Context, *v1.File, string, string) { + fake.fileMutex.RLock() + defer fake.fileMutex.RUnlock() + argsForCall := fake.fileArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4 +} + +func (fake *FakeFileServiceOperatorInterface) FileReturns(result1 error) { + fake.fileMutex.Lock() + defer fake.fileMutex.Unlock() + fake.FileStub = nil + fake.fileReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeFileServiceOperatorInterface) FileReturnsOnCall(i int, result1 error) { + fake.fileMutex.Lock() + defer fake.fileMutex.Unlock() + fake.FileStub = nil + if fake.fileReturnsOnCall == nil { + fake.fileReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.fileReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeFileServiceOperatorInterface) IsConnected() bool { + fake.isConnectedMutex.Lock() + ret, specificReturn := fake.isConnectedReturnsOnCall[len(fake.isConnectedArgsForCall)] + fake.isConnectedArgsForCall = append(fake.isConnectedArgsForCall, struct { + }{}) + stub := fake.IsConnectedStub + fakeReturns := fake.isConnectedReturns + fake.recordInvocation("IsConnected", []interface{}{}) + fake.isConnectedMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeFileServiceOperatorInterface) IsConnectedCallCount() int { + fake.isConnectedMutex.RLock() + defer fake.isConnectedMutex.RUnlock() + return len(fake.isConnectedArgsForCall) +} + +func (fake *FakeFileServiceOperatorInterface) IsConnectedCalls(stub func() bool) { + fake.isConnectedMutex.Lock() + defer fake.isConnectedMutex.Unlock() + fake.IsConnectedStub = stub +} + +func (fake *FakeFileServiceOperatorInterface) IsConnectedReturns(result1 bool) { + fake.isConnectedMutex.Lock() + defer fake.isConnectedMutex.Unlock() + fake.IsConnectedStub = nil + fake.isConnectedReturns = struct { + result1 bool + }{result1} +} + +func (fake *FakeFileServiceOperatorInterface) IsConnectedReturnsOnCall(i int, result1 bool) { + fake.isConnectedMutex.Lock() + defer fake.isConnectedMutex.Unlock() + fake.IsConnectedStub = nil + if fake.isConnectedReturnsOnCall == nil { + fake.isConnectedReturnsOnCall = make(map[int]struct { + result1 bool + }) + } + fake.isConnectedReturnsOnCall[i] = struct { + result1 bool + }{result1} +} + +func (fake *FakeFileServiceOperatorInterface) RenameFile(arg1 context.Context, arg2 string, arg3 string) error { + fake.renameFileMutex.Lock() + ret, specificReturn := fake.renameFileReturnsOnCall[len(fake.renameFileArgsForCall)] + fake.renameFileArgsForCall = append(fake.renameFileArgsForCall, struct { + arg1 context.Context + arg2 string + arg3 string + }{arg1, arg2, arg3}) + stub := fake.RenameFileStub + fakeReturns := fake.renameFileReturns + fake.recordInvocation("RenameFile", []interface{}{arg1, arg2, arg3}) + fake.renameFileMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeFileServiceOperatorInterface) RenameFileCallCount() int { + fake.renameFileMutex.RLock() + defer fake.renameFileMutex.RUnlock() + return len(fake.renameFileArgsForCall) +} + +func (fake *FakeFileServiceOperatorInterface) RenameFileCalls(stub func(context.Context, string, string) error) { + fake.renameFileMutex.Lock() + defer fake.renameFileMutex.Unlock() + fake.RenameFileStub = stub +} + +func (fake *FakeFileServiceOperatorInterface) RenameFileArgsForCall(i int) (context.Context, string, string) { + fake.renameFileMutex.RLock() + defer fake.renameFileMutex.RUnlock() + argsForCall := fake.renameFileArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *FakeFileServiceOperatorInterface) RenameFileReturns(result1 error) { + fake.renameFileMutex.Lock() + defer fake.renameFileMutex.Unlock() + fake.RenameFileStub = nil + fake.renameFileReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeFileServiceOperatorInterface) RenameFileReturnsOnCall(i int, result1 error) { + fake.renameFileMutex.Lock() + defer fake.renameFileMutex.Unlock() + fake.RenameFileStub = nil + if fake.renameFileReturnsOnCall == nil { + fake.renameFileReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.renameFileReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeFileServiceOperatorInterface) SetIsConnected(arg1 bool) { + fake.setIsConnectedMutex.Lock() + fake.setIsConnectedArgsForCall = append(fake.setIsConnectedArgsForCall, struct { + arg1 bool + }{arg1}) + stub := fake.SetIsConnectedStub + fake.recordInvocation("SetIsConnected", []interface{}{arg1}) + fake.setIsConnectedMutex.Unlock() + if stub != nil { + fake.SetIsConnectedStub(arg1) + } +} + +func (fake *FakeFileServiceOperatorInterface) SetIsConnectedCallCount() int { + fake.setIsConnectedMutex.RLock() + defer fake.setIsConnectedMutex.RUnlock() + return len(fake.setIsConnectedArgsForCall) +} + +func (fake *FakeFileServiceOperatorInterface) SetIsConnectedCalls(stub func(bool)) { + fake.setIsConnectedMutex.Lock() + defer fake.setIsConnectedMutex.Unlock() + fake.SetIsConnectedStub = stub +} + +func (fake *FakeFileServiceOperatorInterface) SetIsConnectedArgsForCall(i int) bool { + fake.setIsConnectedMutex.RLock() + defer fake.setIsConnectedMutex.RUnlock() + argsForCall := fake.setIsConnectedArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeFileServiceOperatorInterface) UpdateClient(arg1 context.Context, arg2 v1.FileServiceClient) { + fake.updateClientMutex.Lock() + fake.updateClientArgsForCall = append(fake.updateClientArgsForCall, struct { + arg1 context.Context + arg2 v1.FileServiceClient + }{arg1, arg2}) + stub := fake.UpdateClientStub + fake.recordInvocation("UpdateClient", []interface{}{arg1, arg2}) + fake.updateClientMutex.Unlock() + if stub != nil { + fake.UpdateClientStub(arg1, arg2) + } +} + +func (fake *FakeFileServiceOperatorInterface) UpdateClientCallCount() int { + fake.updateClientMutex.RLock() + defer fake.updateClientMutex.RUnlock() + return len(fake.updateClientArgsForCall) +} + +func (fake *FakeFileServiceOperatorInterface) UpdateClientCalls(stub func(context.Context, v1.FileServiceClient)) { + fake.updateClientMutex.Lock() + defer fake.updateClientMutex.Unlock() + fake.UpdateClientStub = stub +} + +func (fake *FakeFileServiceOperatorInterface) UpdateClientArgsForCall(i int) (context.Context, v1.FileServiceClient) { + fake.updateClientMutex.RLock() + defer fake.updateClientMutex.RUnlock() + argsForCall := fake.updateClientArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeFileServiceOperatorInterface) UpdateFile(arg1 context.Context, arg2 string, arg3 *v1.File) error { + fake.updateFileMutex.Lock() + ret, specificReturn := fake.updateFileReturnsOnCall[len(fake.updateFileArgsForCall)] + fake.updateFileArgsForCall = append(fake.updateFileArgsForCall, struct { + arg1 context.Context + arg2 string + arg3 *v1.File + }{arg1, arg2, arg3}) + stub := fake.UpdateFileStub + fakeReturns := fake.updateFileReturns + fake.recordInvocation("UpdateFile", []interface{}{arg1, arg2, arg3}) + fake.updateFileMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeFileServiceOperatorInterface) UpdateFileCallCount() int { + fake.updateFileMutex.RLock() + defer fake.updateFileMutex.RUnlock() + return len(fake.updateFileArgsForCall) +} + +func (fake *FakeFileServiceOperatorInterface) UpdateFileCalls(stub func(context.Context, string, *v1.File) error) { + fake.updateFileMutex.Lock() + defer fake.updateFileMutex.Unlock() + fake.UpdateFileStub = stub +} + +func (fake *FakeFileServiceOperatorInterface) UpdateFileArgsForCall(i int) (context.Context, string, *v1.File) { + fake.updateFileMutex.RLock() + defer fake.updateFileMutex.RUnlock() + argsForCall := fake.updateFileArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *FakeFileServiceOperatorInterface) UpdateFileReturns(result1 error) { + fake.updateFileMutex.Lock() + defer fake.updateFileMutex.Unlock() + fake.UpdateFileStub = nil + fake.updateFileReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeFileServiceOperatorInterface) UpdateFileReturnsOnCall(i int, result1 error) { + fake.updateFileMutex.Lock() + defer fake.updateFileMutex.Unlock() + fake.UpdateFileStub = nil + if fake.updateFileReturnsOnCall == nil { + fake.updateFileReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.updateFileReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeFileServiceOperatorInterface) UpdateOverview(arg1 context.Context, arg2 string, arg3 []*v1.File, arg4 string, arg5 int) error { + var arg3Copy []*v1.File + if arg3 != nil { + arg3Copy = make([]*v1.File, len(arg3)) + copy(arg3Copy, arg3) + } + fake.updateOverviewMutex.Lock() + ret, specificReturn := fake.updateOverviewReturnsOnCall[len(fake.updateOverviewArgsForCall)] + fake.updateOverviewArgsForCall = append(fake.updateOverviewArgsForCall, struct { + arg1 context.Context + arg2 string + arg3 []*v1.File + arg4 string + arg5 int + }{arg1, arg2, arg3Copy, arg4, arg5}) + stub := fake.UpdateOverviewStub + fakeReturns := fake.updateOverviewReturns + fake.recordInvocation("UpdateOverview", []interface{}{arg1, arg2, arg3Copy, arg4, arg5}) + fake.updateOverviewMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3, arg4, arg5) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeFileServiceOperatorInterface) UpdateOverviewCallCount() int { + fake.updateOverviewMutex.RLock() + defer fake.updateOverviewMutex.RUnlock() + return len(fake.updateOverviewArgsForCall) +} + +func (fake *FakeFileServiceOperatorInterface) UpdateOverviewCalls(stub func(context.Context, string, []*v1.File, string, int) error) { + fake.updateOverviewMutex.Lock() + defer fake.updateOverviewMutex.Unlock() + fake.UpdateOverviewStub = stub +} + +func (fake *FakeFileServiceOperatorInterface) UpdateOverviewArgsForCall(i int) (context.Context, string, []*v1.File, string, int) { + fake.updateOverviewMutex.RLock() + defer fake.updateOverviewMutex.RUnlock() + argsForCall := fake.updateOverviewArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4, argsForCall.arg5 +} + +func (fake *FakeFileServiceOperatorInterface) UpdateOverviewReturns(result1 error) { + fake.updateOverviewMutex.Lock() + defer fake.updateOverviewMutex.Unlock() + fake.UpdateOverviewStub = nil + fake.updateOverviewReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeFileServiceOperatorInterface) UpdateOverviewReturnsOnCall(i int, result1 error) { + fake.updateOverviewMutex.Lock() + defer fake.updateOverviewMutex.Unlock() + fake.UpdateOverviewStub = nil + if fake.updateOverviewReturnsOnCall == nil { + fake.updateOverviewReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.updateOverviewReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeFileServiceOperatorInterface) ValidateFileHash(arg1 context.Context, arg2 string, arg3 string) error { + fake.validateFileHashMutex.Lock() + ret, specificReturn := fake.validateFileHashReturnsOnCall[len(fake.validateFileHashArgsForCall)] + fake.validateFileHashArgsForCall = append(fake.validateFileHashArgsForCall, struct { + arg1 context.Context + arg2 string + arg3 string + }{arg1, arg2, arg3}) + stub := fake.ValidateFileHashStub + fakeReturns := fake.validateFileHashReturns + fake.recordInvocation("ValidateFileHash", []interface{}{arg1, arg2, arg3}) + fake.validateFileHashMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeFileServiceOperatorInterface) ValidateFileHashCallCount() int { + fake.validateFileHashMutex.RLock() + defer fake.validateFileHashMutex.RUnlock() + return len(fake.validateFileHashArgsForCall) +} + +func (fake *FakeFileServiceOperatorInterface) ValidateFileHashCalls(stub func(context.Context, string, string) error) { + fake.validateFileHashMutex.Lock() + defer fake.validateFileHashMutex.Unlock() + fake.ValidateFileHashStub = stub +} + +func (fake *FakeFileServiceOperatorInterface) ValidateFileHashArgsForCall(i int) (context.Context, string, string) { + fake.validateFileHashMutex.RLock() + defer fake.validateFileHashMutex.RUnlock() + argsForCall := fake.validateFileHashArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *FakeFileServiceOperatorInterface) ValidateFileHashReturns(result1 error) { + fake.validateFileHashMutex.Lock() + defer fake.validateFileHashMutex.Unlock() + fake.ValidateFileHashStub = nil + fake.validateFileHashReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeFileServiceOperatorInterface) ValidateFileHashReturnsOnCall(i int, result1 error) { + fake.validateFileHashMutex.Lock() + defer fake.validateFileHashMutex.Unlock() + fake.ValidateFileHashStub = nil + if fake.validateFileHashReturnsOnCall == nil { + fake.validateFileHashReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.validateFileHashReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeFileServiceOperatorInterface) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.chunkedFileMutex.RLock() + defer fake.chunkedFileMutex.RUnlock() + fake.fileMutex.RLock() + defer fake.fileMutex.RUnlock() + fake.isConnectedMutex.RLock() + defer fake.isConnectedMutex.RUnlock() + fake.renameFileMutex.RLock() + defer fake.renameFileMutex.RUnlock() + fake.setIsConnectedMutex.RLock() + defer fake.setIsConnectedMutex.RUnlock() + fake.updateClientMutex.RLock() + defer fake.updateClientMutex.RUnlock() + fake.updateFileMutex.RLock() + defer fake.updateFileMutex.RUnlock() + fake.updateOverviewMutex.RLock() + defer fake.updateOverviewMutex.RUnlock() + fake.validateFileHashMutex.RLock() + defer fake.validateFileHashMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeFileServiceOperatorInterface) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} diff --git a/internal/model/config.go b/internal/model/config.go index d13c299fd..324a5ffd6 100644 --- a/internal/model/config.go +++ b/internal/model/config.go @@ -42,6 +42,10 @@ type ManifestFileMeta struct { Name string `json:"name"` // The hash of the file contents sha256, hex encoded Hash string `json:"hash"` + // ETag of the 3rd Party external file + ETag string `json:"etag"` + // Last modified time of the 3rd Party external file + LastModified string `json:"last_modified"` // The size of the file in bytes Size int64 `json:"size"` // File referenced in the NGINX config diff --git a/internal/model/file.go b/internal/model/file.go index fc6c5baca..020c0d543 100644 --- a/internal/model/file.go +++ b/internal/model/file.go @@ -19,4 +19,10 @@ const ( Update Delete Unchanged + ExternalFile ) + +type DownloadHeader struct { + ETag string + LastModified string +} diff --git a/test/mock/grpc/mock_management_command_service.go b/test/mock/grpc/mock_management_command_service.go index f68c4c7cd..b1badb443 100644 --- a/test/mock/grpc/mock_management_command_service.go +++ b/test/mock/grpc/mock_management_command_service.go @@ -577,7 +577,8 @@ func processConfigApplyRequestBody(c *gin.Context, initialFiles []*mpi.File) ([] } else { newFile := &mpi.File{ FileMeta: &mpi.FileMeta{ - Name: ed.FilePath, + Name: ed.FilePath, + Permissions: "0644", }, ExternalDataSource: &mpi.ExternalDataSource{ Location: ed.Location,