Skip to content

File tree

4 files changed

+222
-0
lines changed

4 files changed

+222
-0
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
44

55
## [Unreleased]
66

7+
### Added
8+
9+
- New commons::s3 module with common S3 connection structs ([#377])
10+
11+
[#377]: https://github.com/stackabletech/operator-rs/issues/377
12+
713
## [0.17.0] - 2022-04-14
814

915
### Changed

src/commons/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@ pub mod authentication;
44
pub mod ldap;
55
pub mod opa;
66
pub mod resources;
7+
pub mod s3;
78
pub mod secret_class;
89
pub mod tls;

src/commons/s3.rs

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
//! Implementation of the bucket definition as described in
2+
//! the <https://github.com/stackabletech/documentation/pull/177>
3+
//!
4+
//! Operator CRDs are expected to use the [S3BucketDef] as an entry point to this module
5+
//! and obtain an [InlinedS3BucketSpec] by calling [`S3BucketDef::resolve`].
6+
//!
7+
use crate::commons::tls::Tls;
8+
use crate::error;
9+
use crate::{client::Client, error::OperatorResult};
10+
use kube::CustomResource;
11+
use schemars::JsonSchema;
12+
use serde::{Deserialize, Serialize};
13+
14+
/// S3 bucket specification containing only the bucket name and an inlined or
15+
/// referenced connection specification.
16+
#[derive(Clone, CustomResource, Debug, Default, Deserialize, JsonSchema, PartialEq, Serialize)]
17+
#[kube(
18+
group = "s3.stackable.tech",
19+
version = "v1alpha1",
20+
kind = "S3Bucket",
21+
plural = "s3buckets",
22+
crates(
23+
kube_core = "kube::core",
24+
k8s_openapi = "k8s_openapi",
25+
schemars = "schemars"
26+
),
27+
namespaced
28+
)]
29+
#[serde(rename_all = "camelCase")]
30+
pub struct S3BucketSpec {
31+
#[serde(default, skip_serializing_if = "Option::is_none")]
32+
pub bucket_name: Option<String>,
33+
#[serde(default, skip_serializing_if = "Option::is_none")]
34+
pub connection: Option<ConnectionDef>,
35+
}
36+
37+
impl S3BucketSpec {
38+
/// Convenience function to retrieve the spec of a S3 bucket resource from the K8S API service.
39+
pub async fn get(
40+
resource_name: &str,
41+
client: &Client,
42+
namespace: Option<&str>,
43+
) -> OperatorResult<S3BucketSpec> {
44+
client
45+
.get::<S3Bucket>(resource_name, namespace)
46+
.await
47+
.map(|crd| crd.spec)
48+
.map_err(|_source| error::Error::MissingS3Bucket {
49+
name: resource_name.to_string(),
50+
})
51+
}
52+
53+
/// Map &self to an [InlinedS3BucketSpec] by obtaining connection spec from the K8S API service if necessary
54+
pub async fn inlined(
55+
&self,
56+
client: &Client,
57+
namespace: Option<&str>,
58+
) -> OperatorResult<InlinedS3BucketSpec> {
59+
match self.connection.as_ref() {
60+
Some(ConnectionDef::Reference(res_name)) => Ok(InlinedS3BucketSpec {
61+
connection: Some(S3ConnectionSpec::get(res_name, client, namespace).await?),
62+
bucket_name: self.bucket_name.clone(),
63+
}),
64+
Some(ConnectionDef::Inline(conn_spec)) => Ok(InlinedS3BucketSpec {
65+
bucket_name: self.bucket_name.clone(),
66+
connection: Some(conn_spec.clone()),
67+
}),
68+
None => Ok(InlinedS3BucketSpec {
69+
bucket_name: self.bucket_name.clone(),
70+
connection: None,
71+
}),
72+
}
73+
}
74+
}
75+
76+
/// Convenience struct with the connection spec inlined.
77+
pub struct InlinedS3BucketSpec {
78+
pub bucket_name: Option<String>,
79+
pub connection: Option<S3ConnectionSpec>,
80+
}
81+
82+
impl InlinedS3BucketSpec {
83+
/// Build the endpoint URL from [S3ConnectionSpec::host] and [S3ConnectionSpec::port].
84+
pub fn endpoint(&self) -> Option<String> {
85+
match self.connection.as_ref() {
86+
Some(conn_spec) => conn_spec.host.as_ref().map(|h| match conn_spec.port {
87+
Some(p) => format!("s3a://{h}:{p}"),
88+
_ => format!("s3a://{h}"),
89+
}),
90+
_ => None,
91+
}
92+
}
93+
94+
/// Shortcut to [S3ConnectionSpec::secret_class]
95+
pub fn secret_class(&self) -> Option<String> {
96+
match self.connection.as_ref() {
97+
Some(conn_spec) => conn_spec.secret_class.clone(),
98+
_ => None,
99+
}
100+
}
101+
}
102+
103+
/// Operators are expected to define fields for this type in order to work with S3 buckets.
104+
#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)]
105+
#[serde(rename_all = "camelCase")]
106+
pub enum S3BucketDef {
107+
Inline(S3BucketSpec),
108+
Reference(String),
109+
}
110+
111+
impl S3BucketDef {
112+
/// Returns an [InlinedS3BucketSpec].
113+
pub async fn resolve(
114+
&self,
115+
client: &Client,
116+
namespace: Option<&str>,
117+
) -> OperatorResult<InlinedS3BucketSpec> {
118+
match self {
119+
S3BucketDef::Inline(s3_bucket) => s3_bucket.inlined(client, namespace).await,
120+
S3BucketDef::Reference(_s3_bucket) => {
121+
S3BucketSpec::get(_s3_bucket.as_str(), client, namespace)
122+
.await?
123+
.inlined(client, namespace)
124+
.await
125+
}
126+
}
127+
}
128+
}
129+
130+
/// S3 connection definition used by [S3BucketSpec]
131+
#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)]
132+
#[serde(rename_all = "camelCase")]
133+
pub enum ConnectionDef {
134+
Inline(S3ConnectionSpec),
135+
Reference(String),
136+
}
137+
138+
/// S3 connection definition as CRD.
139+
#[derive(CustomResource, Clone, Debug, Default, Deserialize, JsonSchema, PartialEq, Serialize)]
140+
#[kube(
141+
group = "s3.stackable.tech",
142+
version = "v1alpha1",
143+
kind = "S3Connection",
144+
plural = "s3connections",
145+
crates(
146+
kube_core = "kube::core",
147+
k8s_openapi = "k8s_openapi",
148+
schemars = "schemars"
149+
),
150+
namespaced
151+
)]
152+
#[serde(rename_all = "camelCase")]
153+
pub struct S3ConnectionSpec {
154+
#[serde(default, skip_serializing_if = "Option::is_none")]
155+
pub host: Option<String>,
156+
#[serde(default, skip_serializing_if = "Option::is_none")]
157+
pub port: Option<u16>,
158+
#[serde(default, skip_serializing_if = "Option::is_none")]
159+
pub secret_class: Option<String>,
160+
#[serde(default, skip_serializing_if = "Option::is_none")]
161+
pub tls: Option<Tls>,
162+
}
163+
impl S3ConnectionSpec {
164+
/// Convenience function to retrieve the spec of a S3 connection resource from the K8S API service.
165+
pub async fn get(
166+
resource_name: &str,
167+
client: &Client,
168+
namespace: Option<&str>,
169+
) -> OperatorResult<S3ConnectionSpec> {
170+
client
171+
.get::<S3Connection>(resource_name, namespace)
172+
.await
173+
.map(|conn| conn.spec)
174+
.map_err(|_source| error::Error::MissingS3Connection {
175+
name: resource_name.to_string(),
176+
})
177+
}
178+
}
179+
180+
#[cfg(test)]
181+
mod test {
182+
use crate::commons::s3::ConnectionDef;
183+
use crate::commons::s3::{S3BucketSpec, S3ConnectionSpec};
184+
185+
#[test]
186+
fn test_ser_inline() {
187+
let bucket = S3BucketSpec {
188+
bucket_name: Some("test-bucket-name".to_owned()),
189+
connection: Some(ConnectionDef::Inline(S3ConnectionSpec {
190+
host: Some("host".to_owned()),
191+
port: Some(8080),
192+
secret_class: None,
193+
tls: None,
194+
})),
195+
};
196+
197+
assert_eq!(
198+
serde_yaml::to_string(&bucket).unwrap(),
199+
"---
200+
bucketName: test-bucket-name
201+
connection:
202+
inline:
203+
host: host
204+
port: 8080
205+
"
206+
.to_owned()
207+
)
208+
}
209+
}

src/error.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,12 @@ pub enum Error {
6464

6565
#[error("Missing OPA connect string in configmap [{configmap_name}]")]
6666
MissingOpaConnectString { configmap_name: String },
67+
68+
#[error("Missing S3 connection [{name}]")]
69+
MissingS3Connection { name: String },
70+
71+
#[error("Missing S3 bucket [{name}]")]
72+
MissingS3Bucket { name: String },
6773
}
6874

6975
pub type OperatorResult<T> = std::result::Result<T, Error>;

0 commit comments

Comments
 (0)