Coverage Report

Created: 2026-07-21 15:28

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/build/source/nativelink-store/src/oci_store.rs
Line
Count
Source
1
// Copyright 2026 The NativeLink Authors. 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::time::Duration;
16
use std::borrow::Cow;
17
use std::sync::Arc;
18
19
use aws_config::default_provider::credentials::DefaultCredentialsChain;
20
use aws_config::provider_config::ProviderConfig;
21
use aws_config::{AppName, BehaviorVersion};
22
use aws_sdk_s3::Client;
23
use aws_sdk_s3::config::{
24
    Credentials, Region, RequestChecksumCalculation, ResponseChecksumValidation,
25
};
26
use aws_smithy_runtime_api::client::http::HttpClient as SmithyHttpClient;
27
use nativelink_config::stores::{ExperimentalAwsSpec, ExperimentalOciSpec};
28
use nativelink_error::Error;
29
use nativelink_util::instant_wrapper::InstantWrapper;
30
31
use crate::common_s3_utils::TlsClient;
32
use crate::s3_store::S3Store;
33
34
/// OCI-shaped config adapter over [`S3Store`]. Builds an S3 SDK client pointed
35
/// at Oracle Cloud Infrastructure Object Storage via its Amazon S3
36
/// Compatibility API and hands it to `S3Store::new_with_client_and_jitter`.
37
///
38
/// The compatibility API requires path-style addressing: the namespace lives in
39
/// the host and the bucket is the first path segment
40
/// (`https://{namespace}.compat.objectstorage.{region}.oci.customer-oci.com/{bucket}/{object}`),
41
/// so `force_path_style(true)` is set on the client. Requests are signed with
42
/// AWS `SigV4` using the OCI region as the signing region; credentials come from
43
/// a Customer Secret Key when supplied, otherwise the default AWS credential
44
/// chain.
45
#[derive(Debug, Clone, Copy)]
46
pub struct OciStore;
47
48
impl OciStore {
49
    #[allow(clippy::new_ret_no_self)] // Because usually everyone returns themselves
50
0
    pub async fn new<I, NowFn>(
51
0
        spec: &ExperimentalOciSpec,
52
0
        now_fn: NowFn,
53
0
    ) -> Result<Arc<S3Store<NowFn>>, Error>
54
0
    where
55
0
        I: InstantWrapper,
56
0
        NowFn: Fn() -> I + Send + Sync + Unpin + 'static,
57
0
    {
58
0
        Self::new_with_http_client(spec, TlsClient::new(&spec.common.clone()), now_fn).await
59
0
    }
60
61
    /// Builds the store with a caller-supplied HTTP client. Production uses
62
    /// [`Self::new`] (which injects a [`TlsClient`]); tests inject a mock (e.g.
63
    /// `StaticReplayClient`) to exercise the OCI-specific wire behavior —
64
    /// path-style URLs and the absence of `aws-chunked` — without a live bucket.
65
    #[allow(clippy::new_ret_no_self)]
66
6
    pub async fn new_with_http_client<I, NowFn, C>(
67
6
        spec: &ExperimentalOciSpec,
68
6
        http_client: C,
69
6
        now_fn: NowFn,
70
6
    ) -> Result<Arc<S3Store<NowFn>>, Error>
71
6
    where
72
6
        I: InstantWrapper,
73
6
        NowFn: Fn() -> I + Send + Sync + Unpin + 'static,
74
6
        C: SmithyHttpClient + Clone + 'static,
75
6
    {
76
6
        let aws_spec = Self::build_aws_spec(spec);
77
6
        let jitter_fn = spec.common.retry.make_jitter_fn();
78
79
6
        let endpoint = Self::derive_endpoint(spec);
80
6
        let region = Region::new(Cow::Owned(spec.region.clone()));
81
82
6
        let mut config_builder = aws_sdk_s3::Config::builder()
83
6
            .behavior_version(BehaviorVersion::latest())
84
6
            .app_name(AppName::new("nativelink").expect("valid app name"))
85
6
            .timeout_config(
86
6
                aws_config::timeout::TimeoutConfig::builder()
87
6
                    .connect_timeout(Duration::from_secs(15))
88
6
                    .build(),
89
            )
90
6
            .region(region.clone())
91
6
            .endpoint_url(&endpoint)
92
            // OCI's compatibility endpoint embeds the bucket in the path, not as
93
            // a subdomain, so virtual-hosted addressing must be disabled.
94
6
            .force_path_style(true)
95
            // OCI does not support the AWS SDK's default flexible-checksum
96
            // behavior.
97
6
            .request_checksum_calculation(RequestChecksumCalculation::WhenRequired)
98
6
            .response_checksum_validation(ResponseChecksumValidation::WhenRequired)
99
6
            .http_client(http_client.clone());
100
101
6
        config_builder = if let Some(key_id) = &spec.access_key_id
102
6
            && let Some(secret) = &spec.secret_access_key
103
        {
104
6
            config_builder.credentials_provider(Credentials::new(
105
6
                key_id,
106
6
                secret,
107
6
                None,
108
6
                None,
109
                "oci-customer-secret-key",
110
            ))
111
        } else {
112
0
            let default_chain = DefaultCredentialsChain::builder()
113
0
                .configure(
114
0
                    ProviderConfig::without_region()
115
0
                        .with_region(Some(region))
116
0
                        .with_http_client(http_client),
117
0
                )
118
0
                .build()
119
0
                .await;
120
0
            config_builder.credentials_provider(default_chain)
121
        };
122
123
6
        let s3_client = Client::from_conf(config_builder.build());
124
125
6
        S3Store::new_with_client_and_jitter(&aws_spec, s3_client, jitter_fn, now_fn)
126
6
    }
127
128
    /// Derives the OCI Object Storage path-style S3-compatibility endpoint from
129
    /// the namespace and region.
130
7
    pub fn derive_endpoint(spec: &ExperimentalOciSpec) -> String {
131
7
        format!(
132
            "https://{}.compat.objectstorage.{}.oci.customer-oci.com",
133
            spec.namespace, spec.region
134
        )
135
7
    }
136
137
7
    pub fn build_aws_spec(spec: &ExperimentalOciSpec) -> ExperimentalAwsSpec {
138
7
        ExperimentalAwsSpec {
139
7
            region: spec.region.clone(),
140
7
            bucket: spec.bucket.clone(),
141
7
            common: spec.common.clone(),
142
7
        }
143
7
    }
144
}