/build/source/nativelink-service/src/push_server.rs
Line | Count | Source |
1 | | // Copyright 2025 Trace Machina, Inc. All rights reserved. |
2 | | // |
3 | | // Licensed under the Functional Source License, Version 1.1, Apache 2.0 Future License (the "License"); |
4 | | // you may not use this file except in compliance with the License. |
5 | | // You may obtain a copy of the License at |
6 | | // |
7 | | // See LICENSE file for details |
8 | | // |
9 | | // Unless required by applicable law or agreed to in writing, software |
10 | | // distributed under the License is distributed on an "AS IS" BASIS, |
11 | | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
12 | | // See the License for the specific language governing permissions and |
13 | | // limitations under the License. |
14 | | |
15 | | use core::convert::Into; |
16 | | use std::collections::HashMap; |
17 | | |
18 | | use nativelink_config::cas_server::{PushConfig, WithInstanceName}; |
19 | | use nativelink_error::{Code, Error, ResultExt, make_err, make_input_err}; |
20 | | use nativelink_proto::build::bazel::remote::asset::v1::push_server::{Push, PushServer as Server}; |
21 | | use nativelink_proto::build::bazel::remote::asset::v1::{ |
22 | | PushBlobRequest, PushBlobResponse, PushDirectoryRequest, PushDirectoryResponse, |
23 | | }; |
24 | | use nativelink_store::store_manager::StoreManager; |
25 | | use nativelink_util::digest_hasher::make_ctx_for_hash_func; |
26 | | use nativelink_util::store_trait::{Store, StoreLike}; |
27 | | use opentelemetry::context::FutureExt; |
28 | | use tonic::{Request, Response, Status}; |
29 | | use tracing::{Instrument, Level, error_span, info, instrument}; |
30 | | |
31 | | use crate::remote_asset_proto::RemoteAssetArtifact; |
32 | | |
33 | | #[derive(Debug, Clone)] |
34 | | pub struct PushStoreInfo { |
35 | | store: Store, |
36 | | read_only: bool, |
37 | | } |
38 | | |
39 | | #[derive(Debug, Clone)] |
40 | | pub struct PushServer { |
41 | | stores: HashMap<String, PushStoreInfo>, |
42 | | } |
43 | | |
44 | | impl PushServer { |
45 | 1 | pub fn new( |
46 | 1 | configs: &[WithInstanceName<PushConfig>], |
47 | 1 | store_manager: &StoreManager, |
48 | 1 | ) -> Result<Self, Error> { |
49 | 1 | let mut stores = HashMap::with_capacity(configs.len()); |
50 | 1 | for config in configs { |
51 | 1 | let store = store_manager.get_store(&config.push_store).ok_or_else(|| {0 |
52 | 0 | make_input_err!("'push_store': '{}' does not exist", config.push_store) |
53 | 0 | })?; |
54 | 1 | stores.insert( |
55 | 1 | config.instance_name.clone(), |
56 | 1 | PushStoreInfo { |
57 | 1 | store, |
58 | 1 | read_only: config.read_only, |
59 | 1 | }, |
60 | | ); |
61 | | } |
62 | 1 | Ok(Self { |
63 | 1 | stores: stores.clone(), |
64 | 1 | }) |
65 | 1 | } |
66 | | |
67 | 0 | pub fn into_service(self) -> Server<Self> { |
68 | 0 | Server::new(self) |
69 | 0 | } |
70 | | |
71 | 1 | async fn inner_push_blob( |
72 | 1 | &self, |
73 | 1 | request: PushBlobRequest, |
74 | 1 | ) -> Result<Response<PushBlobResponse>, Error> { |
75 | 1 | let instance_name = &request.instance_name; |
76 | 1 | let store_info = self.stores.get(instance_name).ok_or_else(|| {0 |
77 | 0 | make_input_err!("'instance_name' not configured for '{instance_name}'") |
78 | 0 | })?; |
79 | | |
80 | 1 | if store_info.read_only { |
81 | 0 | return Err(make_err!( |
82 | 0 | Code::PermissionDenied, |
83 | 0 | "The store '{instance_name}' is read only on this endpoint", |
84 | 0 | )); |
85 | 1 | } |
86 | | |
87 | 1 | let blob_digest = request.blob_digest.ok_or_else(|| {0 |
88 | 0 | Error::new( |
89 | 0 | Code::InvalidArgument, |
90 | 0 | "Missing blob digest in push".to_owned(), |
91 | | ) |
92 | 0 | })?; |
93 | 1 | if request.uris.is_empty() { |
94 | 0 | return Err(Error::new( |
95 | 0 | Code::InvalidArgument, |
96 | 0 | "No uris in push request".to_owned(), |
97 | 0 | )); |
98 | 1 | } |
99 | 1 | for uri in request.uris { |
100 | 1 | let asset = RemoteAssetArtifact::new( |
101 | 1 | uri.clone(), |
102 | 1 | request.qualifiers.clone(), |
103 | 1 | blob_digest.clone(), |
104 | 1 | request.expire_at, |
105 | 1 | request.digest_function, |
106 | | ); |
107 | 1 | let asset_digest = asset.digest(); |
108 | 1 | store_info |
109 | 1 | .store |
110 | 1 | .update_oneshot(asset_digest, asset.as_bytes()) |
111 | 1 | .await |
112 | 1 | .err_tip(|| "Failed to update in push server")?0 ; |
113 | 1 | info!( |
114 | | uri = uri, |
115 | 1 | digest = format!("{}", asset_digest), |
116 | | "Pushed asset metadata with hash" |
117 | | ); |
118 | | } |
119 | | |
120 | 1 | Ok(Response::new(PushBlobResponse {})) |
121 | 1 | } |
122 | | } |
123 | | |
124 | | #[tonic::async_trait] |
125 | | impl Push for PushServer { |
126 | | #[allow(clippy::blocks_in_conditions)] |
127 | | #[instrument( |
128 | | err(level = Level::WARN), |
129 | | ret(level = Level::INFO), |
130 | | skip_all, |
131 | | fields(request = ?grpc_request.get_ref()) |
132 | | )] |
133 | | async fn push_blob( |
134 | | &self, |
135 | | grpc_request: Request<PushBlobRequest>, |
136 | | ) -> Result<Response<PushBlobResponse>, Status> { |
137 | | let request = grpc_request.into_inner(); |
138 | | let digest_function = request.digest_function; |
139 | | self.inner_push_blob(request) |
140 | | .instrument(error_span!("push_push_blob")) |
141 | | .with_context( |
142 | | make_ctx_for_hash_func(digest_function).err_tip(|| "In PushServer::push_blob")?, |
143 | | ) |
144 | | .await |
145 | | .err_tip(|| "Failed on push_blob() command") |
146 | | .map_err(Into::into) |
147 | | } |
148 | | |
149 | | #[allow(clippy::blocks_in_conditions)] |
150 | | #[instrument( |
151 | | err(level = Level::WARN), |
152 | | ret(level = Level::INFO), |
153 | | skip_all, |
154 | | fields(request = ?_grpc_request.get_ref()) |
155 | | )] |
156 | | async fn push_directory( |
157 | | &self, |
158 | | _grpc_request: Request<PushDirectoryRequest>, |
159 | | ) -> Result<Response<PushDirectoryResponse>, Status> { |
160 | | Err(Status::unimplemented("PushDirectory not implemented")) |
161 | | } |
162 | | } |