Skip to content
Open
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
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ path = "tests/functional_tests.rs"
[workspace]
members = [
"libazureinit",
"libazureinit-kvp",
]

[features]
Expand Down
126 changes: 0 additions & 126 deletions doc/libazurekvp.md

This file was deleted.

20 changes: 20 additions & 0 deletions libazureinit-kvp/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[package]
name = "libazureinit-kvp"
version = "0.1.0"
edition = "2021"
rust-version = "1.88"
repository = "https://github.com/Azure/azure-init/"
homepage = "https://github.com/Azure/azure-init/"
license = "MIT"
description = "Hyper-V KVP (Key-Value Pair) storage library for azure-init."

[dependencies]
fs2 = "0.4"
sysinfo = "0.38"

[dev-dependencies]
tempfile = "3"

[lib]
name = "libazureinit_kvp"
path = "src/lib.rs"
134 changes: 134 additions & 0 deletions libazureinit-kvp/src/azure.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

//! Azure-specific KVP store.
//!
//! Wraps [`HyperVKvpStore`] with the stricter value-size limit imposed
//! by the Azure host (1,022 bytes). All other behavior — record
//! format, file locking, append-only writes — is inherited from the
//! underlying Hyper-V pool file implementation.

use std::collections::HashMap;
use std::path::{Path, PathBuf};

use crate::hyperv::HyperVKvpStore;
use crate::{KvpError, KvpStore};

/// Azure host-side value limit (values beyond this are truncated).
const AZURE_MAX_VALUE_BYTES: usize = 1022;

/// Azure KVP store backed by a Hyper-V pool file.
///
/// Identical to [`HyperVKvpStore`] except that
/// [`MAX_VALUE_SIZE`](KvpStore::MAX_VALUE_SIZE) is set to 1,022 bytes,
/// matching the Azure host's truncation behavior.
#[derive(Clone, Debug)]
pub struct AzureKvpStore {
inner: HyperVKvpStore,
}

impl AzureKvpStore {
/// Create a new Azure KVP store backed by the pool file at `path`.
///
/// When `truncate_on_stale` is `true` the constructor checks
/// whether the pool file predates the current boot and, if so,
/// truncates it before returning.
pub fn new(
path: impl Into<PathBuf>,
truncate_on_stale: bool,
) -> Result<Self, KvpError> {
Ok(Self {
inner: HyperVKvpStore::new(path, truncate_on_stale)?,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, I think this composition setup works super well! This is the intended replacement for inheritance for Rust. My only request is that I would recommend updating inner to be a more descriptive term, maybe kvp_store, since I do not see many official references to using the inner name. This could get confusing if we ever extend this further.
https://trpl.rantai.dev/docs/part-iii/chapter-20/#2043-implementing-composition-in-rust

For example, if AzureKvpStore were ever put into a separate composition, the pattern of calling the composed object inner could remain and would result in a ExtendedAzureKvpStore.inner.inner.path calling, instead of ExtendedAzureKvpStore.azure_kvp_store.kvp_store.path which strikes me as more verbose.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Big fan of this - will make this change!

})
}

/// Return a reference to the pool file path.
pub fn path(&self) -> &Path {
self.inner.path()
}
}

impl KvpStore for AzureKvpStore {
const MAX_KEY_SIZE: usize = HyperVKvpStore::MAX_KEY_SIZE;
const MAX_VALUE_SIZE: usize = AZURE_MAX_VALUE_BYTES;

fn backend_read(&self, key: &str) -> Result<Option<String>, KvpError> {
self.inner.backend_read(key)
}

fn backend_write(&self, key: &str, value: &str) -> Result<(), KvpError> {
self.inner.backend_write(key, value)
}

fn entries(&self) -> Result<HashMap<String, String>, KvpError> {
self.inner.entries()
}

fn entries_raw(&self) -> Result<Vec<(String, String)>, KvpError> {
self.inner.entries_raw()
}

fn delete(&self, key: &str) -> Result<bool, KvpError> {
self.inner.delete(key)
}

fn backend_clear(&self) -> Result<(), KvpError> {
self.inner.backend_clear()
}

fn is_stale(&self) -> Result<bool, KvpError> {
self.inner.is_stale()
}
}

#[cfg(test)]
mod tests {
use super::*;
use tempfile::NamedTempFile;

fn azure_store(path: &Path) -> AzureKvpStore {
AzureKvpStore::new(path, false).unwrap()
}

#[test]
fn test_azure_rejects_value_over_1022() {
let tmp = NamedTempFile::new().unwrap();
let store = azure_store(tmp.path());

let value = "V".repeat(AZURE_MAX_VALUE_BYTES + 1);
let err = store.write("k", &value).unwrap_err();
assert!(
matches!(err, KvpError::ValueTooLarge { .. }),
"expected ValueTooLarge, got: {err}"
);
}

#[test]
fn test_azure_accepts_value_at_1022() {
let tmp = NamedTempFile::new().unwrap();
let store = azure_store(tmp.path());

let value = "V".repeat(AZURE_MAX_VALUE_BYTES);
store.write("k", &value).unwrap();
assert_eq!(store.read("k").unwrap(), Some(value));
}

#[test]
fn test_azure_write_and_read() {
let tmp = NamedTempFile::new().unwrap();
let store = azure_store(tmp.path());

store.write("key", "value").unwrap();
assert_eq!(store.read("key").unwrap(), Some("value".to_string()));
}

#[test]
fn test_azure_clear() {
let tmp = NamedTempFile::new().unwrap();
let store = azure_store(tmp.path());

store.write("key", "value").unwrap();
store.clear().unwrap();
assert_eq!(store.read("key").unwrap(), None);
}
}
Loading