use eyre::{Context, Result};
use std::path::{Path, PathBuf};
use crate::client::{Auth, KibanaClient};
use crate::kibana::saved_objects::SavedObjectsManifest;
use crate::kibana::spaces::{SpacesExtractor, SpacesManifest};
use crate::storage::{sanitize_filename, transform_env_file};
use url::Url;
pub fn migrate_manifest(
project_dir: impl AsRef<Path>,
backup_old: bool,
) -> Result<MigrationResult> {
let project_dir = project_dir.as_ref();
let old_manifest_path = project_dir.join("manifest.json");
let new_manifest_dir = project_dir.join("manifest");
let new_manifest_path = new_manifest_dir.join("saved_objects.json");
if !old_manifest_path.exists() {
return Ok(MigrationResult::NoLegacyManifest);
}
if new_manifest_path.exists() {
return Ok(MigrationResult::AlreadyMigrated);
}
log::info!(
"Migrating manifest from {} to {}",
old_manifest_path.display(),
new_manifest_path.display()
);
let manifest = SavedObjectsManifest::read(&old_manifest_path).with_context(|| {
format!(
"Failed to read legacy manifest: {}",
old_manifest_path.display()
)
})?;
log::info!("Read legacy manifest with {} objects", manifest.count());
std::fs::create_dir_all(&new_manifest_dir).with_context(|| {
format!(
"Failed to create manifest directory: {}",
new_manifest_dir.display()
)
})?;
manifest.write(&new_manifest_path).with_context(|| {
format!(
"Failed to write new manifest: {}",
new_manifest_path.display()
)
})?;
log::info!("Wrote new manifest to {}", new_manifest_path.display());
migrate_object_files(project_dir)?;
if backup_old {
let backup_path = project_dir.join("manifest.json.backup");
std::fs::rename(&old_manifest_path, &backup_path).with_context(|| {
format!("Failed to backup old manifest to {}", backup_path.display())
})?;
log::info!("Backed up old manifest to {}", backup_path.display());
Ok(MigrationResult::MigratedWithBackup(backup_path))
} else {
std::fs::remove_file(&old_manifest_path).with_context(|| {
format!(
"Failed to remove old manifest: {}",
old_manifest_path.display()
)
})?;
log::info!("Removed old manifest");
Ok(MigrationResult::MigratedWithoutBackup)
}
}
fn migrate_object_files(project_dir: &Path) -> Result<()> {
let objects_dir = project_dir.join("objects");
if !objects_dir.exists() {
log::debug!("No objects directory found, skipping object file migration");
return Ok(());
}
let mut migrated_count = 0;
let mut already_hierarchical = 0;
for entry in std::fs::read_dir(&objects_dir).with_context(|| {
format!(
"Failed to read objects directory: {}",
objects_dir.display()
)
})? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
already_hierarchical += 1;
continue;
}
if path.extension().and_then(|s| s.to_str()) != Some("json") {
continue;
}
let filename = match path.file_stem().and_then(|s| s.to_str()) {
Some(name) => name,
None => continue,
};
if let Some(dot_pos) = filename.rfind('.') {
let obj_name = &filename[..dot_pos];
let obj_type = &filename[dot_pos + 1..];
let sanitized_name = sanitize_filename(obj_name);
let type_dir = objects_dir.join(obj_type);
let new_path = type_dir.join(format!("{}.json", sanitized_name));
std::fs::create_dir_all(&type_dir)
.with_context(|| format!("Failed to create directory: {}", type_dir.display()))?;
std::fs::rename(&path, &new_path).with_context(|| {
format!(
"Failed to move {} to {}",
path.display(),
new_path.display()
)
})?;
log::debug!(
"Migrated object file: {} → {}",
path.display(),
new_path.display()
);
migrated_count += 1;
} else {
log::warn!(
"Skipping file with unexpected name format (expected 'object_name.type.json'): {}",
path.display()
);
}
}
if migrated_count > 0 {
log::info!(
"Migrated {} object file(s) from flat to hierarchical structure",
migrated_count
);
} else if already_hierarchical > 0 {
log::info!(
"Object files already in hierarchical structure ({} subdirectories found)",
already_hierarchical
);
} else {
log::info!("No object files to migrate");
}
Ok(())
}
pub fn needs_migration(project_dir: impl AsRef<Path>) -> bool {
let project_dir = project_dir.as_ref();
let old_manifest = project_dir.join("manifest.json");
let new_manifest = project_dir.join("manifest/saved_objects.json");
old_manifest.exists() && !new_manifest.exists()
}
pub fn load_saved_objects_manifest(project_dir: impl AsRef<Path>) -> Result<SavedObjectsManifest> {
let project_dir = project_dir.as_ref();
let new_path = project_dir.join("manifest/saved_objects.json");
let old_path = project_dir.join("manifest.json");
if new_path.exists() {
log::debug!("Loading manifest from new location: {}", new_path.display());
Ok(SavedObjectsManifest::read(&new_path)?)
} else if old_path.exists() {
log::warn!(
"Loading manifest from legacy location: {}. Consider running 'kibob migrate' to update.",
old_path.display()
);
Ok(SavedObjectsManifest::read(&old_path)?)
} else {
eyre::bail!(
"No saved objects manifest found at {} or {}",
new_path.display(),
old_path.display()
)
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum MigrationResult {
MigratedWithBackup(PathBuf),
MigratedWithoutBackup,
NoLegacyManifest,
AlreadyMigrated,
}
impl std::fmt::Display for MigrationResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MigrationResult::MigratedWithBackup(path) => {
write!(
f,
"Migration completed. Old manifest backed up to: {}",
path.display()
)
}
MigrationResult::MigratedWithoutBackup => {
write!(f, "Migration completed. Old manifest removed.")
}
MigrationResult::NoLegacyManifest => {
write!(f, "No legacy manifest.json found. Nothing to migrate.")
}
MigrationResult::AlreadyMigrated => {
write!(f, "Already migrated. manifest/saved_objects.json exists.")
}
}
}
}
pub fn needs_migration_unified(project_dir: impl AsRef<Path>) -> bool {
let project_dir = project_dir.as_ref();
let legacy_manifest = project_dir.join("manifest.json");
let old_manifest = project_dir.join("manifest/saved_objects.json");
let target_space = std::env::var("kibana_space")
.or_else(|_| std::env::var("KIBANA_SPACE"))
.unwrap_or_else(|_| "default".to_string());
let new_default_manifest_dir = project_dir.join(&target_space).join("manifest");
if new_default_manifest_dir.exists() {
return false;
}
legacy_manifest.exists() || old_manifest.exists()
}
pub async fn migrate_to_multispace_unified(
project_dir: impl AsRef<Path>,
backup_old: bool,
env_path: Option<impl AsRef<Path>>,
) -> Result<MigrationResult> {
let project_dir = project_dir.as_ref();
if !needs_migration_unified(project_dir) {
return Ok(MigrationResult::AlreadyMigrated);
}
let target_space = std::env::var("kibana_space")
.or_else(|_| std::env::var("KIBANA_SPACE"))
.unwrap_or_else(|_| "default".to_string());
if target_space != "default" {
log::info!("Using space '{}' for migration target", target_space);
}
log::info!("Migrating project to multi-space structure...");
log::info!("Target space: '{}'", target_space);
let legacy_manifest_path = project_dir.join("manifest.json");
let old_manifest_path = project_dir.join("manifest/saved_objects.json");
let target_space_dir = project_dir.join(&target_space);
let new_manifest_dir = target_space_dir.join("manifest");
let new_manifest_path = new_manifest_dir.join("saved_objects.json");
std::fs::create_dir_all(&new_manifest_dir)?;
std::fs::create_dir_all(&target_space_dir)?;
if let Some(env_path) = env_path {
transform_env_file(env_path)?;
}
let kibana_url = match std::env::var("KIBANA_URL") {
Ok(url) => Url::parse(&url)?,
Err(_) => return Err(eyre::eyre!("KIBANA_URL environment variable not set")),
};
let auth = if let Ok(api_key) = std::env::var("KIBANA_APIKEY") {
Auth::Apikey(api_key)
} else if let (Ok(u), Ok(p)) = (
std::env::var("KIBANA_USERNAME"),
std::env::var("KIBANA_PASSWORD"),
) {
Auth::Basic(u, p)
} else {
Auth::None
};
if let Ok(client) = KibanaClient::builder(kibana_url)
.auth(auth)
.max_concurrency(8)
.build()
{
let extractor = SpacesExtractor::all(client);
if let Ok(space_def) = extractor.fetch_space(&target_space).await {
let space_file = target_space_dir.join("space.json");
let json = serde_json::to_string_pretty(&space_def)?;
std::fs::write(&space_file, json)?;
log::info!(
"Fetched and wrote space definition to {}",
space_file.display()
);
let spaces_manifest_path = project_dir.join("spaces.yml");
let mut spaces_manifest = if spaces_manifest_path.exists() {
SpacesManifest::read(&spaces_manifest_path)?
} else {
SpacesManifest::new()
};
let space_name = space_def
.get("name")
.and_then(|v| v.as_str())
.unwrap_or(&target_space);
if spaces_manifest.add_space(target_space.clone(), space_name.to_string()) {
spaces_manifest.write(&spaces_manifest_path)?;
log::info!("Added space '{}' to root spaces.yml", target_space);
}
}
}
let backup_path = if legacy_manifest_path.exists() {
log::info!("Migrating from legacy manifest.json...");
let manifest = SavedObjectsManifest::read(&legacy_manifest_path)?;
log::info!("Read legacy manifest with {} objects", manifest.count());
manifest.write(&new_manifest_path)?;
log::info!("Wrote new manifest to {}", new_manifest_path.display());
migrate_legacy_object_files(project_dir)?;
let old_objects_dir = project_dir.join("objects");
let new_objects_dir = target_space_dir.join("objects");
if old_objects_dir.exists() && !new_objects_dir.exists() {
std::fs::rename(&old_objects_dir, &new_objects_dir)?;
log::info!("Moved objects/ → {}/objects/", target_space);
}
if backup_old {
let backup = project_dir.join("manifest.json.backup");
std::fs::rename(&legacy_manifest_path, &backup)?;
log::info!("Backed up old manifest to {}", backup.display());
Some(backup)
} else {
std::fs::remove_file(&legacy_manifest_path)?;
log::info!("Removed old manifest");
None
}
} else if old_manifest_path.exists() {
log::info!("Migrating from v0.1.0 single-space structure...");
migrate_file_simple(&old_manifest_path, &new_manifest_path)?;
let old_manifest_dir = project_dir.join("manifest");
for file_name in &["workflows.yml", "agents.yml", "tools.yml"] {
let old_file = old_manifest_dir.join(file_name);
let new_file = new_manifest_dir.join(file_name);
if old_file.exists() {
migrate_file_simple(&old_file, &new_file)?;
}
}
for dir_name in &["objects", "workflows", "agents", "tools"] {
let old_dir = project_dir.join(dir_name);
let new_dir = target_space_dir.join(dir_name);
if old_dir.exists() && !new_dir.exists() {
std::fs::rename(&old_dir, &new_dir)?;
log::info!("Moved {}/ → {}/{}/", dir_name, target_space, dir_name);
}
}
let old_manifest_dir = project_dir.join("manifest");
if old_manifest_dir.exists()
&& let Ok(entries) = std::fs::read_dir(&old_manifest_dir)
&& entries.count() == 0
{
std::fs::remove_dir(&old_manifest_dir)?;
log::info!("Removed empty manifest/ directory");
}
None
} else {
return Ok(MigrationResult::NoLegacyManifest);
};
log::info!("✓ Migration to multi-space structure complete!");
log::info!(" New structure: {}/manifest/", target_space);
if target_space != "default" {
log::info!(
"Note: KIBANA_SPACE='{}' is deprecated. You can now safely unset it.",
target_space
);
}
if let Some(backup) = backup_path {
Ok(MigrationResult::MigratedWithBackup(backup))
} else {
Ok(MigrationResult::MigratedWithoutBackup)
}
}
fn migrate_legacy_object_files(project_dir: &Path) -> Result<()> {
let objects_dir = project_dir.join("objects");
if !objects_dir.exists() {
return Ok(());
}
let mut migrated_count = 0;
for entry in std::fs::read_dir(&objects_dir)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
continue;
}
if path.extension().and_then(|s| s.to_str()) != Some("json") {
continue;
}
let filename = match path.file_stem().and_then(|s| s.to_str()) {
Some(name) => name,
None => continue,
};
if let Some(dot_pos) = filename.rfind('.') {
let obj_name = &filename[..dot_pos];
let obj_type = &filename[dot_pos + 1..];
let sanitized_name = sanitize_filename(obj_name);
let type_dir = objects_dir.join(obj_type);
let new_path = type_dir.join(format!("{}.json", sanitized_name));
std::fs::create_dir_all(&type_dir)?;
std::fs::rename(&path, &new_path)?;
log::debug!("Migrated: {} → {}", path.display(), new_path.display());
migrated_count += 1;
}
}
if migrated_count > 0 {
log::info!(
"Migrated {} object file(s) to hierarchical structure",
migrated_count
);
}
Ok(())
}
fn migrate_file_simple(old_path: &Path, new_path: &Path) -> Result<()> {
if !old_path.exists() {
return Ok(());
}
if new_path.exists() {
log::warn!("Skipping {}: target already exists", old_path.display());
return Ok(());
}
if let Some(parent) = new_path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::rename(old_path, new_path)?;
log::info!("Moved {} → {}", old_path.display(), new_path.display());
Ok(())
}
pub fn needs_multispace_migration(project_dir: impl AsRef<Path>) -> bool {
let project_dir = project_dir.as_ref();
let old_manifest = project_dir.join("manifest/saved_objects.json");
let old_workflows = project_dir.join("manifest/workflows.yml");
let old_agents = project_dir.join("manifest/agents.yml");
let old_tools = project_dir.join("manifest/tools.yml");
let new_default_manifest_dir = project_dir.join("manifest/default");
if new_default_manifest_dir.exists() {
return false;
}
old_manifest.exists() || old_workflows.exists() || old_agents.exists() || old_tools.exists()
}
pub fn migrate_to_multispace(
project_dir: impl AsRef<Path>,
backup_old: bool,
) -> Result<MultispaceMigrationResult> {
let project_dir = project_dir.as_ref();
if !needs_multispace_migration(project_dir) {
return Ok(MultispaceMigrationResult::AlreadyMigrated);
}
log::info!("Migrating project to multi-space structure...");
let mut migrated_items = Vec::new();
let default_manifest_dir = project_dir.join("manifest/default");
let default_space_dir = project_dir.join("default");
std::fs::create_dir_all(&default_manifest_dir)?;
std::fs::create_dir_all(&default_space_dir)?;
migrate_file_to_space(
project_dir,
"manifest/saved_objects.json",
"manifest/default/saved_objects.json",
backup_old,
&mut migrated_items,
)?;
migrate_file_to_space(
project_dir,
"manifest/workflows.yml",
"manifest/default/workflows.yml",
backup_old,
&mut migrated_items,
)?;
migrate_file_to_space(
project_dir,
"manifest/agents.yml",
"manifest/default/agents.yml",
backup_old,
&mut migrated_items,
)?;
migrate_file_to_space(
project_dir,
"manifest/tools.yml",
"manifest/default/tools.yml",
backup_old,
&mut migrated_items,
)?;
migrate_directory_to_space(
project_dir,
"objects",
"default/objects",
backup_old,
&mut migrated_items,
)?;
migrate_directory_to_space(
project_dir,
"workflows",
"default/workflows",
backup_old,
&mut migrated_items,
)?;
migrate_directory_to_space(
project_dir,
"agents",
"default/agents",
backup_old,
&mut migrated_items,
)?;
migrate_directory_to_space(
project_dir,
"tools",
"default/tools",
backup_old,
&mut migrated_items,
)?;
log::info!(
"✓ Multi-space migration complete: {} items migrated",
migrated_items.len()
);
Ok(MultispaceMigrationResult::Migrated {
items: migrated_items,
backup_created: backup_old,
})
}
fn migrate_file_to_space(
project_dir: &Path,
old_path: &str,
new_path: &str,
backup: bool,
migrated_items: &mut Vec<String>,
) -> Result<()> {
let old = project_dir.join(old_path);
let new = project_dir.join(new_path);
if !old.exists() {
log::debug!("Skipping {}: file doesn't exist", old_path);
return Ok(());
}
if new.exists() {
log::warn!("Skipping {}: target already exists", new_path);
return Ok(());
}
if let Some(parent) = new.parent() {
std::fs::create_dir_all(parent)?;
}
if backup {
std::fs::copy(&old, &new)?;
std::fs::remove_file(&old)?;
log::info!("Migrated (with backup): {} → {}", old_path, new_path);
} else {
std::fs::rename(&old, &new)?;
log::info!("Migrated: {} → {}", old_path, new_path);
}
migrated_items.push(old_path.to_string());
Ok(())
}
fn migrate_directory_to_space(
project_dir: &Path,
old_path: &str,
new_path: &str,
backup: bool,
migrated_items: &mut Vec<String>,
) -> Result<()> {
let old = project_dir.join(old_path);
let new = project_dir.join(new_path);
if !old.exists() {
log::debug!("Skipping {}: directory doesn't exist", old_path);
return Ok(());
}
if new.exists() {
log::warn!("Skipping {}: target already exists", new_path);
return Ok(());
}
if let Some(parent) = new.parent() {
std::fs::create_dir_all(parent)?;
}
if backup {
copy_dir_all(&old, &new)?;
std::fs::remove_dir_all(&old)?;
log::info!(
"Migrated directory (with backup): {} → {}",
old_path,
new_path
);
} else {
std::fs::rename(&old, &new)?;
log::info!("Migrated directory: {} → {}", old_path, new_path);
}
migrated_items.push(format!("{}/", old_path));
Ok(())
}
fn copy_dir_all(src: &Path, dst: &Path) -> Result<()> {
std::fs::create_dir_all(dst)?;
for entry in std::fs::read_dir(src)? {
let entry = entry?;
let ty = entry.file_type()?;
let src_path = entry.path();
let dst_path = dst.join(entry.file_name());
if ty.is_dir() {
copy_dir_all(&src_path, &dst_path)?;
} else {
std::fs::copy(&src_path, &dst_path)?;
}
}
Ok(())
}
#[derive(Debug, Clone, PartialEq)]
pub enum MultispaceMigrationResult {
Migrated {
items: Vec<String>,
backup_created: bool,
},
AlreadyMigrated,
}
impl std::fmt::Display for MultispaceMigrationResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MultispaceMigrationResult::Migrated {
items,
backup_created,
} => {
write!(
f,
"Multi-space migration completed: {} items migrated{}",
items.len(),
if *backup_created {
" (with backups)"
} else {
""
}
)
}
MultispaceMigrationResult::AlreadyMigrated => {
write!(f, "Already using multi-space structure")
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::kibana::saved_objects::SavedObject;
use tempfile::TempDir;
#[test]
fn test_needs_migration() {
let temp_dir = TempDir::new().unwrap();
let project_dir = temp_dir.path();
assert!(!needs_migration(project_dir));
let manifest =
SavedObjectsManifest::with_objects(vec![SavedObject::new("dashboard", "test-1")]);
manifest.write(project_dir.join("manifest.json")).unwrap();
assert!(needs_migration(project_dir));
std::fs::create_dir_all(project_dir.join("manifest")).unwrap();
manifest
.write(project_dir.join("manifest/saved_objects.json"))
.unwrap();
assert!(!needs_migration(project_dir));
}
#[test]
fn test_migrate_with_backup() {
let temp_dir = TempDir::new().unwrap();
let project_dir = temp_dir.path();
let manifest = SavedObjectsManifest::with_objects(vec![
SavedObject::new("dashboard", "test-1"),
SavedObject::new("visualization", "test-2"),
]);
manifest.write(project_dir.join("manifest.json")).unwrap();
let result = migrate_manifest(project_dir, true).unwrap();
match result {
MigrationResult::MigratedWithBackup(backup_path) => {
assert!(backup_path.exists());
assert_eq!(backup_path, project_dir.join("manifest.json.backup"));
}
_ => panic!("Expected MigratedWithBackup result"),
}
let new_manifest =
SavedObjectsManifest::read(project_dir.join("manifest/saved_objects.json")).unwrap();
assert_eq!(new_manifest.count(), 2);
assert!(new_manifest.contains("dashboard", "test-1"));
assert!(new_manifest.contains("visualization", "test-2"));
assert!(!project_dir.join("manifest.json").exists());
}
#[test]
fn test_migrate_without_backup() {
let temp_dir = TempDir::new().unwrap();
let project_dir = temp_dir.path();
let manifest =
SavedObjectsManifest::with_objects(vec![SavedObject::new("dashboard", "test-1")]);
manifest.write(project_dir.join("manifest.json")).unwrap();
let result = migrate_manifest(project_dir, false).unwrap();
assert_eq!(result, MigrationResult::MigratedWithoutBackup);
assert!(project_dir.join("manifest/saved_objects.json").exists());
assert!(!project_dir.join("manifest.json").exists());
assert!(!project_dir.join("manifest.json.backup").exists());
}
#[test]
fn test_migrate_no_legacy_manifest() {
let temp_dir = TempDir::new().unwrap();
let project_dir = temp_dir.path();
let result = migrate_manifest(project_dir, true).unwrap();
assert_eq!(result, MigrationResult::NoLegacyManifest);
}
#[test]
fn test_migrate_already_migrated() {
let temp_dir = TempDir::new().unwrap();
let project_dir = temp_dir.path();
let manifest =
SavedObjectsManifest::with_objects(vec![SavedObject::new("dashboard", "test-1")]);
manifest.write(project_dir.join("manifest.json")).unwrap();
std::fs::create_dir_all(project_dir.join("manifest")).unwrap();
manifest
.write(project_dir.join("manifest/saved_objects.json"))
.unwrap();
let result = migrate_manifest(project_dir, true).unwrap();
assert_eq!(result, MigrationResult::AlreadyMigrated);
}
#[test]
fn test_load_saved_objects_manifest_new_location() {
let temp_dir = TempDir::new().unwrap();
let project_dir = temp_dir.path();
std::fs::create_dir_all(project_dir.join("manifest")).unwrap();
let manifest =
SavedObjectsManifest::with_objects(vec![SavedObject::new("dashboard", "new-1")]);
manifest
.write(project_dir.join("manifest/saved_objects.json"))
.unwrap();
let loaded = load_saved_objects_manifest(project_dir).unwrap();
assert_eq!(loaded.count(), 1);
assert!(loaded.contains("dashboard", "new-1"));
}
#[test]
fn test_load_saved_objects_manifest_legacy_location() {
let temp_dir = TempDir::new().unwrap();
let project_dir = temp_dir.path();
let manifest =
SavedObjectsManifest::with_objects(vec![SavedObject::new("dashboard", "legacy-1")]);
manifest.write(project_dir.join("manifest.json")).unwrap();
let loaded = load_saved_objects_manifest(project_dir).unwrap();
assert_eq!(loaded.count(), 1);
assert!(loaded.contains("dashboard", "legacy-1"));
}
#[test]
fn test_load_saved_objects_manifest_prefers_new() {
let temp_dir = TempDir::new().unwrap();
let project_dir = temp_dir.path();
let legacy =
SavedObjectsManifest::with_objects(vec![SavedObject::new("dashboard", "legacy-1")]);
legacy.write(project_dir.join("manifest.json")).unwrap();
std::fs::create_dir_all(project_dir.join("manifest")).unwrap();
let new = SavedObjectsManifest::with_objects(vec![
SavedObject::new("dashboard", "new-1"),
SavedObject::new("visualization", "new-2"),
]);
new.write(project_dir.join("manifest/saved_objects.json"))
.unwrap();
let loaded = load_saved_objects_manifest(project_dir).unwrap();
assert_eq!(loaded.count(), 2);
assert!(loaded.contains("dashboard", "new-1"));
assert!(loaded.contains("visualization", "new-2"));
}
#[test]
fn test_load_saved_objects_manifest_not_found() {
let temp_dir = TempDir::new().unwrap();
let project_dir = temp_dir.path();
let result = load_saved_objects_manifest(project_dir);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("No saved objects manifest found")
);
}
#[tokio::test]
#[serial_test::serial]
async fn test_needs_migration_unified() {
unsafe {
std::env::set_var("KIBANA_URL", "http://localhost:5601");
}
let temp_dir = TempDir::new().unwrap();
let project_dir = temp_dir.path();
assert!(!needs_migration_unified(project_dir));
let manifest =
SavedObjectsManifest::with_objects(vec![SavedObject::new("dashboard", "test-1")]);
manifest.write(project_dir.join("manifest.json")).unwrap();
assert!(needs_migration_unified(project_dir));
let result = migrate_to_multispace_unified(project_dir, false, None::<&Path>)
.await
.unwrap();
match result {
MigrationResult::MigratedWithoutBackup => {}
_ => panic!("Expected MigratedWithoutBackup result"),
}
assert!(!needs_migration_unified(project_dir));
assert!(
project_dir
.join("default/manifest/saved_objects.json")
.exists()
);
assert!(project_dir.join("default").exists());
}
#[tokio::test]
#[serial_test::serial]
async fn test_migrate_unified_from_legacy() {
unsafe {
std::env::set_var("KIBANA_URL", "http://localhost:5601");
}
let temp_dir = TempDir::new().unwrap();
let project_dir = temp_dir.path();
let manifest = SavedObjectsManifest::with_objects(vec![
SavedObject::new("dashboard", "test-1"),
SavedObject::new("visualization", "test-2"),
]);
manifest.write(project_dir.join("manifest.json")).unwrap();
let objects_dir = project_dir.join("objects");
std::fs::create_dir_all(&objects_dir).unwrap();
std::fs::write(objects_dir.join("test-1.dashboard.json"), "{}").unwrap();
let result = migrate_to_multispace_unified(project_dir, true, None::<&Path>)
.await
.unwrap();
match result {
MigrationResult::MigratedWithBackup(backup_path) => {
assert!(backup_path.exists());
assert_eq!(backup_path, project_dir.join("manifest.json.backup"));
}
_ => panic!("Expected MigratedWithBackup result"),
}
let new_manifest = project_dir.join("default/manifest/saved_objects.json");
assert!(new_manifest.exists());
let loaded = SavedObjectsManifest::read(&new_manifest).unwrap();
assert_eq!(loaded.count(), 2);
assert!(project_dir.join("default/objects").exists());
}
#[tokio::test]
#[serial_test::serial]
async fn test_migrate_unified_from_v0_1_0() {
unsafe {
std::env::set_var("KIBANA_URL", "http://localhost:5601");
}
let temp_dir = TempDir::new().unwrap();
let project_dir = temp_dir.path();
let manifest_dir = project_dir.join("manifest");
std::fs::create_dir_all(&manifest_dir).unwrap();
let manifest =
SavedObjectsManifest::with_objects(vec![SavedObject::new("dashboard", "test-1")]);
manifest
.write(manifest_dir.join("saved_objects.json"))
.unwrap();
let objects_dir = project_dir.join("objects");
std::fs::create_dir_all(&objects_dir).unwrap();
let result = migrate_to_multispace_unified(project_dir, false, None::<&Path>)
.await
.unwrap();
match result {
MigrationResult::MigratedWithoutBackup => {}
_ => panic!("Expected MigratedWithoutBackup result"),
}
assert!(
project_dir
.join("default/manifest/saved_objects.json")
.exists()
);
assert!(project_dir.join("default/objects").exists());
assert!(!project_dir.join("manifest/saved_objects.json").exists());
}
#[tokio::test]
#[serial_test::serial]
async fn test_migrate_unified_already_migrated() {
let temp_dir = TempDir::new().unwrap();
let project_dir = temp_dir.path();
let new_manifest_dir = project_dir.join("default/manifest");
std::fs::create_dir_all(&new_manifest_dir).unwrap();
let manifest =
SavedObjectsManifest::with_objects(vec![SavedObject::new("dashboard", "test-1")]);
manifest
.write(new_manifest_dir.join("saved_objects.json"))
.unwrap();
let result = migrate_to_multispace_unified(project_dir, false, None::<&Path>)
.await
.unwrap();
match result {
MigrationResult::AlreadyMigrated => {}
_ => panic!("Expected AlreadyMigrated result"),
}
}
#[tokio::test]
#[serial_test::serial]
async fn test_migrate_space_aware() {
unsafe {
std::env::set_var("KIBANA_URL", "http://localhost:5601");
}
let temp_dir = TempDir::new().unwrap();
let project_dir = temp_dir.path();
let manifest =
SavedObjectsManifest::with_objects(vec![SavedObject::new("dashboard", "test-1")]);
manifest.write(project_dir.join("manifest.json")).unwrap();
unsafe {
std::env::set_var("kibana_space", "marketing");
}
let result = migrate_to_multispace_unified(project_dir, false, None::<&Path>)
.await
.unwrap();
assert!(matches!(result, MigrationResult::MigratedWithoutBackup));
assert!(
project_dir
.join("marketing/manifest/saved_objects.json")
.exists()
);
assert!(!project_dir.join("default").exists());
unsafe {
std::env::remove_var("kibana_space");
}
}
}