Skip to content

Fallout.Reconcile — generic plan/apply primitive for CD targets #249

Description

@ChrisonSimtian

Motivation

Building CD for ERP for Factory Games as a real-world Fallout-as-CD dogfood (ErpForFactoryGames#265), we hand-rolled the same shape every CD target eventually needs:

  • Fetch current state from the remote.
  • Diff against desired state.
  • Render the plan (dry-run, structured + tabular).
  • Optionally apply.

Concretely: ResourcePlan, FieldChange, PlanAction, PlanRenderer, plus per-resource reconcilers (TunnelReconciler, DnsRecordReconciler, IngressReconciler) — ~250 LOC of pure scaffolding before any Cloudflare-specific code. The next CD target (K8s manifests, Docker compose, IAM policies, on-disk config) will hand-roll a sibling because the primitives aren't upstream.

Proposal

A Fallout.Reconcile namespace with the generic primitives:

public abstract record PlanAction { Create, Update, Delete, NoChange }
public sealed record FieldChange(string Field, string? Before, string? After);
public sealed record ResourcePlan<T>(string ResourceKind, string Identifier, PlanAction Action, IReadOnlyList<FieldChange> Changes, T Current, T Desired);

public interface IReconciler<T> {
    Task<IReadOnlyList<ResourcePlan<T>>> PlanAsync(T desired, CancellationToken ct);
    Task ApplyAsync(IReadOnlyList<ResourcePlan<T>> plans, CancellationToken ct);
}

public static class PlanRenderer {
    public static void RenderTable(IEnumerable<ResourcePlan> plans, IAnsiConsole console);
    public static string RenderJson(IEnumerable<ResourcePlan> plans);
}

A target composes reconcilers with the existing [Parameter] DryRun machinery — ./build.sh Provision --dry-run becomes one-liner glue instead of 250 LOC of scaffolding.

Why this fits Fallout (not a separate library)

Open questions

  • Diff fidelity across resource types. Cloudflare is easy (clean GET API). K8s, IAM, Terraform-state have defaults, server-side mutation, ordering quirks. The API needs an escape hatch (custom IDiffStrategy<T>?) without becoming Cloudflare-only.
  • T vs untyped state. Typed records are ergonomic for known shapes (DNS records, tunnels) but awkward for opaque server state (K8s objects, IAM docs). Generic over T, or over JsonNode / IDictionary<string, object?> with optional typed views?
  • Apply ordering. Plans apply in production order today. Real CD needs ordering hints (tunnel before DNS for cloudflared). First-class DependsOn, or leave it to caller-side composition?

Related

Reference implementation (to be replaced)

ErpForFactoryGames/src/Deploy/Erp.Deploy/Reconcile/ — the hand-rolled version we'd delete once this lands upstream.

Metadata

Metadata

Assignees

No one assigned

    Labels

    RFCDesign discussion / RFC. Comment with feedback; consensus shapes the implementation.enhancementNew feature or requesttarget/vNextTargets the next calendar-version

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions