-
Notifications
You must be signed in to change notification settings - Fork 45
Serving files from Nexus -> Sled Agent for the update system #457
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
Changes from all commits
fc70559
80250e1
3ac2ddb
9357831
67fa74c
d7728a1
7900929
a3f8121
fad83b5
4523cbd
e82885c
dc5897a
e2af470
e22c736
9116b07
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,7 @@ | |
// License, v. 2.0. If a copy of the MPL was not distributed with this | ||
// file, You can obtain one at https://mozilla.org/MPL/2.0/. | ||
|
||
use crate::nexus::BASE_ARTIFACT_DIR; | ||
/** | ||
* Handler functions (entrypoints) for HTTP APIs internal to the control plane | ||
*/ | ||
|
@@ -19,13 +20,16 @@ use dropshot::HttpResponseUpdatedNoContent; | |
use dropshot::Path; | ||
use dropshot::RequestContext; | ||
use dropshot::TypedBody; | ||
use http::{Response, StatusCode}; | ||
use hyper::Body; | ||
use omicron_common::api::internal::nexus::DiskRuntimeState; | ||
use omicron_common::api::internal::nexus::InstanceRuntimeState; | ||
use omicron_common::api::internal::nexus::ProducerEndpoint; | ||
use oximeter::types::ProducerResults; | ||
use oximeter_producer::{collect, ProducerIdPathParams}; | ||
use schemars::JsonSchema; | ||
use serde::Deserialize; | ||
use std::path::PathBuf; | ||
use std::sync::Arc; | ||
use uuid::Uuid; | ||
|
||
|
@@ -44,6 +48,7 @@ pub fn internal_api() -> NexusApiDescription { | |
api.register(cpapi_producers_post)?; | ||
api.register(cpapi_collectors_post)?; | ||
api.register(cpapi_metrics_collect)?; | ||
api.register(cpapi_artifact_download)?; | ||
Ok(()) | ||
} | ||
|
||
|
@@ -280,3 +285,76 @@ async fn cpapi_metrics_collect( | |
.instrument_dropshot_handler(&request_context, handler) | ||
.await | ||
} | ||
|
||
#[derive(Deserialize, JsonSchema)] | ||
struct AllPath { | ||
path: String, | ||
} | ||
|
||
/// Endpoint used by Sled Agents to download cached artifacts. | ||
#[endpoint { | ||
method = GET, | ||
path = "/artifacts/{path}", | ||
}] | ||
async fn cpapi_artifact_download( | ||
request_context: Arc<RequestContext<Arc<ServerContext>>>, | ||
path: Path<AllPath>, | ||
) -> Result<Response<Body>, HttpError> { | ||
let context = request_context.context(); | ||
let nexus = &context.nexus; | ||
let mut entry = PathBuf::from(BASE_ARTIFACT_DIR); | ||
|
||
// TODO: Most of the below code is ready to accept a multi-component path, | ||
// such as in: | ||
// https://github.com/oxidecomputer/dropshot/blob/78be3deda556a9339ea09f3a9961fd91389f8757/dropshot/examples/file_server.rs#L86-L89 | ||
// | ||
// However, openapi does *not* like that currently, so we limit the endpoint | ||
// to only accepting single-component paths. | ||
let path = vec![path.into_inner().path]; | ||
|
||
for component in &path { | ||
// Dropshot should not provide "." and ".." components. | ||
assert_ne!(component, "."); | ||
assert_ne!(component, ".."); | ||
iliana marked this conversation as resolved.
Show resolved
Hide resolved
|
||
entry.push(component); | ||
|
||
if entry.exists() { | ||
// We explicitly prohibit consumers from following symlinks to prevent | ||
// showing data outside of the intended directory. | ||
let m = entry.symlink_metadata().map_err(|e| { | ||
HttpError::for_bad_request( | ||
None, | ||
format!("Failed to query file metadata: {}", e), | ||
) | ||
})?; | ||
if m.file_type().is_symlink() { | ||
return Err(HttpError::for_bad_request( | ||
None, | ||
"Cannot traverse symlinks".to_string(), | ||
)); | ||
} | ||
} | ||
} | ||
|
||
// Note - at this point, "entry" may or may not actually exist. | ||
// We try to avoid creating intermediate artifacts until we know there | ||
// is something "real" to download, as this would let malformed paths | ||
// create defunct intermediate directories. | ||
if entry.is_dir() { | ||
return Err(HttpError::for_bad_request( | ||
None, | ||
"Directory download not supported".to_string(), | ||
)); | ||
} | ||
let body = nexus.download_artifact(&entry).await?; | ||
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. Isn't there a TOCTOU race here, if the path has changed since we checked it? I think the traditional approach is to use open(2) and then fstat(2). I haven't dug into it but it looks like you can do this in Rust with 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 may need an update: https://github.com/oxidecomputer/dropshot/blob/main/dropshot/examples/file_server.rs#L132 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. So I'm looking at the file server - I think that whole example may need a re-work, in addition to this "final entry" check. We also check that the intermediate paths are not symlinks, and I believe this suffers from the same TOCTTOU issue - they could be replaced by symlinks, since we're re-checking the whole path again. In C, I would normally use |
||
|
||
// Derive the MIME type from the file name | ||
let content_type = mime_guess::from_path(&entry) | ||
.first() | ||
.map_or_else(|| "text/plain".to_string(), |m| m.to_string()); | ||
|
||
Ok(Response::builder() | ||
.status(StatusCode::OK) | ||
.header(http::header::CONTENT_TYPE, content_type) | ||
.body(body.into())?) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
// This Source Code Form is subject to the terms of the Mozilla Public | ||
// License, v. 2.0. If a copy of the MPL was not distributed with this | ||
// file, You can obtain one at https://mozilla.org/MPL/2.0/. | ||
|
||
use http::method::Method; | ||
use http::StatusCode; | ||
use nexus_test_utils::test_setup; | ||
|
||
// Tests the "normal" case of downloading an artifact. | ||
// | ||
// This will typically be invoked by the Sled Agent, after instructed | ||
// to access an artifact. | ||
#[tokio::test] | ||
async fn test_download_known_artifact_returns_ok() { | ||
let cptestctx = test_setup("test_download_known_artifact_returns_ok").await; | ||
let client = &cptestctx.internal_client; | ||
|
||
// TODO: Can we replace this with a "real" small file that must be | ||
// downloaded, instead of synthetically created? | ||
let filename = "testfile"; | ||
let artifact_get_url = format!("/artifacts/{}", filename); | ||
|
||
let response = client | ||
.make_request_no_body(Method::GET, &artifact_get_url, StatusCode::OK) | ||
.await | ||
.unwrap(); | ||
|
||
assert_eq!( | ||
hyper::body::to_bytes(response.into_body()).await.unwrap(), | ||
"testfile contents" | ||
); | ||
cptestctx.teardown().await; | ||
} | ||
|
||
// Tests that missing artifacts return "NOT_FOUND". | ||
#[tokio::test] | ||
async fn test_download_bad_artifact_not_found() { | ||
let cptestctx = test_setup("test_download_bad_artifact_not_found").await; | ||
let client = &cptestctx.internal_client; | ||
|
||
let filename = "not_a_real_artifact"; | ||
let artifact_get_url = format!("/artifacts/{}", filename); | ||
|
||
client | ||
.make_request_error( | ||
Method::GET, | ||
&artifact_get_url, | ||
StatusCode::NOT_FOUND, | ||
) | ||
.await; | ||
|
||
cptestctx.teardown().await; | ||
} | ||
|
||
// Tests that ".." paths are disallowed by dropshot. | ||
#[tokio::test] | ||
async fn test_download_with_dots_fails() { | ||
let cptestctx = test_setup("test_download_with_dots_fails").await; | ||
let client = &cptestctx.internal_client; | ||
|
||
let filename = "hey/can/you/look/../../../../up/the/directory/tree"; | ||
let artifact_get_url = format!("/artifacts/{}", filename); | ||
|
||
client | ||
.make_request_error( | ||
Method::GET, | ||
&artifact_get_url, | ||
StatusCode::BAD_REQUEST, | ||
) | ||
.await; | ||
|
||
cptestctx.teardown().await; | ||
} |
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.
It looks like we're adding / have added a bunch of stuff here that's only appropriate to the internal API. I'm guessing that's because of the way we try to construct
external::Error
s from diesel errors using the helper function that takes a resource type. Maybe we should have a different helper that uses a different enum for internal stuff? Doesn't have to be done here.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.
Agreed. Filed #532 to track.