Coverage Report

Created: 2026-11-13 23:29

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}