-
Notifications
You must be signed in to change notification settings - Fork 99
Make File Transfer Storage Size Configurable #5478
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
# Kind can be one of: | ||
# - breaking-change: a change to previously-documented behavior | ||
# - deprecation: functionality that is being removed in a later release | ||
# - bug-fix: fixes a problem in a previous version | ||
# - enhancement: extends functionality but does not break or fix existing behavior | ||
# - feature: new functionality | ||
# - known-issue: problems that we are aware of in a given version | ||
# - security: impacts on the security of a product or a user’s deployment. | ||
# - upgrade: important information for someone upgrading from a prior version | ||
# - other: does not fit into any of the other categories | ||
kind: enhancement | ||
|
||
# Change summary; a 80ish characters long description of the change. | ||
summary: Makes file storage size configurable | ||
|
||
# Long description; in case the summary is not enough to describe the change | ||
# this field accommodate a description without length limits. | ||
# NOTE: This field will be rendered only for breaking-change and known-issue kinds at the moment. | ||
#description: | ||
|
||
# Affected component; a word indicating the component this changeset affects. | ||
component: fleet-server | ||
|
||
# PR URL; optional; the PR number that added the changeset. | ||
# If not present is automatically filled by the tooling finding the PR where this changelog fragment has been added. | ||
# NOTE: the tooling supports backports, so it's able to fill the original PR number instead of the backport PR number. | ||
# Please provide it if you are adding a fragment for a different PR. | ||
#pr: https://github.com/owner/repo/1234 | ||
|
||
# Issue URL; optional; the GitHub issue related to this changeset (either closes or is part of). | ||
# If not present is automatically filled by the tooling with the issue linked to the PR number. | ||
#issue: https://github.com/owner/repo/1234 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. small nit, if there is a parent issue it would be nice to have it here |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,6 +11,7 @@ import ( | |
"crypto/sha256" | ||
"encoding/hex" | ||
"encoding/json" | ||
"fmt" | ||
"io" | ||
"io/ioutil" | ||
"net/http" | ||
|
@@ -71,18 +72,6 @@ func TestUploadBeginValidation(t *testing.T) { | |
"src": "agent" | ||
}`, | ||
}, | ||
{"Oversized file should be rejected", http.StatusBadRequest, "size", | ||
`{ | ||
"file": { | ||
"size": ` + strconv.Itoa(maxFileSize+1024) + `, | ||
"name": "foo.png", | ||
"mime_type": "image/png" | ||
}, | ||
"agent_id": "foo", | ||
"action_id": "123", | ||
"src": "agent" | ||
}`, | ||
}, | ||
{"zero size file should be rejected", http.StatusBadRequest, "size", | ||
`{ | ||
"file": { | ||
|
@@ -346,6 +335,48 @@ func TestUploadBeginBadRequest(t *testing.T) { | |
assert.Equal(t, http.StatusBadRequest, rec.Code) | ||
} | ||
|
||
func TestUploadBeginFileSize(t *testing.T) { | ||
|
||
mockFile := func(size int64) string { | ||
return fmt.Sprintf(`{ | ||
"file": { | ||
"size": %d, | ||
"name": "foo.png", | ||
"mime_type": "image/png" | ||
}, | ||
"agent_id": "foo", | ||
"action_id": "123", | ||
"src": "agent" | ||
}`, size) | ||
} | ||
|
||
// now test various body contents | ||
tests := []struct { | ||
Name string | ||
MaxSize int64 | ||
ExpectStatus int | ||
InputSize int64 | ||
}{ | ||
{"MaxSize 0 allows uploads", 0, http.StatusOK, 1000}, | ||
{"MaxSize 0 allows large uploads", 0, http.StatusOK, 1024 * 1024 * 1024 * 2}, | ||
{"MaxSize 0 does not allow 0-length files", 0, http.StatusBadRequest, 0}, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems a little odd? If I set MaxSize 1, would I allow 0-length files? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nope! This test was just to make sure a size limit of "0" is not interpreted as a literal value, making "0 length files" suddenly succeed, when the limit is 0. 0-length files should always fail, but this makes sure we don't trip on an edge case when the limit is also |
||
{"Sizes larger than MaxSize are denied", 1024, http.StatusBadRequest, 2048}, | ||
{"Sizes smaller than MaxSize are allowed", 1024, http.StatusOK, 900}, | ||
} | ||
|
||
for _, tc := range tests { | ||
t.Run(tc.Name, func(t *testing.T) { | ||
|
||
hr, _, _, _ := configureUploaderMock(t, tc.MaxSize) | ||
rec := httptest.NewRecorder() | ||
req := httptest.NewRequest(http.MethodPost, RouteUploadBegin, strings.NewReader(mockFile(tc.InputSize))) | ||
hr.ServeHTTP(rec, req) | ||
assert.Equal(t, tc.ExpectStatus, rec.Code) | ||
}) | ||
} | ||
|
||
} | ||
|
||
/* | ||
Chunk data upload route | ||
*/ | ||
|
@@ -377,7 +408,7 @@ func TestChunkUploadRouteParams(t *testing.T) { | |
mockUploadInfoResult(fakebulk, file.Info{ | ||
DocID: "bar.foo", | ||
ID: mockUploadID, | ||
ChunkSize: maxFileSize, | ||
ChunkSize: file.MaxChunkSize, | ||
Total: file.MaxChunkSize + 1, | ||
Count: 2, // this is a 2-chunk "file" based on size above | ||
Start: time.Now(), | ||
|
@@ -410,7 +441,7 @@ func TestChunkUploadRequiresChunkHashHeader(t *testing.T) { | |
mockUploadInfoResult(fakebulk, file.Info{ | ||
DocID: "bar.foo", | ||
ID: mockUploadID, | ||
ChunkSize: maxFileSize, | ||
ChunkSize: file.MaxChunkSize, | ||
Total: 10, | ||
Count: 1, | ||
Start: time.Now(), | ||
|
@@ -458,7 +489,7 @@ func TestChunkUploadStatus(t *testing.T) { | |
mockUploadInfoResult(fakebulk, file.Info{ | ||
DocID: "bar.foo", | ||
ID: mockUploadID, | ||
ChunkSize: maxFileSize, | ||
ChunkSize: file.MaxChunkSize, | ||
Total: 10, | ||
Count: 1, | ||
Start: time.Now(), | ||
|
@@ -509,7 +540,7 @@ func TestChunkUploadExpiry(t *testing.T) { | |
mockUploadInfoResult(fakebulk, file.Info{ | ||
DocID: "bar.foo", | ||
ID: mockUploadID, | ||
ChunkSize: maxFileSize, | ||
ChunkSize: file.MaxChunkSize, | ||
Total: 10, | ||
Count: 1, | ||
Start: tc.StartTime, | ||
|
@@ -547,7 +578,7 @@ func TestChunkUploadWritesTimestamp(t *testing.T) { | |
mockUploadInfoResult(fakebulk, file.Info{ | ||
DocID: "bar.foo", | ||
ID: mockUploadID, | ||
ChunkSize: maxFileSize, | ||
ChunkSize: file.MaxChunkSize, | ||
Total: 10, | ||
Count: 1, | ||
Start: time.Now(), | ||
|
@@ -597,7 +628,7 @@ func TestUploadCompleteRequiresMatchingAuth(t *testing.T) { | |
mockInfo := file.Info{ | ||
DocID: "bar." + tc.AgentInFileRecord, | ||
ID: mockUploadID, | ||
ChunkSize: maxFileSize, | ||
ChunkSize: file.MaxChunkSize, | ||
Total: 10, | ||
Count: 1, | ||
Start: time.Now().Add(-time.Minute), | ||
|
@@ -998,6 +1029,10 @@ func TestUploadCompleteBadRequests(t *testing.T) { | |
|
||
// prepareUploaderMock sets up common dependencies and registers upload routes to a returned router | ||
func prepareUploaderMock(t *testing.T) (http.Handler, apiServer, *itesting.MockBulk, *MockTransport) { | ||
return configureUploaderMock(t, 0) | ||
} | ||
|
||
func configureUploaderMock(t *testing.T, fileSize int64) (http.Handler, apiServer, *itesting.MockBulk, *MockTransport) { | ||
// chunk index operations skip the bulker in order to send binary docs directly | ||
// so a mock *elasticsearch.Client needs to be be prepared | ||
es, tx := mockESClient(t) | ||
|
@@ -1034,7 +1069,7 @@ func prepareUploaderMock(t *testing.T) (http.Handler, apiServer, *itesting.MockB | |
bulker: fakebulk, | ||
chunkClient: es, | ||
cache: c, | ||
uploader: uploader.New(es, fakebulk, c, maxFileSize, maxUploadTimer), | ||
uploader: uploader.New(es, fakebulk, c, fileSize, maxUploadTimer), | ||
authAgent: func(r *http.Request, id *string, bulker bulk.Bulk, c cache.Cache) (*model.Agent, error) { | ||
return &model.Agent{ | ||
ESDocument: model.ESDocument{ | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,9 +16,10 @@ type Limit struct { | |
} | ||
|
||
type ServerLimits struct { | ||
MaxAgents int `config:"max_agents"` | ||
MaxHeaderByteSize int `config:"max_header_byte_size"` | ||
MaxConnections int `config:"max_connections"` | ||
MaxAgents int `config:"max_agents"` | ||
MaxHeaderByteSize int `config:"max_header_byte_size"` | ||
MaxConnections int `config:"max_connections"` | ||
MaxFileStorageByteSize int64 `config:"max_file_storage_size"` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder, for consistency, if this should have been There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Naming is hard and begs some questions, so please pardon me for asking a question with maybe an obvious answer, but: Why is it not
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I tried to avoid an overly-broad name in case fleet server ever needs to deal with files in another context (e.g. supporting files on the host its on, cache files, or any other situation involving "files" in fleet). I thought of this feature (file transfer to/from an agent and elasticsearch) as "file storage" to give it a narrower namespace. But it can be whatever, no attachment to the name |
||
|
||
ActionLimit Limit `config:"action_limit"` | ||
PolicyLimit Limit `config:"policy_limit"` | ||
|
@@ -47,6 +48,7 @@ func (c *ServerLimits) LoadLimits(limits *envLimits) { | |
if c.MaxConnections == 0 { | ||
c.MaxConnections = l.MaxConnections | ||
} | ||
c.MaxFileStorageByteSize = l.MaxFileStorageByteSize | ||
|
||
c.ActionLimit = mergeEnvLimit(c.ActionLimit, l.ActionLimit) | ||
c.PolicyLimit = mergeEnvLimit(c.PolicyLimit, l.PolicyLimit) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -73,7 +73,7 @@ func (u *Uploader) Begin(ctx context.Context, namespaces []string, data JSDict) | |
} | ||
|
||
size, _ := data.Int64("file", "size") | ||
if size > u.sizeLimit { | ||
if u.sizeLimit != 0 && size > u.sizeLimit { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I understand the I'm wondering if we should use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Disallowing in just this manner would lead to some wacky and hard-to-debug errors in kibana features that rely on the file transfer. Kibana features (especially in Defend) would still appear enabled and allow user actions to take place that would fail when the agent (or endpoint) receives an action and then proceeds to upload. I do see the arbitrary nature of 0's and negatives as not particularly meaningful to whichever disabled/unlimited mode here. I guess they happen to be flipped here. -1 (or any negative value) will fail to upload, as file size will always be larger than that. Negatives seem to be the disabler here (not by design, just by happenstance). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Did we consider making an explicit There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure I understand the suggestion here. change the config's type to
? That's good with me, I'm happy with that. Just to restate the general case here: the end goal is to remove the file size cap, that is the specific need. I'd like to leave it configurable in case some user has strange edge cases I cannot predict and wants to bound it for their own reasons. But it is not essential to keep this a config. We can simplify the PR if we just want to remove the cap. The actual protections in place are the rate limits. If having a sentinel value here at There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was about to add a comment about the config type to be changed to uint64, then I saw this comment so I'll try to keep the number of open threads to a minimum. I don't want to hijack @nkvoll comment and will leave it to him to get back here, I would love to see the About the sentinel value/explicit enabled/disabled flag I am not sure: if we have such a flag we could leave the default limit to 100MB as today (still configurable using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for the general-case explanation @pzl, much appreciated! What I'm thinking is pretty much what @pchila is saying, but maybe with a slight variant to avoid discussing the naming, by having it be a pointer. If the pointer is
If I understand correctly, we want to default to "no maximum configured", and this being a pointer, usually defaults to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. in re: negative numbers being nonsensical values here -- do we want this to be Also what should happen if it is configured and set to 0? Feature disabled? |
||
vSpan.End() | ||
return file.Info{}, ErrFileSizeTooLarge | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.