mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 05:25:40 +09:00
207 lines
5.9 KiB
Rust
207 lines
5.9 KiB
Rust
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
use crate::cassandra::{apply_schema, config_from_env};
|
|
use crate::proc::{RunOptions, merged_env, run_command};
|
|
use anyhow::{Context, Result, bail};
|
|
use std::env;
|
|
use std::process::{Command, Output, Stdio};
|
|
use std::thread;
|
|
use std::time::{Duration, Instant};
|
|
use tokio::time::sleep;
|
|
|
|
pub const S3_BUCKETS: &[&str] = &[
|
|
"fluxer",
|
|
"fluxer-uploads",
|
|
"fluxer-downloads",
|
|
"fluxer-reports",
|
|
"fluxer-harvests",
|
|
"fluxer-static",
|
|
];
|
|
|
|
pub fn s3_endpoint() -> String {
|
|
env::var("FLUXER_S3_ENDPOINT").unwrap_or_else(|_| "http://127.0.0.1:8333".to_owned())
|
|
}
|
|
|
|
pub fn s3_env() -> Vec<(String, Option<String>)> {
|
|
vec![
|
|
(
|
|
"AWS_ACCESS_KEY_ID".to_owned(),
|
|
Some(env::var("FLUXER_S3_ACCESS_KEY_ID").unwrap_or_else(|_| "fluxer".to_owned())),
|
|
),
|
|
(
|
|
"AWS_SECRET_ACCESS_KEY".to_owned(),
|
|
Some(
|
|
env::var("FLUXER_S3_SECRET_ACCESS_KEY")
|
|
.unwrap_or_else(|_| "fluxer-secret".to_owned()),
|
|
),
|
|
),
|
|
(
|
|
"AWS_DEFAULT_REGION".to_owned(),
|
|
Some(env::var("FLUXER_S3_REGION").unwrap_or_else(|_| "us-east-1".to_owned())),
|
|
),
|
|
]
|
|
}
|
|
|
|
pub async fn wait_s3_api(timeout_secs: u64) -> Result<()> {
|
|
let endpoint = s3_endpoint();
|
|
let deadline = Instant::now() + Duration::from_secs(timeout_secs);
|
|
let mut last_output = String::new();
|
|
while Instant::now() < deadline {
|
|
let env = merged_env(Some(&s3_env()), true)?;
|
|
let output = Command::new("aws")
|
|
.args(["--endpoint-url", &endpoint, "s3api", "list-buckets"])
|
|
.env_clear()
|
|
.envs(env)
|
|
.stdout(Stdio::piped())
|
|
.stderr(Stdio::piped())
|
|
.output()
|
|
.context("failed to run aws s3api list-buckets")?;
|
|
if output.status.success() {
|
|
println!("SeaweedFS S3 API is reachable at {endpoint}");
|
|
return Ok(());
|
|
}
|
|
let mut combined = output.stdout;
|
|
combined.extend(output.stderr);
|
|
last_output = String::from_utf8_lossy(&combined).trim().to_owned();
|
|
sleep(Duration::from_secs(2)).await;
|
|
}
|
|
bail!("Timed out waiting for SeaweedFS S3 API at {endpoint}: {last_output}");
|
|
}
|
|
|
|
pub fn ensure_s3_buckets() -> Result<()> {
|
|
let endpoint = s3_endpoint();
|
|
let env = s3_env();
|
|
for bucket in S3_BUCKETS {
|
|
ensure_s3_bucket(&endpoint, &env, bucket)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn ensure_s3_bucket(endpoint: &str, env: &[(String, Option<String>)], bucket: &str) -> Result<()> {
|
|
let deadline = Instant::now() + Duration::from_secs(30);
|
|
|
|
loop {
|
|
if s3_bucket_exists(endpoint, env, bucket)? {
|
|
println!("S3 bucket exists: {bucket}");
|
|
return Ok(());
|
|
}
|
|
|
|
let output = create_s3_bucket(endpoint, env, bucket)?;
|
|
if output.status.success() {
|
|
println!("Created S3 bucket: {bucket}");
|
|
return Ok(());
|
|
}
|
|
|
|
let text = command_output_text(&output);
|
|
if !s3_bucket_already_exists_output(&text) {
|
|
let code = output.status.code().unwrap_or(-1);
|
|
bail!(
|
|
"Command failed with exit code {code}: aws --endpoint-url {endpoint} s3api create-bucket --bucket {bucket}"
|
|
);
|
|
}
|
|
|
|
if Instant::now() >= deadline {
|
|
bail!(
|
|
"Timed out waiting for S3 bucket {bucket} to become readable after create-bucket reported it already exists: {text}"
|
|
);
|
|
}
|
|
thread::sleep(Duration::from_secs(1));
|
|
}
|
|
}
|
|
|
|
fn s3_bucket_exists(
|
|
endpoint: &str,
|
|
env: &[(String, Option<String>)],
|
|
bucket: &str,
|
|
) -> Result<bool> {
|
|
let output = run_command(
|
|
&[
|
|
"aws",
|
|
"--endpoint-url",
|
|
endpoint,
|
|
"s3api",
|
|
"head-bucket",
|
|
"--bucket",
|
|
bucket,
|
|
],
|
|
RunOptions {
|
|
env: env.to_vec(),
|
|
check: false,
|
|
capture: true,
|
|
..RunOptions::default()
|
|
},
|
|
)?;
|
|
Ok(output.status.success())
|
|
}
|
|
|
|
fn create_s3_bucket(
|
|
endpoint: &str,
|
|
env: &[(String, Option<String>)],
|
|
bucket: &str,
|
|
) -> Result<Output> {
|
|
run_command(
|
|
&[
|
|
"aws",
|
|
"--endpoint-url",
|
|
endpoint,
|
|
"s3api",
|
|
"create-bucket",
|
|
"--bucket",
|
|
bucket,
|
|
],
|
|
RunOptions {
|
|
env: env.to_vec(),
|
|
check: false,
|
|
capture: true,
|
|
..RunOptions::default()
|
|
},
|
|
)
|
|
}
|
|
|
|
fn command_output_text(output: &Output) -> String {
|
|
let mut combined = output.stdout.clone();
|
|
combined.extend_from_slice(&output.stderr);
|
|
String::from_utf8_lossy(&combined).trim().to_owned()
|
|
}
|
|
|
|
fn s3_bucket_already_exists_output(output: &str) -> bool {
|
|
output.contains("BucketAlreadyExists") || output.contains("BucketAlreadyOwnedByYou")
|
|
}
|
|
|
|
pub async fn bootstrap_schema() -> Result<()> {
|
|
if cassandra_backend_enabled() {
|
|
apply_schema(Some(config_from_env()?)).await?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn cassandra_backend_enabled() -> bool {
|
|
env::var("FLUXER_DATABASE_BACKEND").as_deref() == Ok("cassandra")
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn s3_env_uses_fluxer_defaults() {
|
|
let env = s3_env();
|
|
assert!(env.iter().any(
|
|
|(key, value)| key == "AWS_DEFAULT_REGION" && value.as_deref() == Some("us-east-1")
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn bucket_already_exists_output_matches_aws_errors() {
|
|
assert!(s3_bucket_already_exists_output(
|
|
"An error occurred (BucketAlreadyExists) when calling the CreateBucket operation"
|
|
));
|
|
assert!(s3_bucket_already_exists_output(
|
|
"An error occurred (BucketAlreadyOwnedByYou) when calling the CreateBucket operation"
|
|
));
|
|
assert!(!s3_bucket_already_exists_output(
|
|
"An error occurred (AccessDenied) when calling the CreateBucket operation"
|
|
));
|
|
}
|
|
}
|