/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 | | } |