///|
pub(all) struct GrowthPolicy {
format_version : Int
maximum_net_growth_bytes : Int64
maximum_added_files : Int
maximum_removed_files : Int
maximum_changed_files : Int
maximum_single_file_growth_bytes : Int64
protected_patterns : Array[String]
} derive(Eq, Debug, ToJson, FromJson)
///|
pub(all) enum GrowthViolationKind {
NetGrowthExceeded
AddedFilesExceeded
RemovedFilesExceeded
ChangedFilesExceeded
SingleFileGrowthExceeded
ProtectedPathChanged
} derive(Eq, Debug, ToJson, FromJson)
///|
pub(all) struct GrowthViolation {
kind : GrowthViolationKind
path : String
actual : Int64
limit : Int64
message : String
} derive(Eq, Debug, ToJson, FromJson)
///|
pub(all) struct GrowthPolicyResult {
net_growth_bytes : Int64
added_files : Int
removed_files : Int
changed_files : Int
largest_file_growth_bytes : Int64
largest_file_growth_path : String
violations : Array[GrowthViolation]
} derive(Eq, Debug, ToJson, FromJson)
///|
pub fn GrowthPolicy::default() -> GrowthPolicy {
GrowthPolicy::{
format_version: 1,
maximum_net_growth_bytes: 512L * 1024L * 1024L,
maximum_added_files: 10000,
maximum_removed_files: 10000,
maximum_changed_files: 10000,
maximum_single_file_growth_bytes: 256L * 1024L * 1024L,
protected_patterns: ["src/*", "LICENSE", "README*"],
}
}
///|
pub fn validate_growth_policy(policy : GrowthPolicy) -> Array[String] {
let issues : Array[String] = []
if policy.format_version != 1 {
issues.push("Unsupported growth policy version.")
}
if policy.maximum_net_growth_bytes < 0L {
issues.push("Maximum net growth cannot be negative.")
}
if policy.maximum_added_files < 0 {
issues.push("Maximum added-file count cannot be negative.")
}
if policy.maximum_removed_files < 0 {
issues.push("Maximum removed-file count cannot be negative.")
}
if policy.maximum_changed_files < 0 {
issues.push("Maximum changed-file count cannot be negative.")
}
if policy.maximum_single_file_growth_bytes < 0L {
issues.push("Maximum single-file growth cannot be negative.")
}
for pattern in policy.protected_patterns {
if pattern.trim().is_empty() {
issues.push("Protected path patterns cannot be empty.")
}
}
issues
}
///|
fn growth_violation(
kind : GrowthViolationKind,
path : String,
actual : Int64,
limit : Int64,
message : String,
) -> GrowthViolation {
GrowthViolation::{ kind, path, actual, limit, message, }
}
///|
fn changed_regular_file(change : EntryChange) -> Bool {
match (change.before, change.after) {
(Some(before), _) if before.is_file() => true
(_, Some(after)) if after.is_file() => true
_ => false
}
}
///|
pub fn evaluate_growth_policy(
before : Snapshot,
after : Snapshot,
policy : GrowthPolicy,
) -> GrowthPolicyResult raise {
let policy_issues = validate_growth_policy(policy)
if !policy_issues.is_empty() {
raise Failure("Invalid growth policy: " + policy_issues[0])
}
let diff = compare_snapshots(before, after)
let added = diff.added_count()
let removed = diff.removed_count()
let changed = diff.changed_count()
let violations : Array[GrowthViolation] = []
if diff.net_size > policy.maximum_net_growth_bytes {
violations.push(
growth_violation(
NetGrowthExceeded,
"",
diff.net_size,
policy.maximum_net_growth_bytes,
"Net storage growth exceeded the configured limit.",
),
)
}
if added > policy.maximum_added_files {
violations.push(
growth_violation(
AddedFilesExceeded,
"",
added.to_int64(),
policy.maximum_added_files.to_int64(),
"Added-file count exceeded the configured limit.",
),
)
}
if removed > policy.maximum_removed_files {
violations.push(
growth_violation(
RemovedFilesExceeded,
"",
removed.to_int64(),
policy.maximum_removed_files.to_int64(),
"Removed-file count exceeded the configured limit.",
),
)
}
if changed > policy.maximum_changed_files {
violations.push(
growth_violation(
ChangedFilesExceeded,
"",
changed.to_int64(),
policy.maximum_changed_files.to_int64(),
"Changed-file count exceeded the configured limit.",
),
)
}
let mut largest_growth = 0L
let mut largest_path = ""
for change in diff.changes {
if changed_regular_file(change) && change.size_delta > largest_growth {
largest_growth = change.size_delta
largest_path = change.path
}
if changed_regular_file(change) &&
change.size_delta > policy.maximum_single_file_growth_bytes {
violations.push(
growth_violation(
SingleFileGrowthExceeded,
change.path,
change.size_delta,
policy.maximum_single_file_growth_bytes,
"A file grew beyond the configured per-file limit.",
),
)
}
if changed_regular_file(change) &&
matches_any_pattern(change.path, policy.protected_patterns) {
violations.push(
growth_violation(
ProtectedPathChanged,
change.path,
change.size_delta,
0L,
"A protected path changed between snapshots.",
),
)
}
}
GrowthPolicyResult::{
net_growth_bytes: diff.net_size,
added_files: added,
removed_files: removed,
changed_files: changed,
largest_file_growth_bytes: largest_growth,
largest_file_growth_path: largest_path,
violations,
}
}
///|
pub fn render_growth_policy_result(result : GrowthPolicyResult) -> String {
let lines : Array[String] = [
"MoonDisk growth policy check",
"Net growth: " + format_bytes(result.net_growth_bytes),
"Added files: " + result.added_files.to_string(),
"Removed files: " + result.removed_files.to_string(),
"Changed files: " + result.changed_files.to_string(),
"Largest file growth: " +
format_bytes(result.largest_file_growth_bytes) +
(if result.largest_file_growth_path == "" {
""
} else {
" (" + result.largest_file_growth_path + ")"
}),
"Violations: " + result.violations.length().to_string(),
]
for violation in result.violations {
let location = if violation.path == "" {
""
} else {
" [" + violation.path + "]"
}
lines.push("- " + violation.message + location)
}
if result.violations.is_empty() {
lines.push("Result: approved.")
} else {
lines.push("Result: rejected.")
}
lines.join("\n")
}
///|
pub fn growth_policy_to_json(policy : GrowthPolicy) -> String {
ToJson::to_json(policy).stringify(indent=2)
}
///|
pub fn growth_policy_from_json(source : String) -> GrowthPolicy raise {
let policy : GrowthPolicy = @json.from_json(@json.parse(source))
let issues = validate_growth_policy(policy)
if !issues.is_empty() {
raise Failure("Invalid growth policy: " + issues[0])
}
policy
}
///|
pub fn growth_policy_result_to_json(result : GrowthPolicyResult) -> String {
ToJson::to_json(result).stringify(indent=2)
}
///|
pub async fn write_growth_policy(path : String, policy : GrowthPolicy) -> Unit {
@fs.write_file(path, growth_policy_to_json(policy))
}
///|
pub async fn read_growth_policy(path : String) -> GrowthPolicy {
growth_policy_from_json(@fs.read_file(path).text())
}