blob: 76a7b861748c67b0bf9578ec10031f6036bb9e25 [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
use crate::bootstrap::create_root_user;
use crate::state::file::FileState;
use crate::state::models::CreateUserWithId;
use crate::state::{COMPONENT, EntryCommand, StateEntry};
use ahash::AHashMap;
use err_trail::ErrContext;
use iggy_binary_protocol::requests::users::CreateUserRequest;
use iggy_binary_protocol::{WireIdentifier, WireName};
use iggy_common::CompressionAlgorithm;
use iggy_common::IggyError;
use iggy_common::IggyExpiry;
use iggy_common::IggyTimestamp;
use iggy_common::MaxTopicSize;
use iggy_common::PersonalAccessToken;
use iggy_common::defaults::DEFAULT_ROOT_USER_ID;
use iggy_common::wire_conversions::{permissions_to_wire, wire_permissions_to_permissions};
use iggy_common::{Permissions, UserStatus};
use std::collections::BTreeMap;
use std::fmt::Display;
use tracing::{debug, error, info};
#[derive(Debug, Clone)]
pub struct SystemState {
pub streams: BTreeMap<u32, StreamState>,
pub users: AHashMap<u32, UserState>,
}
impl SystemState {
pub fn decompose(self) -> (BTreeMap<u32, StreamState>, AHashMap<u32, UserState>) {
(self.streams, self.users)
}
}
#[derive(Debug, Clone)]
pub struct StreamState {
pub id: u32,
pub name: String,
pub created_at: IggyTimestamp,
pub topics: BTreeMap<u32, TopicState>,
}
#[derive(Debug, Clone)]
pub struct TopicState {
pub id: u32,
pub name: String,
pub partitions: BTreeMap<u32, PartitionState>,
pub consumer_groups: BTreeMap<u32, ConsumerGroupState>,
pub compression_algorithm: CompressionAlgorithm,
pub message_expiry: IggyExpiry,
pub max_topic_size: MaxTopicSize,
pub replication_factor: Option<u8>,
pub created_at: IggyTimestamp,
}
#[derive(Debug, Clone)]
pub struct PartitionState {
pub id: u32,
pub created_at: IggyTimestamp,
}
// TODO: consider converting token_hash to SecretString (requires updating the full hash flow across crates)
#[derive(Clone)]
pub struct PersonalAccessTokenState {
pub name: String,
pub token_hash: String,
pub expiry_at: Option<IggyTimestamp>,
}
impl std::fmt::Debug for PersonalAccessTokenState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PersonalAccessTokenState")
.field("name", &self.name)
.field("token_hash", &"[REDACTED]")
.field("expiry_at", &self.expiry_at)
.finish()
}
}
// TODO: consider converting password_hash to SecretString (requires updating the full hash flow across crates)
#[derive(Clone)]
pub struct UserState {
pub id: u32,
pub username: String,
pub password_hash: String,
pub status: UserStatus,
pub created_at: IggyTimestamp,
pub permissions: Option<Permissions>,
pub personal_access_tokens: AHashMap<String, PersonalAccessTokenState>,
}
impl std::fmt::Debug for UserState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("UserState")
.field("id", &self.id)
.field("username", &self.username)
.field("password_hash", &"[REDACTED]")
.field("status", &self.status)
.field("created_at", &self.created_at)
.field("permissions", &self.permissions)
.field("personal_access_tokens", &self.personal_access_tokens)
.finish()
}
}
#[derive(Debug, Clone)]
pub struct ConsumerGroupState {
pub id: u32,
pub name: String,
}
impl SystemState {
pub async fn load(state: FileState) -> Result<Self, IggyError> {
let mut state_entries = state.init().await.error(|e: &IggyError| {
format!("{COMPONENT} (error: {e}) - failed to initialize state entries")
})?;
// Create root user if does not exist.
let root_exists = state_entries
.iter()
.any(|entry| {
entry
.command()
.map(|command| matches!(command, EntryCommand::CreateUser(payload) if payload.user_id == DEFAULT_ROOT_USER_ID))
.unwrap_or_else(|err| {
error!("Failed to check if root user exists: {err}");
false
})
});
if !root_exists {
info!("No users found, creating the root user...");
let root = create_root_user();
let command = CreateUserRequest {
username: WireName::new(root.username.clone())
.expect("root username must be valid"),
password: root.password.clone(),
status: root.status.as_code(),
permissions: root.permissions.as_ref().map(permissions_to_wire),
};
state
.apply(0, &EntryCommand::CreateUser(CreateUserWithId {
user_id: root.id,
command
}))
.await
.error(|e: &IggyError| {
format!(
"{COMPONENT} (error: {e}) - failed to apply create user command, username: {}",
root.username
)
})?;
state_entries = state.init().await.error(|e: &IggyError| {
format!("{COMPONENT} (error: {e}) - failed to initialize state entries")
})?;
}
let system_state = Self::init(state_entries).await.error(|e: &IggyError| {
format!("{COMPONENT} (error: {e}) - failed to initialize system state")
})?;
Ok(system_state)
}
pub async fn init(entries: Vec<StateEntry>) -> Result<Self, IggyError> {
let mut streams = BTreeMap::new();
let mut users = AHashMap::new();
for entry in entries {
debug!("Processing state entry: {entry}",);
match entry.command().error(|e: &IggyError| {
format!(
"{COMPONENT} (error: {e}) - failed to retrieve state entry command: {entry}"
)
})? {
EntryCommand::CreateStream(command) => {
info!("Creating stream: {command:?}");
let stream_id = command.stream_id;
let stream = StreamState {
id: stream_id,
name: command.command.name.to_string(),
topics: BTreeMap::new(),
created_at: entry.timestamp,
};
streams.insert(stream.id, stream);
}
EntryCommand::UpdateStream(command) => {
let stream_id = find_stream_id(&streams, &command.stream_id);
let stream = streams
.get_mut(&stream_id)
.unwrap_or_else(|| panic!("{}", format!("Stream: {stream_id} not found")));
stream.name = command.name.to_string();
}
EntryCommand::DeleteStream(command) => {
let stream_id = find_stream_id(&streams, &command.stream_id);
streams.remove(&stream_id);
}
EntryCommand::PurgeStream(command) => {
let stream_id = find_stream_id(&streams, &command.stream_id);
streams
.get(&stream_id)
.unwrap_or_else(|| panic!("{}", format!("Stream: {stream_id} not found")));
}
EntryCommand::CreateTopic(command) => {
let stream_id = find_stream_id(&streams, &command.command.stream_id);
let stream = streams
.get_mut(&stream_id)
.unwrap_or_else(|| panic!("{}", format!("Stream: {stream_id} not found")));
let topic_id = command.topic_id;
let wire = command.command;
let topic = TopicState {
id: topic_id,
name: wire.name.to_string(),
consumer_groups: BTreeMap::new(),
compression_algorithm: CompressionAlgorithm::from_code(
wire.compression_algorithm,
)?,
message_expiry: IggyExpiry::from(wire.message_expiry),
max_topic_size: MaxTopicSize::from(wire.max_topic_size),
replication_factor: if wire.replication_factor == 0 {
None
} else {
Some(wire.replication_factor)
},
created_at: entry.timestamp,
partitions: if wire.partitions_count > 0 {
let mut partitions = BTreeMap::new();
for i in 0..wire.partitions_count {
partitions.insert(
i,
PartitionState {
id: i,
created_at: entry.timestamp,
},
);
}
partitions
} else {
BTreeMap::new()
},
};
stream.topics.insert(topic.id, topic);
}
EntryCommand::UpdateTopic(command) => {
let stream_id = find_stream_id(&streams, &command.stream_id);
let stream = streams
.get_mut(&stream_id)
.unwrap_or_else(|| panic!("{}", format!("Stream: {stream_id} not found")));
let topic_id = find_topic_id(&stream.topics, &command.topic_id);
let topic = stream
.topics
.get_mut(&topic_id)
.unwrap_or_else(|| panic!("{}", format!("Topic: {topic_id} not found")));
topic.name = command.name.to_string();
topic.compression_algorithm =
CompressionAlgorithm::from_code(command.compression_algorithm)?;
topic.message_expiry = IggyExpiry::from(command.message_expiry);
topic.max_topic_size = MaxTopicSize::from(command.max_topic_size);
topic.replication_factor = if command.replication_factor == 0 {
None
} else {
Some(command.replication_factor)
};
}
EntryCommand::DeleteTopic(command) => {
let stream_id = find_stream_id(&streams, &command.stream_id);
let stream = streams
.get_mut(&stream_id)
.unwrap_or_else(|| panic!("{}", format!("Stream: {stream_id} not found")));
let topic_id = find_topic_id(&stream.topics, &command.topic_id);
stream.topics.remove(&topic_id);
}
EntryCommand::PurgeTopic(command) => {
let stream_id = find_stream_id(&streams, &command.stream_id);
let stream = streams
.get(&stream_id)
.unwrap_or_else(|| panic!("{}", format!("Stream: {stream_id} not found")));
let topic_id = find_topic_id(&stream.topics, &command.topic_id);
stream
.topics
.get(&topic_id)
.unwrap_or_else(|| panic!("{}", format!("Topic: {topic_id} not found")));
}
EntryCommand::CreatePartitions(command) => {
let stream_id = find_stream_id(&streams, &command.stream_id);
let stream = streams
.get_mut(&stream_id)
.unwrap_or_else(|| panic!("{}", format!("Stream: {stream_id} not found")));
let topic_id = find_topic_id(&stream.topics, &command.topic_id);
let topic = stream
.topics
.get_mut(&topic_id)
.unwrap_or_else(|| panic!("{}", format!("Topic: {topic_id} not found")));
let last_partition_id = if topic.partitions.is_empty() {
0
} else {
topic
.partitions
.values()
.map(|p| p.id)
.max()
.unwrap_or_else(|| panic!("No partition found"))
};
for i in 1..=command.partitions_count {
topic.partitions.insert(
last_partition_id + i,
PartitionState {
id: last_partition_id + i,
created_at: entry.timestamp,
},
);
}
}
EntryCommand::DeletePartitions(command) => {
let stream_id = find_stream_id(&streams, &command.stream_id);
let stream = streams
.get_mut(&stream_id)
.unwrap_or_else(|| panic!("{}", format!("Stream: {stream_id} not found")));
let topic_id = find_topic_id(&stream.topics, &command.topic_id);
let topic = stream
.topics
.get_mut(&topic_id)
.unwrap_or_else(|| panic!("{}", format!("Topic: {topic_id} not found")));
if topic.partitions.is_empty() {
continue;
}
let last_partition_id = topic
.partitions
.values()
.map(|p| p.id)
.max()
.unwrap_or_else(|| panic!("No partition found"));
for i in 0..command.partitions_count {
topic.partitions.remove(&(last_partition_id - i));
}
}
EntryCommand::DeleteSegments(command) => {
let stream_id = find_stream_id(&streams, &command.stream_id);
let stream = streams
.get_mut(&stream_id)
.unwrap_or_else(|| panic!("{}", format!("Stream: {stream_id} not found")));
let topic_id = find_topic_id(&stream.topics, &command.topic_id);
let topic = stream
.topics
.get_mut(&topic_id)
.unwrap_or_else(|| panic!("{}", format!("Topic: {topic_id} not found")));
if topic.partitions.is_empty() {
continue;
}
let partition_id = command.partition_id;
let _partition =
topic
.partitions
.get(&command.partition_id)
.unwrap_or_else(|| {
panic!("{}", format!("Partition {partition_id} not found."))
});
}
EntryCommand::CreateConsumerGroup(command) => {
let consumer_group_id = command.group_id;
let wire = command.command;
let stream_id = find_stream_id(&streams, &wire.stream_id);
let stream = streams
.get_mut(&stream_id)
.unwrap_or_else(|| panic!("{}", format!("Stream: {stream_id} not found")));
let topic_id = find_topic_id(&stream.topics, &wire.topic_id);
let topic = stream
.topics
.get_mut(&topic_id)
.unwrap_or_else(|| panic!("{}", format!("Topic: {topic_id} not found")));
let consumer_group = ConsumerGroupState {
id: consumer_group_id,
name: wire.name.to_string(),
};
topic
.consumer_groups
.insert(consumer_group.id, consumer_group);
}
EntryCommand::DeleteConsumerGroup(command) => {
let stream_id = find_stream_id(&streams, &command.stream_id);
let stream = streams
.get_mut(&stream_id)
.unwrap_or_else(|| panic!("{}", format!("Stream: {stream_id} not found")));
let topic_id = find_topic_id(&stream.topics, &command.topic_id);
let topic = stream
.topics
.get_mut(&topic_id)
.unwrap_or_else(|| panic!("{}", format!("Topic: {topic_id} not found")));
let consumer_group_id =
find_consumer_group_id(&topic.consumer_groups, &command.group_id);
topic.consumer_groups.remove(&consumer_group_id);
}
EntryCommand::CreateUser(command) => {
let user_id = command.user_id;
let wire = command.command;
let user = UserState {
id: user_id,
username: wire.username.to_string(),
password_hash: wire.password, // already hashed at write time
status: UserStatus::from_code(wire.status)?,
created_at: entry.timestamp,
permissions: wire
.permissions
.as_ref()
.map(wire_permissions_to_permissions),
personal_access_tokens: AHashMap::new(),
};
users.insert(user.id, user);
}
EntryCommand::UpdateUser(command) => {
let user_id = find_user_id(&users, &command.user_id);
let user = users
.get_mut(&user_id)
.unwrap_or_else(|| panic!("{}", format!("User: {user_id} not found")));
if let Some(username) = &command.username {
user.username = username.to_string();
}
if let Some(status) = command.status {
user.status = UserStatus::from_code(status)?;
}
}
EntryCommand::DeleteUser(command) => {
let user_id = find_user_id(&users, &command.user_id);
users.remove(&user_id);
}
EntryCommand::ChangePassword(command) => {
let user_id = find_user_id(&users, &command.user_id);
let user = users
.get_mut(&user_id)
.unwrap_or_else(|| panic!("{}", format!("User: {user_id} not found")));
user.password_hash = command.new_password; // already hashed at write time
}
EntryCommand::UpdatePermissions(command) => {
let user_id = find_user_id(&users, &command.user_id);
let user = users
.get_mut(&user_id)
.unwrap_or_else(|| panic!("{}", format!("User: {user_id} not found")));
user.permissions = command
.permissions
.as_ref()
.map(wire_permissions_to_permissions);
}
EntryCommand::CreatePersonalAccessToken(command) => {
let token_hash = command.hash;
let user_id = find_user_id(&users, &WireIdentifier::numeric(entry.user_id));
let user = users
.get_mut(&user_id)
.unwrap_or_else(|| panic!("{}", format!("User: {user_id} not found")));
let expiry_at = PersonalAccessToken::calculate_expiry_at(
entry.timestamp,
IggyExpiry::from(command.command.expiry),
);
if let Some(expiry_at) = expiry_at
&& expiry_at.as_micros() <= IggyTimestamp::now().as_micros()
{
debug!("Personal access token: {token_hash} has already expired.");
continue;
}
let name = command.command.name.to_string();
user.personal_access_tokens.insert(
name.clone(),
PersonalAccessTokenState {
name,
token_hash,
expiry_at,
},
);
}
EntryCommand::DeletePersonalAccessToken(command) => {
let user_id = find_user_id(&users, &WireIdentifier::numeric(entry.user_id));
let user = users
.get_mut(&user_id)
.unwrap_or_else(|| panic!("{}", format!("User: {user_id} not found")));
user.personal_access_tokens.remove(command.name.as_str());
}
}
}
let state = SystemState { streams, users };
debug!("+++ State +++");
debug!("{state}");
debug!("+++ State +++");
Ok(state)
}
}
fn find_stream_id(streams: &BTreeMap<u32, StreamState>, stream_id: &WireIdentifier) -> u32 {
match stream_id {
WireIdentifier::Numeric(id) => *id,
WireIdentifier::String(name) => {
let name = name.as_str();
let stream = streams
.values()
.find(|s| s.name == name)
.unwrap_or_else(|| panic!("Stream: {name} not found"));
stream.id
}
}
}
fn find_topic_id(topics: &BTreeMap<u32, TopicState>, topic_id: &WireIdentifier) -> u32 {
match topic_id {
WireIdentifier::Numeric(id) => *id,
WireIdentifier::String(name) => {
let name = name.as_str();
let topic = topics
.values()
.find(|s| s.name == name)
.unwrap_or_else(|| panic!("Topic: {name} not found"));
topic.id
}
}
}
fn find_consumer_group_id(
groups: &BTreeMap<u32, ConsumerGroupState>,
group_id: &WireIdentifier,
) -> u32 {
match group_id {
WireIdentifier::Numeric(id) => *id,
WireIdentifier::String(name) => {
let name = name.as_str();
let group = groups
.values()
.find(|s| s.name == name)
.unwrap_or_else(|| panic!("Consumer group: {name} not found"));
group.id
}
}
}
fn find_user_id(users: &AHashMap<u32, UserState>, user_id: &WireIdentifier) -> u32 {
match user_id {
WireIdentifier::Numeric(id) => *id,
WireIdentifier::String(name) => {
let name = name.as_str();
let user = users
.values()
.find(|s| s.username == name)
.unwrap_or_else(|| panic!("User: {name} not found"));
user.id
}
}
}
impl Display for SystemState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Streams:")?;
for stream in self.streams.iter() {
write!(f, "\n================\n")?;
write!(f, "{}", stream.1)?;
}
write!(f, "Users:")?;
for user in self.users.iter() {
write!(f, "\n================\n")?;
write!(f, "{}", user.1)?;
}
Ok(())
}
}
impl Display for ConsumerGroupState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "ConsumerGroup -> ID: {}, Name: {}", self.id, self.name)
}
}
impl Display for UserState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let permissions = if let Some(permissions) = &self.permissions {
permissions.to_string()
} else {
"no_permissions".to_string()
};
write!(
f,
"User -> ID: {}, Username: {}, Status: {}, Permissions: {}",
self.id, self.username, self.status, permissions
)
}
}
impl Display for StreamState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Stream -> ID: {}, Name: {}", self.id, self.name,)?;
for topic in self.topics.iter() {
write!(f, "\n {}", topic.1)?;
}
Ok(())
}
}
impl Display for TopicState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Topic -> ID: {}, Name: {}", self.id, self.name,)?;
for partition in self.partitions.iter() {
write!(f, "\n {}", partition.1)?;
}
write!(f, "\nConsumer Groups:")?;
for consumer_group in self.consumer_groups.iter() {
write!(f, "\n {}", consumer_group.1)?;
}
Ok(())
}
}
impl Display for PartitionState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Partition -> ID: {}, Created At: {}",
self.id, self.created_at
)
}
}