rustfs-cli 0.1.30

A Rust S3 CLI client for S3-compatible object storage
Documentation
#![cfg(not(windows))]

mod admin_support;

use std::process::Command;
use std::time::Duration;

use admin_support::{rc_binary, rc_host_alias, start_admin_response_test_server};
use serde_json::Value;

const MANUAL_TRANSITION_RESPONSE: &str = r#"{
  "state":"completed",
  "mode":"enqueue_only",
  "job_id":null,
  "status_endpoint":null,
  "report":{
    "bucket":"photos",
    "prefix":"logs/",
    "tier":"COLDTIER",
    "dry_run":true,
    "lifecycle_config_found":true,
    "scanned":3,
    "eligible":2,
    "enqueued":0,
    "dry_run_eligible":2,
    "skipped_not_transition":1,
    "skipped_tier":0,
    "skipped_delete_marker":0,
    "skipped_directory":0,
    "skipped_replication":0,
    "skipped_already_transitioned":1,
    "skipped_already_in_flight":0,
    "skipped_queue_full":0,
    "skipped_queue_closed":0,
    "skipped_queue_timeout":0,
    "truncated_by_limit":false,
    "truncated_by_duration":true
  }
}"#;

const MANUAL_TRANSITION_CONTROL_RESPONSE: &str = r#"{
  "state":"completed\nspoofed",
  "mode":"enqueue_only",
  "job_id":null,
  "status_endpoint":null,
  "report":{
    "bucket":"photos",
    "prefix":"logs/\nnext",
    "tier":"COLD\tTIER",
    "dry_run":true,
    "lifecycle_config_found":true,
    "scanned":1,
    "eligible":1,
    "enqueued":0,
    "dry_run_eligible":1,
    "skipped_not_transition":0,
    "skipped_tier":0,
    "skipped_delete_marker":0,
    "skipped_directory":0,
    "skipped_replication":0,
    "skipped_already_transitioned":0,
    "skipped_already_in_flight":0,
    "skipped_queue_full":0,
    "skipped_queue_closed":0,
    "skipped_queue_timeout":0,
    "truncated_by_limit":false,
    "truncated_by_duration":false
  }
}"#;

const MANUAL_TRANSITION_LEGACY_RESPONSE: &str = r#"{
  "state":"completed",
  "mode":"enqueue_only",
  "job_id":null,
  "status_endpoint":null,
  "report":{
    "bucket":"photos",
    "prefix":"logs/",
    "tier":"COLDTIER",
    "dry_run":true,
    "lifecycle_config_found":true,
    "scanned":1,
    "eligible":1,
    "enqueued":0,
    "dry_run_eligible":1,
    "skipped_not_transition":0,
    "skipped_tier":0,
    "skipped_delete_marker":0,
    "skipped_directory":0,
    "skipped_replication":0,
    "skipped_already_in_flight":0,
    "skipped_queue_full":0,
    "skipped_queue_closed":0,
    "skipped_queue_timeout":0,
    "truncated_by_limit":false
  }
}"#;

#[test]
fn ilm_transition_run_json_calls_manual_transition_endpoint() {
    let config_dir = tempfile::tempdir().expect("create config dir");
    let (endpoint, receiver, handle) = start_admin_response_test_server(
        "200 OK",
        "application/json",
        MANUAL_TRANSITION_RESPONSE.to_string(),
    );

    let output = Command::new(rc_binary())
        .args([
            "--json",
            "admin",
            "ilm",
            "transition",
            "run",
            "myalias",
            "photos",
            "--prefix",
            "logs/",
            "--tier",
            "COLDTIER",
            "--dry-run",
            "--max-objects",
            "25",
            "--max-duration-seconds",
            "30",
        ])
        .env("RC_CONFIG_DIR", config_dir.path())
        .env("RC_HOST_myalias", rc_host_alias(&endpoint))
        .output()
        .expect("run rc command");

    assert!(
        output.status.success(),
        "stderr: {}",
        String::from_utf8_lossy(&output.stderr)
    );
    let value: Value = serde_json::from_slice(&output.stdout).expect("stdout should be JSON");
    assert_eq!(value["schema_version"], 3);
    assert_eq!(value["type"], "manual_transition_run");
    assert_eq!(value["data"]["state"], "completed");
    assert_eq!(value["data"]["report"]["dry_run_eligible"], 2);
    assert_eq!(value["data"]["report"]["skipped_already_transitioned"], 1);

    let request = receiver
        .recv_timeout(Duration::from_secs(5))
        .expect("captured admin request");
    assert_eq!(request.method, "POST");
    assert_eq!(
        request.target,
        "/rustfs/admin/v3/ilm/transition/run?bucket=photos&prefix=logs%2F&dryRun=true&maxObjects=25&maxDurationSeconds=30&tier=COLDTIER"
    );
    assert!(request.body.is_empty());
    handle.join().expect("admin test server finished");
}

#[test]
fn ilm_transition_run_human_output_exposes_counts_without_keys() {
    let config_dir = tempfile::tempdir().expect("create config dir");
    let response = MANUAL_TRANSITION_RESPONSE
        .replace(
            "\"state\":\"completed\"",
            "\"state\":\"completed\\u001b[31m\"",
        )
        .replace(
            "\"mode\":\"enqueue_only\"",
            "\"mode\":\"enqueue_only\\u001b[31m\"",
        )
        .replace("\"prefix\":\"logs/\"", "\"prefix\":\"logs/\\u001b[31m\"")
        .replace("\"tier\":\"COLDTIER\"", "\"tier\":\"COLDTIER\\u001b[31m\"");
    let (endpoint, _receiver, handle) =
        start_admin_response_test_server("200 OK", "application/json", response);

    let output = Command::new(rc_binary())
        .args([
            "admin",
            "ilm",
            "transition",
            "run",
            "myalias",
            "photos",
            "--prefix",
            "logs/",
            "--dry-run",
        ])
        .env("RC_CONFIG_DIR", config_dir.path())
        .env("RC_HOST_myalias", rc_host_alias(&endpoint))
        .output()
        .expect("run rc command");

    assert!(
        output.status.success(),
        "stderr: {}",
        String::from_utf8_lossy(&output.stderr)
    );
    let stdout = String::from_utf8(output.stdout).expect("stdout should be UTF-8");
    assert!(stdout.contains("Manual Transition Run"), "stdout: {stdout}");
    assert!(stdout.contains("Eligible:       2"), "stdout: {stdout}");
    assert!(stdout.contains("Already moved:  1"), "stdout: {stdout}");
    assert!(stdout.contains("Duration hit:   true"), "stdout: {stdout}");
    assert!(!stdout.contains("next_marker"), "stdout: {stdout}");
    assert!(!stdout.contains("\x1b["), "stdout: {stdout}");
    assert!(stdout.contains("\\u{1b}"), "stdout: {stdout}");
    handle.join().expect("admin test server finished");
}

#[test]
fn ilm_transition_run_human_output_sanitizes_server_text() {
    let config_dir = tempfile::tempdir().expect("create config dir");
    let (endpoint, _receiver, handle) = start_admin_response_test_server(
        "200 OK",
        "application/json",
        MANUAL_TRANSITION_CONTROL_RESPONSE.to_string(),
    );

    let output = Command::new(rc_binary())
        .args([
            "admin",
            "ilm",
            "transition",
            "run",
            "myalias",
            "photos",
            "--dry-run",
        ])
        .env("RC_CONFIG_DIR", config_dir.path())
        .env("RC_HOST_myalias", rc_host_alias(&endpoint))
        .output()
        .expect("run rc command");

    assert!(
        output.status.success(),
        "stderr: {}",
        String::from_utf8_lossy(&output.stderr)
    );
    let stdout = String::from_utf8(output.stdout).expect("stdout should be UTF-8");
    assert!(
        stdout.contains("State:          completed\\nspoofed"),
        "stdout: {stdout}"
    );
    assert!(
        stdout.contains("Prefix:         logs/\\nnext"),
        "stdout: {stdout}"
    );
    assert!(
        stdout.contains("Tier:           COLD\\tTIER"),
        "stdout: {stdout}"
    );
    handle.join().expect("admin test server finished");
}

#[test]
fn ilm_transition_run_accepts_legacy_response_without_duration_flag() {
    let config_dir = tempfile::tempdir().expect("create config dir");
    let (endpoint, _receiver, handle) = start_admin_response_test_server(
        "200 OK",
        "application/json",
        MANUAL_TRANSITION_LEGACY_RESPONSE.to_string(),
    );

    let output = Command::new(rc_binary())
        .args([
            "admin",
            "ilm",
            "transition",
            "run",
            "myalias",
            "photos",
            "--dry-run",
        ])
        .env("RC_CONFIG_DIR", config_dir.path())
        .env("RC_HOST_myalias", rc_host_alias(&endpoint))
        .output()
        .expect("run rc command");

    assert!(
        output.status.success(),
        "stderr: {}",
        String::from_utf8_lossy(&output.stderr)
    );
    let stdout = String::from_utf8(output.stdout).expect("stdout should be UTF-8");
    assert!(stdout.contains("Already moved:  0"), "stdout: {stdout}");
    assert!(stdout.contains("Duration hit:   false"), "stdout: {stdout}");
    handle.join().expect("admin test server finished");
}