Files
rustfs-manager-operator/operator/src/controllers/rustfs_instance.rs
Nikolai Rodionov dd4151c10f
Some checks failed
ci/woodpecker/pr/publish-tagged-helm-chart Pipeline is pending
ci/woodpecker/pr/test-helm-charts Pipeline is pending
ci/woodpecker/pr/build-dev-docs-container/1 Pipeline was successful
ci/woodpecker/pr/code-checks Pipeline was successful
ci/woodpecker/pr/publish-dev-helm-chart Pipeline failed
ci/woodpecker/pr/build-dev-docs-container/2 Pipeline failed
ci/woodpecker/tag/build-tagged-version/2 Pipeline failed
ci/woodpecker/tag/build-tagged-version/1 Pipeline failed
First more or less working version
Signed-off-by: Nikolai Rodionov <iam@allanger.xyz>
2026-03-20 16:08:05 +01:00

412 lines
14 KiB
Rust

use crate::conditions::{init_conditions, is_condition_true, is_condition_unknown, set_condition};
use crate::rc::{RcAlias, admin_info, get_aliases, list_buckets, set_alias};
use api::api::v1beta1_rustfs_instance::{RustFSInstance, RustFSInstanceStatus};
use futures::StreamExt;
use k8s_openapi::api::core::v1::Secret;
use kube::api::{ListParams, PostParams};
use kube::runtime::Controller;
use kube::runtime::controller::Action;
use kube::runtime::events::Recorder;
use kube::runtime::watcher::Config;
use kube::{Api, Client, Error, ResourceExt};
use std::collections::BTreeMap;
use std::sync::Arc;
use std::time::Duration;
use thiserror::Error;
use tracing::*;
const TYPE_CONNECTED: &str = "RustFSReady";
const TYPE_SECRET_LABELED: &str = "SecretLabeled";
const FIN_SECRET_LABEL: &str = "s3.badhouseplants.net/s3-label";
const SECRET_LABEL: &str = "s3.badhouseplants.net/s3-instance";
pub(crate) const ACCESS_KEY: &str = "ACCESS_KEY";
pub(crate) const SECRET_KEY: &str = "SECRET_KEY";
#[instrument(skip(ctx, req), fields(trace_id, controller = "rustfs-instance"))]
pub(crate) async fn reconcile(
req: Arc<RustFSInstance>,
ctx: Arc<Context>,
) -> RustFSInstanceResult<Action> {
info!("Staring to reconcile");
info!("Getting the RustFSInstance resource");
let rustfs_api: Api<RustFSInstance> = Api::all(ctx.client.clone());
let mut rustfs_cr = match rustfs_api.get(req.name_any().as_str()).await {
Ok(res) => res,
Err(Error::Api(ae)) if ae.code == 404 => {
info!("Object is not found, probably removed");
return Ok(Action::await_change());
}
Err(err) => return Err(RustFSInstanceError::KubeError(err)),
};
let secret_ns = rustfs_cr.clone().spec.credentials_secret.namespace;
let secret_api: Api<Secret> = Api::namespaced(ctx.client.clone(), &secret_ns);
// If status is none, we need to initialize the object
let mut status = match rustfs_cr.clone().status {
None => {
info!("Status is not yet set, initializing the object");
return init_object(rustfs_cr, rustfs_api).await;
}
Some(status) => status,
};
// We need to know the secret before deletion, because the operator needs to unlabel it
let secret = match get_secret(secret_api.clone(), rustfs_cr.clone()).await {
Ok(secret) => secret,
Err(err) => return Err(RustFSInstanceError::KubeError(err)),
};
// Handle the deletion logic
if rustfs_cr.metadata.deletion_timestamp.is_some() {
info!("Object is marked for deletion");
if let Some(mut finalizers) = rustfs_cr.clone().metadata.finalizers {
if finalizers.contains(&FIN_SECRET_LABEL.to_string()) {
info!("Removing labels from the secret with credentials");
match unlabel_secret(ctx.clone(), rustfs_cr.clone(), secret).await {
Ok(_) => {
if let Some(index) = finalizers.iter().position(|x| x == FIN_SECRET_LABEL) {
finalizers.remove(index);
};
}
Err(err) => return Err(RustFSInstanceError::KubeError(err)),
};
}
rustfs_cr.metadata.finalizers = Some(finalizers);
};
match rustfs_api
.replace(&rustfs_cr.name_any(), &PostParams::default(), &rustfs_cr)
.await
{
Ok(_) => return Ok(Action::await_change()),
Err(err) => return Err(RustFSInstanceError::KubeError(err)),
}
}
// If secret is labeled, add a finalizer to the rustfs_cr
if is_condition_true(status.clone().conditions, TYPE_SECRET_LABELED) {
let mut current_finalizers = rustfs_cr.clone().metadata.finalizers.unwrap_or_default();
// Only if the finalizer is not added yet
if !current_finalizers.contains(&FIN_SECRET_LABEL.to_string()) {
info!("Adding a finalizer");
current_finalizers.push(FIN_SECRET_LABEL.to_string());
rustfs_cr.metadata.finalizers = Some(current_finalizers);
match rustfs_api
.replace(&rustfs_cr.name_any(), &PostParams::default(), &rustfs_cr)
.await
{
Ok(_) => return Ok(Action::await_change()),
Err(err) => return Err(RustFSInstanceError::KubeError(err)),
}
}
}
// Label the secret, if not yet labeled
if is_condition_unknown(status.clone().conditions, TYPE_SECRET_LABELED) {
if is_secret_labeled(secret.clone()) {
if is_secret_labeled_by_another_obj(rustfs_cr.clone(), secret.clone()) {
return Err(RustFSInstanceError::SecretIsAlreadyLabeled);
}
if is_secret_labeled_by_obj(rustfs_cr.clone(), secret.clone()) {
info!("Secret is already labeled");
status.conditions = set_condition(
status.clone().conditions,
req.metadata.clone(),
TYPE_SECRET_LABELED,
"True".to_string(),
"Reconciled".to_string(),
"Secret is already labeled".to_string(),
);
}
} else {
info!("Labeling the secret");
if let Err(err) = label_secret(ctx.clone(), rustfs_cr.clone(), secret).await {
return Err(RustFSInstanceError::KubeError(err));
};
status.conditions = set_condition(
status.clone().conditions,
req.metadata.clone(),
TYPE_SECRET_LABELED,
"True".to_string(),
"Reconciled".to_string(),
"Secret is labeled".to_string(),
);
};
rustfs_cr.status = Some(status);
match rustfs_api
.replace_status(&rustfs_cr.name_any(), &PostParams::default(), &rustfs_cr)
.await
{
Ok(_) => return Ok(Action::await_change()),
Err(err) => return Err(RustFSInstanceError::KubeError(err)),
}
};
info!("Checking if the secret is labeled by another object");
if !is_secret_labeled_by_obj(rustfs_cr.clone(), secret.clone()) {
status.conditions = set_condition(
status.conditions,
rustfs_cr.clone().metadata,
TYPE_SECRET_LABELED,
"Unknown".to_string(),
"RustFSInstanceReconciliation".to_string(),
"Secret is not labeled".to_string(),
);
rustfs_cr.status = Some(status);
match rustfs_api
.replace_status(
&rustfs_cr.clone().name_any(),
&PostParams::default(),
&rustfs_cr,
)
.await
{
Ok(_) => return Ok(Action::await_change()),
Err(err) => return Err(RustFSInstanceError::KubeError(err)),
}
};
info!("Getting data from the secret");
let (access_key, secret_key) = match get_data_from_secret(secret) {
Ok((ak, sk)) => (ak, sk),
Err(err) => return Err(RustFSInstanceError::InvalidSecret(err)),
};
let current_aliases = match get_aliases() {
Ok(aliases) => aliases,
Err(err) => return Err(RustFSInstanceError::RcCliError(err)),
};
// Check if alias already exists
if current_aliases.aliases.is_none_or(|a| {
!a.contains(&RcAlias {
name: rustfs_cr.name_any().to_string(),
})
}) && let Err(err) = set_alias(
rustfs_cr.name_any(),
rustfs_cr.clone().spec.endpoint,
access_key.clone(),
secret_key.clone(),
) {
return Err(RustFSInstanceError::RcCliError(err));
}
let admin_info = match admin_info(rustfs_cr.name_any().to_string()) {
Ok(ai) => ai,
Err(err) => return Err(RustFSInstanceError::RcCliError(err)),
};
let bucket_list = match list_buckets(rustfs_cr.name_any().to_string()) {
Ok(bl) => bl,
Err(err) => return Err(RustFSInstanceError::RcCliError(err)),
};
status.ready = true;
status.total_buckets = admin_info.buckets;
status.region = admin_info.region;
status.buckets = Some(
bucket_list
.items
.unwrap()
.iter()
.map(|b| b.clone().key.unwrap())
.collect(),
);
rustfs_cr.status = Some(status);
match rustfs_api
.replace_status(&rustfs_cr.name_any(), &PostParams::default(), &rustfs_cr)
.await
{
Ok(_) => return Ok(Action::requeue(Duration::from_secs(120))),
Err(err) => return Err(RustFSInstanceError::KubeError(err)),
};
}
// Bootstrap the object by adding a default status to it
async fn init_object(
mut obj: RustFSInstance,
api: Api<RustFSInstance>,
) -> Result<Action, RustFSInstanceError> {
let conditions = init_conditions(vec![
TYPE_CONNECTED.to_string(),
TYPE_SECRET_LABELED.to_string(),
]);
obj.status = Some(RustFSInstanceStatus {
conditions,
..Default::default()
});
match api
.replace_status(obj.clone().name_any().as_str(), &Default::default(), &obj)
.await
{
Ok(_) => Ok(Action::await_change()),
Err(err) => Err(RustFSInstanceError::KubeError(err)),
}
}
// Get the secret with credentials
pub(crate) async fn get_secret(
api: Api<Secret>,
obj: RustFSInstance,
) -> Result<Secret, kube::Error> {
api.get(&obj.spec.credentials_secret.name).await
}
async fn unlabel_secret(
ctx: Arc<Context>,
obj: RustFSInstance,
mut secret: Secret,
) -> Result<(), kube::Error> {
let secret_ns = obj.clone().spec.credentials_secret.namespace;
let api: Api<Secret> = Api::namespaced(ctx.client.clone(), &secret_ns);
if let Some(mut labels) = secret.clone().metadata.labels {
labels.remove(SECRET_LABEL);
secret.metadata.labels = Some(labels);
api.replace(&secret.name_any(), &PostParams::default(), &secret)
.await?;
}
Ok(())
}
async fn label_secret(
ctx: Arc<Context>,
obj: RustFSInstance,
mut secret: Secret,
) -> Result<Secret, kube::Error> {
let secret_ns = obj.clone().spec.credentials_secret.namespace;
let api: Api<Secret> = Api::namespaced(ctx.client.clone(), &secret_ns);
secret
.clone()
.metadata
.labels
.get_or_insert_with(BTreeMap::new)
.insert(SECRET_LABEL.to_string(), obj.name_any());
let mut labels = match &secret.clone().metadata.labels {
Some(labels) => labels.clone(),
None => {
let map: BTreeMap<String, String> = BTreeMap::new();
map
}
};
labels.insert(SECRET_LABEL.to_string(), obj.name_any());
secret.metadata.labels = Some(labels);
api.replace(&secret.name_any(), &PostParams::default(), &secret)
.await?;
let secret = match api.get(&obj.spec.credentials_secret.name).await {
Ok(secret) => secret,
Err(err) => return Err(err),
};
Ok(secret)
}
// Checks whether a secret ia already labeled by the operator
fn is_secret_labeled(secret: Secret) -> bool {
match secret.metadata.labels {
Some(labels) => labels.get_key_value(SECRET_LABEL).is_some(),
None => false,
}
}
// Checks whether a secret is already labeled by another object
fn is_secret_labeled_by_another_obj(obj: RustFSInstance, secret: Secret) -> bool {
match secret.metadata.labels {
Some(labels) => labels
.get(SECRET_LABEL)
.is_some_and(|label| label != &obj.name_any()),
None => false,
}
}
// Checks whether a secret is already labeled by this object
fn is_secret_labeled_by_obj(obj: RustFSInstance, secret: Secret) -> bool {
match secret.metadata.labels {
Some(labels) => labels
.get(SECRET_LABEL)
.is_some_and(|label| label == &obj.name_any()),
None => false,
}
}
// Returns (access_key, secret_key)
pub(crate) fn get_data_from_secret(secret: Secret) -> Result<(String, String), anyhow::Error> {
let data = match secret.data {
Some(data) => data,
None => return Err(anyhow::Error::msg("empty data")),
};
let access_key = match data.get(ACCESS_KEY) {
Some(access_key) => String::from_utf8(access_key.0.clone()).unwrap(),
None => return Err(anyhow::Error::msg("empty access key")),
};
let secret_key = match data.get(SECRET_KEY) {
Some(secret_key) => String::from_utf8(secret_key.0.clone()).unwrap(),
None => return Err(anyhow::Error::msg("empty secret key")),
};
Ok((access_key, secret_key))
}
pub(crate) fn error_policy(
_rustfs_cr: Arc<RustFSInstance>,
err: &RustFSInstanceError,
_ctx: Arc<Context>,
) -> Action {
error!(trace.error = %err, "Error occurred during the reconciliation");
Action::requeue(Duration::from_secs(5 * 60))
}
#[instrument(skip(client), fields(trace_id))]
pub async fn run(client: Client) {
let s3instances = Api::<RustFSInstance>::all(client.clone());
if let Err(err) = s3instances.list(&ListParams::default().limit(1)).await {
error!("{}", err);
std::process::exit(1);
}
let recorder = Recorder::new(client.clone(), "s3instance-controller".into());
let context = Context { client, recorder };
Controller::new(s3instances, Config::default().any_semantic())
.shutdown_on_signal()
.run(reconcile, error_policy, Arc::new(context))
.filter_map(|x| async move { std::result::Result::ok(x) })
.for_each(|_| futures::future::ready(()))
.await;
}
// Context for our reconciler
#[derive(Clone)]
pub(crate) struct Context {
/// Kubernetes client
pub client: Client,
/// Event recorder
pub recorder: Recorder,
}
#[derive(Error, Debug)]
pub enum RustFSInstanceError {
#[error("Kube Error: {0}")]
KubeError(#[source] kube::Error),
#[error("SecretIsAlreadyLabeled")]
SecretIsAlreadyLabeled,
#[error("Invalid Secret: {0}")]
InvalidSecret(#[source] anyhow::Error),
#[error("Error while executing rc cli: {0}")]
RcCliError(#[source] anyhow::Error),
}
pub type RustFSInstanceResult<T, E = RustFSInstanceError> = std::result::Result<T, E>;