Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
resolver = "2"
members = [
"crates/redis-cloud",
"crates/redis-enterprise",
"crates/redis-common",
"crates/redis-enterprise",
"crates/redisctl",
]

Expand Down Expand Up @@ -59,7 +58,6 @@ pretty_assertions = "1.4"
# Internal crates
redis-cloud = { path = "crates/redis-cloud" }
redis-enterprise = { path = "crates/redis-enterprise" }
redis-common = { path = "crates/redis-common" }

[profile.release]
opt-level = 3
Expand Down
1 change: 1 addition & 0 deletions crates/redis-common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ description = "Shared utilities for Redis CLI tools"
keywords = ["redis", "cli", "common", "utilities"]
categories = ["command-line-utilities", "api-bindings"]
readme = "../../README.md"
publish = false

[dependencies]
anyhow = { workspace = true }
Expand Down
10 changes: 9 additions & 1 deletion crates/redisctl/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ path = "src/enterprise_bin.rs"
required-features = ["enterprise-only"]

[dependencies]
redis-common = { version = "0.1.0", path = "../redis-common" }
redis-cloud = { version = "0.1.0", path = "../redis-cloud" }
redis-enterprise = { version = "0.1.0", path = "../redis-enterprise" }

Expand All @@ -43,6 +42,15 @@ chrono = "0.4"
rpassword = { workspace = true }
urlencoding = "2.1"

# Dependencies from redis-common
thiserror = { workspace = true }
serde_yaml = { workspace = true }
comfy-table = { workspace = true }
jmespath = { workspace = true }
config = { workspace = true }
toml = { workspace = true }
directories = { workspace = true }

# Conditional dependencies for feature-gated binaries
[features]
default = ["full"]
Expand Down
3 changes: 2 additions & 1 deletion crates/redisctl/src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::commands::api::ApiCommands;
use crate::config::DeploymentType;
use crate::output::OutputFormat;
use clap::{Parser, Subcommand};
use redis_common::{DeploymentType, OutputFormat};

#[derive(Parser)]
#[command(name = "redisctl")]
Expand Down
7 changes: 5 additions & 2 deletions crates/redisctl/src/cloud_bin.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
// Redis Cloud only binary
// This binary only includes Cloud functionality to reduce size for Cloud-only deployments

use crate::config::{Config, DeploymentType};
use anyhow::Result;
use clap::Parser;
use redis_common::{Config, DeploymentType};
use tracing::info;

mod cli;
mod commands;
mod config;
mod error;
mod output;

use cli::{Cli, Commands};
use commands::{cloud, profile};
Expand Down Expand Up @@ -53,7 +56,7 @@ async fn main() -> Result<()> {
fn get_cloud_profile<'a>(
config: &'a Config,
profile_name: &Option<String>,
) -> Result<&'a redis_common::Profile> {
) -> Result<&'a crate::config::Profile> {
let env_profile = std::env::var("REDISCTL_PROFILE").ok();
let profile_name = profile_name
.as_deref()
Expand Down
2 changes: 1 addition & 1 deletion crates/redisctl/src/commands/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

#![allow(dead_code)]

use crate::output::{OutputFormat, print_output};
use anyhow::{Context, Result};
use clap::Subcommand;
use redis_cloud::CloudClient;
use redis_common::{OutputFormat, print_output};
use redis_enterprise::EnterpriseClient;
use serde_json::Value;
use std::fs;
Expand Down
3 changes: 2 additions & 1 deletion crates/redisctl/src/commands/cloud.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::config::{Profile, ProfileCredentials};
use crate::output::{OutputFormat, print_output};
use anyhow::Result;
use redis_cloud::CloudClient;
use redis_common::{OutputFormat, Profile, ProfileCredentials, print_output};

use crate::cli::{
AccountCommands, AclCommands, ApiKeyCommands, BackupCommands, CloudAccountCommands,
Expand Down
2 changes: 1 addition & 1 deletion crates/redisctl/src/commands/cloud_billing.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::output::{OutputFormat, print_output};
use anyhow::Result;
use redis_cloud::{CloudBillingHandler, CloudClient};
use redis_common::{OutputFormat, print_output};

use crate::cli::BillingCommands;

Expand Down
3 changes: 2 additions & 1 deletion crates/redisctl/src/commands/enterprise.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::config::{Profile, ProfileCredentials};
use crate::output::{OutputFormat, print_output};
use anyhow::Result;
use redis_common::{OutputFormat, Profile, ProfileCredentials, print_output};
use redis_enterprise::EnterpriseClient;
use std::io::Write;

Expand Down
5 changes: 2 additions & 3 deletions crates/redisctl/src/commands/profile.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
use crate::config::{Config, DeploymentType, Profile, ProfileCredentials};
use crate::output::{OutputFormat, print_output};
use anyhow::Result;
use redis_common::{
Config, DeploymentType, OutputFormat, Profile, ProfileCredentials, print_output,
};

use crate::cli::ProfileCommands;

Expand Down
123 changes: 123 additions & 0 deletions crates/redisctl/src/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
#![allow(dead_code)]

use anyhow::{Context, Result};
use directories::ProjectDirs;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;

#[derive(Debug, Serialize, Deserialize, Default, Clone)]
pub struct Config {
#[serde(default)]
pub default: Option<String>, // Name of the default profile
#[serde(default)]
pub profiles: HashMap<String, Profile>,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Profile {
pub deployment_type: DeploymentType,
#[serde(flatten)]
pub credentials: ProfileCredentials,
}

#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, clap::ValueEnum)]
pub enum DeploymentType {
Cloud,
Enterprise,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(untagged)]
pub enum ProfileCredentials {
Cloud {
api_key: String,
api_secret: String,
#[serde(default = "default_cloud_url")]
api_url: String,
},
Enterprise {
url: String,
username: String,
password: Option<String>, // Optional for prompting
#[serde(default)]
insecure: bool,
},
}

fn default_cloud_url() -> String {
"https://api.redislabs.com/v1".to_string()
}

impl ProfileCredentials {
pub fn has_password(&self) -> bool {
match self {
ProfileCredentials::Enterprise { password, .. } => password.is_some(),
_ => false,
}
}
}

impl Config {
pub fn load() -> Result<Self> {
let config_path = Self::config_path()?;

if !config_path.exists() {
return Ok(Config::default());
}

let content = fs::read_to_string(&config_path)
.with_context(|| format!("Failed to read config from {:?}", config_path))?;

toml::from_str(&content)
.with_context(|| format!("Failed to parse config from {:?}", config_path))
}

pub fn save(&self) -> Result<()> {
let config_path = Self::config_path()?;

// Create parent directories if they don't exist
if let Some(parent) = config_path.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("Failed to create config directory {:?}", parent))?;
}

let content = toml::to_string_pretty(self).context("Failed to serialize config")?;

fs::write(&config_path, content)
.with_context(|| format!("Failed to write config to {:?}", config_path))?;

Ok(())
}

pub fn get_profile(&self, name: Option<&str>) -> Option<&Profile> {
let env_profile = std::env::var("REDISCTL_PROFILE").ok();
let profile_name = name
.or(self.default.as_deref())
.or(env_profile.as_deref())?;

self.profiles.get(profile_name)
}

pub fn set_profile(&mut self, name: String, profile: Profile) {
self.profiles.insert(name, profile);
}

pub fn remove_profile(&mut self, name: &str) -> Option<Profile> {
self.profiles.remove(name)
}

pub fn list_profiles(&self) -> Vec<(&String, &Profile)> {
let mut profiles: Vec<_> = self.profiles.iter().collect();
profiles.sort_by_key(|(name, _)| *name);
profiles
}

fn config_path() -> Result<PathBuf> {
let proj_dirs = ProjectDirs::from("com", "redis", "redisctl")
.context("Failed to determine config directory")?;

Ok(proj_dirs.config_dir().join("config.toml"))
}
}
7 changes: 5 additions & 2 deletions crates/redisctl/src/enterprise_bin.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
// Redis Enterprise only binary
// This binary only includes Enterprise functionality to reduce size for Enterprise-only deployments

use crate::config::{Config, DeploymentType};
use anyhow::Result;
use clap::Parser;
use redis_common::{Config, DeploymentType};
use tracing::info;

mod cli;
mod commands;
mod config;
mod error;
mod output;

use cli::{Cli, Commands};
use commands::{enterprise, profile};
Expand Down Expand Up @@ -62,7 +65,7 @@ async fn main() -> Result<()> {
fn get_enterprise_profile<'a>(
config: &'a Config,
profile_name: &Option<String>,
) -> Result<&'a redis_common::Profile> {
) -> Result<&'a crate::config::Profile> {
let env_profile = std::env::var("REDISCTL_PROFILE").ok();
let profile_name = profile_name
.as_deref()
Expand Down
59 changes: 59 additions & 0 deletions crates/redisctl/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#![allow(dead_code)]

use thiserror::Error;

#[derive(Error, Debug)]
pub enum RedisCtlError {
#[error("Configuration error: {0}")]
Config(#[from] ConfigError),

#[error("Profile error: {0}")]
Profile(#[from] ProfileError),

#[error("Command routing error: {0}")]
Routing(#[from] RoutingError),
}

#[derive(Error, Debug)]
pub enum ConfigError {
#[error("Profile '{name}' not found")]
ProfileNotFound { name: String },

#[error("No default profile set")]
NoDefaultProfile,

#[error("Config file error: {message}")]
FileError { message: String },
}

#[derive(Error, Debug)]
pub enum ProfileError {
#[error("Profile '{name}' is type '{actual_type}' but command requires '{expected_type}'")]
TypeMismatch {
name: String,
actual_type: String,
expected_type: String,
},

#[error("Missing credentials for profile '{name}'")]
MissingCredentials { name: String },
}

#[derive(Error, Debug)]
pub enum RoutingError {
#[error(
"Command '{command}' exists in both cloud and enterprise. Use 'redisctl cloud {command}' or 'redisctl enterprise {command}'"
)]
AmbiguousCommand { command: String },

#[error("Command '{command}' not found in {deployment_type}")]
CommandNotFound {
command: String,
deployment_type: String,
},

#[error(
"No profile specified and no default profile set. Use --profile or set REDISCTL_PROFILE"
)]
NoProfileSpecified,
}
Loading
Loading