apply method
Moves the node to what request declares.
Implementation
Future<BlueprintApplyResult> apply(BlueprintApplyRequest request) async {
final resolved = request.blueprint;
final ledger = await ledgers.read(resolved.blueprint.value);
final reading = await _read(resolved);
final notes = [...reading.notes];
final planned = _diff(
resolved,
reading.states,
ledger,
notes,
purgeAdopted: request.purgeAdopted,
);
// A dry run stops here, having run exactly the code a real one runs up to
// this point. A dry run that took a different path would be a dry run that
// lies about what the real one will do.
if (request.dryRun) {
return BlueprintApplyResult(
requestId: request.requestId,
success: true,
changes: planned,
appliedHash: ledger?.hash ?? '',
notes: [...notes, 'dry run: nothing was changed'],
);
}
// Adoption is a fact about history, not about this run: a resource is
// adopted if it was already correct the **first** time this blueprint saw
// it, which is why the ledger is consulted before the reading is.
//
// Re-deciding it every time would be wrong in the worst direction. A
// resource this blueprint installed is, by the second apply, "already
// correct" — so it would be marked adopted, and dropping it from the
// blueprint later would leave it behind forever.
final known = ledger?.entries ?? const <ResourceId, LedgerEntry>{};
final adopted = <ResourceId>{
for (final resource in resolved.resources)
if (!known.containsKey(resource.id) &&
(reading.states[resource.id]?.satisfies(resource.ensure) ?? false))
resource.id,
// Once adopted, always adopted: the machine's history does not change
// because a later apply touched something else.
for (final entry in known.values)
if (entry.adopted) entry.id,
};
final done = <ResourceId, ResourceChange>{};
final failed = <ResourceId>{};
for (final change in planned) {
// A resource whose dependency failed is not attempted, and says so. The
// branches that do not depend on the failure carry on: a blueprint of
// forty resources with one bad package should report the other
// thirty-nine, not stop at the third.
final declaration = _resourceFor(resolved, change.id);
final blocker = _blockedBy(declaration, failed);
if (blocker != null) {
done[change.id] = change.completed(
kind: ChangeKind.skipped,
reason: 'skipped: $blocker failed',
);
failed.add(change.id);
continue;
}
if (!change.kind.isWork) {
done[change.id] = change;
continue;
}
final applied = await _applyOne(
change,
declaration,
reading.states[change.id],
);
done[change.id] = applied;
if (applied.kind == ChangeKind.failed) failed.add(change.id);
}
final changes = [for (final change in planned) done[change.id] ?? change];
// A resource we tried to remove and could not is still on the machine, and
// so is still ours. It has to stay in the ledger or it is leaked: see
// `Ledger.recording`.
final unremoved = {
for (final change in planned)
if (change.kind == ChangeKind.remove &&
done[change.id]?.kind != ChangeKind.remove)
change.id,
};
// The ledger records what was *declared*, not what succeeded: a resource
// whose install failed is still this system's responsibility, and forgetting
// it would orphan whatever half of it landed.
await ledgers.write(
(ledger ?? Ledger.empty(resolved.blueprint, clock.now())).recording(
resolved,
clock.now(),
adopted: request.purgeAdopted ? const {} : adopted,
states: reading.states,
retain: unremoved,
),
);
return BlueprintApplyResult(
requestId: request.requestId,
success: failed.isEmpty,
changes: changes,
appliedHash: resolved.hash,
notes: notes,
);
}