If you're interacting with stateful systems (which you usually are with this kind of command), --dry-run can still have a race condition.
The tool tells you what it would do in the current situation, you take a look and confirm that that's alright. Then you run it again without --dry-run, in a potentially different situation.
That's why I prefer Terraform's approach of having a "plan" mode. It doesn't just tell you what it would do but does so in the form of a plan it can later execute programmatically. Then, if any of the assumptions made during planning have changed, it can abort and roll back.
As a nice bonus, this pattern gives a good answer to the problem of having "if dry_run:" sprinkled everywhere: You have to separate the planning and execution in code anyway, so you can make the "just apply immediately" mode simply execute(plan()).
>That's why I prefer Terraform's approach of having a "plan" mode. It doesn't just tell you what it would do but does so in the form of a plan it can later execute programmatically. Then, if any of the assumptions made during planning have changed, it can abort and roll back.
Not to take anything away from your comment but just to add a related story... the previous big AWS outage had an unforeseen race condition between their DNS planner vs DNS executor:
>[...] Right before this event started, one DNS Enactor experienced unusually high delays needing to retry its update on several of the DNS endpoints. As it was slowly working through the endpoints, several other things were also happening. First, the DNS Planner continued to run and produced many newer generations of plans. Second, one of the other DNS Enactors then began applying one of the newer plans and rapidly progressed through all of the endpoints. The timing of these events triggered the latent race condition. When the second Enactor (applying the newest plan) completed its endpoint updates, it then invoked the plan clean-up process, which identifies plans that are significantly older than the one it just applied and deletes them. At the same time that this clean-up process was invoked, the first Enactor (which had been unusually delayed) applied its much older plan to the regional DDB endpoint, overwriting the newer plan. The check that was made at the start of the plan application process, which ensures that the plan is newer than the previously applied plan, was stale by this time due to the unusually high delays in Enactor processing. [...]
Overkill I’m sure for many things but I’m curious as to whether there’s a TLA kind of solution for this sort of thing. It feels like it could although it depends how well modelled things are (also aware this is a 30s thought and lots of better qualified people work on this full time).
This is why I think things like devops benefit from the traditional computer science education. Once you see the pattern, whatever project you were assigned looks like something you've done before. And your users will appreciate the care and attention.
Yeah any time you're translating "user args" and "system state" to actions + execution and supporting a "dry run" preview it seems like you only really have two options: the "ad-hoc quick and dirty informal implementation", or the "let's actually separate the planning and assumption checking and state checking from the execution" design.
I was thinking that he's describing implementing an initial algebra for a functor (≈AST) and an F-Algebra for evaluation. But I guess those are different words for the same things.
I like that idea! For an application like Terraform, Ansible or the like, it seems ideal.
For something like in the article, I’m pretty sure a plan mode is overkill though.
Planning mode must involve making a domain specific language or data structure of some sort, which the execution mode will interpret and execute. I’m sure it would add a lot of complexity to a reporting tool where data is only collected once per day.
No need to overthink it. In any semi-modern language you can (de)serialize anything to and from JSON, so it's really not that hard. The only thing you need to do is have a representation for the plan in your program. Which I will argue is probably the least error-prone way to implement --dry-run anyway (as opposed to sprinkling branches everywhere).
> you can (de)serialize anything to and from JSON, so it's really not that hard
First, it is hard, especially in at least somewhat portable manner.
Second, serialization only matters if you cannot (storage, IPC) pass data around in-memory anyway. That's not the problem raised, though. Whatever the backing implementation, the plan, ultimately, consists of some instructions (verbs in parent) over objects (arguments in parent). Serializing instructions any other way than dropping non-portable named references requires one to define execution language, which is not an easy feat.
> The only thing you need to do is have a representation for the plan in your program.
That "only" is doing lifting heavier than you probably realize. Such representation, which is by the way specified to be executable bidirectionally (roll back capabilities), is a full blown program, so you end up implementing language spec, godegen and execution engines. In cases of relatively simple business models that is going to be the majority of the engineering effort.
> First, it is hard, especially in at least somewhat portable manner.
I'm curious what portability concerns you've run into with JSON serialization. Unless you need to deal with binary data for some reason, I don't immediately see an issue.
> Such representation, which is by the way specified to be executable bidirectionally (roll back capabilities), is a full blown program
Of course this depends on the complexity of your problem, but I'd imagine this could be as simple as a few configuration flags for some problems. You have a function to execute the process that takes the configuration and a function to roll back that takes the same configuration. This does tie the representation very closely to the program itself so it doesn't work if you want to be able to change the program and have previously generated "plans" continue to work.
> I'm curious what portability concerns you've run into with JSON serialization.
The hard part concerns instructions and it is not technical implementation of serializing an in-memory data structures into serialization format (be it JSON or something bespoke) that is the root of complexity.
> You have a function to execute the process that takes the configuration and a function to roll back that takes the same configuration.
Don't forget granularity and state tracking. The opposite of a seemingly simple operation like "set config option foo to bar" is not a straightforward inverse: you need to track the previous value. Does the dry run stop at computing the final value for foo and leaves possible access control issues to surface during real run or does it perform "write nothing" operation to catch those?
> This does tie the representation very closely to the program itself so it doesn't work if you want to be able to change the program and have previously generated "plans" continue to work.
Why serialize then? Dump everything into one process space and call the native functions. Serialization implies either strictly, out of band controlled interfaces, which is a fragile implementation of codegen+interpreter machinery.
Right, but you still have to define every ”verb” your plan will have, their ”arguments”, etc. Not need to write a parser (even Java can serialize/deserialize stuff), as you say, but you have to meta-engineer the tool. Not just script a series of commands.
It's not strictly related to the original theme, but I want to mention this.
Ansible implementation is okay, but not perfect (plus, this is difficult to implement properly). For cases like file changes, it works, but if you install a package and rely on it later, the --check command will fail. So I am finding myself adding conditions like "is this a --check run?"
Ansible is treated as an idempotent tool, which it's not. If I delete a package from the list, then it will pollute the system until I create a set of "tearing-down" jobs.
Yes! I'm currently working on a script that modifies a bunch of sensitive files, and this the approach I'm taking to make sure I don't accidentally lose any important data.
I've split the process into three parts:
1. Walk the filesystem, capture the current state of the files, and write out a plan to disk.
2. Make sure the state of the files from step 1 has not changed, then execute the plan. Capture the new state of the files. Additionally, log all operations to disk in a journal.
3. Validate that no data was lost or unexpectedly changed using the captured file state from steps 1 and 2. Manually look at the operations log (or dump it into an LLM) to make sure nothing looks off.
These three steps can be three separate scripts, or three flags to the same script.
I think it's configurable, but my experience with terraform is that by default when you `terraform apply` it refreshes state, which seems to be tantamount to running a new plan. i.e. its not simply executing whats in the plan, its effectively running a fresh plan and using that. The plan is more like a preview.
That is the default, but the correct (and poorly documented and supported) way to use terraform is to save the plan and re-use it when you apply. See the -out parameter to terraform plan, and then never apply again without it.
The snippet which demonstrates the plan-then-execute pattern I have is this:
def gather(paths):
files = []
for pattern in paths:
files.extend(glob.glob(pattern))
return files
def execute(files):
for f in files:
os.remove(f)
files = gather([os.path.join(tmp_dir, "*.txt")])
if dryrun:
print(f"Would remove: {files}")
else:
execute(files)
I introduced dry-run at my company and I've been happy to see it spread throughout the codebase, because it's a coding practice that more than pays for itself.
> That's why I prefer Terraform's approach of having a "plan" mode. It doesn't just tell you what it would do but does so in the form of a plan it can later execute programmatically. Then, if any of the assumptions made during planning have changed, it can abort and roll back.
And how do you imagine doing that for the "rm" command?
Really? Should I be snapshotting the volume before every "rm"? Even if it's a part of routine file exchanges between machines? (As it happens for many production lines, especially older ones).
I think the current semantic of "rm" works fine. But I understand the new world where we'll perhaps gonna be deleting single files using Terraform or cluster of machines, or possibly LLMs/AI agents.
Oh, I don't think we should change the semantics of "rm"--not because reversibility is unimportant (shameless self-promotion: it is: https://blog.zacbentley.com/post/on-reversibility/), but because baking it into "rm" is the wrong layer.
Folks usually want reversibility in the context of a logical set of operations, like a Terraform apply. For shell commands like "rm", the logical set of operations might be a session (having a ZFS snapshot taken on terminal session start, with a sane auto-delete/age-out rotation, would be super useful! I might script that up in my shell profile in fact) or a script, or a task/prompt/re-prompt of an AI agent. But yeah, it definitely shouldn't happen at the level of a singular "rm" call.
Since filesystem snapshots (in most snapshot-capable filesystems, not just ZFS) are very simple to create, and are constant-time or otherwise extremely fast to perform, the overhead of taking this approach wouldn't be too hard.
I had a similar (but not as good) thought which was to separate out the action from the planning in code then inject the action system. So —-dry-run would pass the ConsoleOutput() action interface but without it passes a LiveExecutor() (I’m sure there’s a better name).
Assuming our system is complex enough. I guess it sits between if dry_run and execute(plan()) in its complexity.
The article names a lot of other things that AI is being used for besides scamming the elderly, such as making us distrust everything we see online, generating sexually explicit pictures of women without their consent, stealing all kinds of copyrighted material, driving autonomous killer drones and more generally sucking the joy out of everything.
And I think I'm inclined to agree. There are a small amount of things that have gotten better due to AI (certain kinds of accessibility tech) and a huge pile of things that just suck now. The internet by comparison feels like a clear net positive to me, even with all the bad it enables.
Here's the thing with AI, especially as it becomes more AGI like, it will encompass all human behaviors. This will lead to the bad behaviors becoming especially noticeable since bad actors quickly realized this is a force multiplication factor for them.
This is something everyone needs to think about when discussing AI safety. Even ANI applications carry a lot of potential societal risks and they may not be immediately evident. I know with the information superhighway few expected it to turn into a dopamine drip feed for advertising dollars, yet here we are.
> bad actors quickly realized this is a force multiplication factor for them
You'd think we would have learned this lesson in failing to implement email charges that net'd to $0 for balanced send/receive patterns. And thereby heralded in a couple decades of spam, only eventually solved by centralization (Google).
Driving the cost of anything valuable to zero inevitably produces an infinite torrent of volume.
AI doesn't encompass any "human behaviours", the humans controlling it do. Grok doesn't generate nude pictures of women because it wants to, it does it because people tell it to and it has (or had) no instructions to the contrary
If it can generate porn, it can do so because it was explicitly trained on porn. Therefore the system was designed to generate porn. It can't just materialize a naked body without having seen millions of them. they do not work that way.
I hate to be a smartass, but do you read the stuff you type out?
>Grok doesn't generate nude pictures of women because it wants to,
I don't generate chunks of code because I want to. I do it because that's how I get paid and like to eat.
What's interesting with LLMs is they are more like human behaviors than any other software. First you can't tell non-AI (not just genAI)software to generate a picture of a naked women, it doesn't have that capability. So after that you have models that are trained on content such as naked people. I mean, that's something humans are trained on, unless we're blind I guess. If you take a data set encompassing all human behaviors, which we do, then the model will have human like behaviors.
It's in post training that we add instructions to the contrary. Much like if you live in American you're taught that seeing naked people is worse than murdering someone and that if someone creates a naked picture of you, your soul has been stolen. With those cultural biases programmed into you, you will find it hard to do things like paint a picture of a naked person as art. This would be openAI's models. And if you're a person that wanted to rebel, or lived in a culture that accepted nudity, then you wouldn't have a problem with it.
How many things do you do because society programmed you that way, and you're unable to think outside that programming?
These are one of those things that are hard to get statistics of due to the nature of the subject, but going to any website that features AI generated content like CivitAI will show you a lot more naked AI generated women than men, and that the images of women are greatly better in quality than the men. None of the people actually exist, of course, but some things stem from this:
1. There are probably AI portals that are OK with uploading nonconsensual sexual images of people. I am not about to go looking for those, but the ratio of women to men on those sites is likely similar.
2. The fact that the quality of women is better than the quality of men speaks to vastly more training being done on women
3. Because there's so much training on women it's just easier to use AI for nefarious purposes on women than men (have to find custom trained LORAs to get male anatomy right, for example).
I did try to look for statistics out of curiosity, but most just cite a number without evidence.
Obviously there is more interest in generating images of naked women, since naked women look better than naked men. It’s not some kind of patriarchal conspiracy.
It is obvious, but again that's subjective (I'm a straight male so of course I find it to be true but I'm not sure straight women would agree). The person I was responding to was asking if evidence existed, so I was curious to see if evidence did indeed exist.
In addition to AI-specific data, the existing volume and consumption patterns for non-AI pornography can be extrapolated to AI, I think, with high confidence.
>The internet by comparison feels like a clear net positive to me, even with all the bad it enables.
When I think of the internet, I think of malware, porn, social media manipulating people, flame wars, "influencers", and more.
It is also used to scam the elderly, sharing photoshopped sexually explicit pictures of men, women, and children, without their consent, stealing all kinds of copyrighted material, and definitely sucking the joy out of everything. Revenge porn wasn't started in 2023 with OpenAI. And just look at META's current case about Instagram being addicting and harmful to children. If "AI" is a tech carcinogen, then the internet is a nuclear reactor, spewing radioactive material every which way. But hey, it keeps the lights on! Clearly, a net positive.
Let's just be intellectually consistent, that's all I'm saying.
I actually love this about Linux. The syscall API is much better than libc (both the one defined by POSIX and libc as it actually exists on different Unixen). No errno (which requires weird and inefficient TLS bullshit), no hooks like atfork/atexit/etc., no locales, no silly non-reentrant functions using global variables, no dlopen/dlclose. Just the actual OS interface. Languages that aren't as braindead as C can have their own wrappers around this and skip all that nonsense.
Also, there are syscalls which are basically not possible to directly expose as C functions, because they mess with things that the C runtime considers invariant. An example would be `SYS_clone3`. This is an immensely useful syscall, and glibc uses it for spawning threads these days. But it cannot be called directly from C, you need platform-specific assembly code around it.
No system call can, you need a wrapper like syscall() provided by glibc. glibc also provides a dedicated wrapper for the clone system call which properly sets up the return address for the child thread. No idea what you're angry about
Sure, you need a tiny bit of asm to do the actual syscall. That's not what I'm talking about. Most syscalls are easy to wrap, clone is slightly harder but doable (as evidenced by glibc). clone3 is for all intents and purposes impossible to write a general C wrapper for. It allows you to create situations such as threads that share virtual memory but not file descriptors, or vice-versa. That is, it can leave the caller in a situation that violates core assumptions by libc.
The C library maintains its own set of file descriptors, which are mapped to the OS file descriptors (because the stdio file descriptors and the OS file descriptors have different types and different behaviors).
I do not know whether this is true, but perhaps the previous poster means that using clone3 with certain arguments may break this file descriptor mapping so invoking after that stdio functions may have unexpected results.
Also the state kept by the libc malloc may get confused after certain invocations of clone3, because it has memory pages that have been obtained through mmap or sbrk and which may sometimes be returned to the OS.
So libc certainly cares about the OS file descriptors and virtual memory mappings, because it maintains its own internal state, which has references to the corresponding OS state. I have not looked to see when an incorrect state can result after a clone3, but it is plausible that such cases may exist, so that glibc allows calling clone3 only with a restricted combination of arguments and it does not provide a wrapper that would allow other combinations of arguments.
Yes; this is why QEMU's user-space-emulation clone syscall handling restricts the caller to only those combinations of clone flags which match either "looks like fork()" or "looks like creating a new pthread", because QEMU itself is linked with the host libc and weird clone flag combinations will put the new process/thread into a state the libc isn't expecting.
All fair points. What do other languages' standard libraries do to walk around clone3 then? If two threads share file descriptors but not virtual memory, do they perform some kind of IPC to lock them for synchronizing reads and writes?
> What do other languages' standard libraries do to walk around clone3 then?
They don't offer generic clone3 wrappers either AFAIK. All the code I've seen that uses it - and a lot of it is not in standard libraries but in e.g. container runtime implementations - has its own special-purpose code around a specific way to call it.
My point is not that other standard libraries do it better, but that clone3 as a syscall interface is highly versatile, moreso than it could be as a function in either C or most other languages. That is, the syscall API is the right layer for this feature to be.
It's sad how much of this thread of supposed hackers comes from people who are simply parroting this dogma because it has been drilled into them. People were even preaching this before IPv6 privacy extensions came into use, either downplaying the privacy issues or outright telling people they were bad for wanting privacy because IPv6 is more important.
I understand the difference between NAT and firewall perfectly well. I have deployed and configured both for many years. The strawman of "NAT without firewall" is pretty much irrelevant, because that's not what people run IRL.
Firewalls are policy-based security, NAT is namespacing. In other fields, we consider namespacing an important security mechanism. If an attacker can't even name a resource they're not allowed to access, that's quite a strong security property.
And of course, anyone can spoof IP and try to send traffic to 192.168.0.6 or whatever. But if you're anywhere in the world other than right inside my ISP's access network, you can't actually get the internet to route this to my local 192.68.0.6. On the other hand, an IPv6 firewall is one misconfigured rule away from giving anybody on the planet access.
Yeah, I think it is a bit more subtle of an issue than this flamewar always descends into.
There's people upthread arguing that every cellphone in the country is on IPv6 and nobody worries about it, but I'm certain there are thousands of people getting paid salaries to worry about that for you.
Meanwhile, the problem is about the level of trust in the consumer grade router sitting on my desk over there. With IPv4 NAT it is more likely that the router will break in such a way that I won't be able to access the internet. Having NAT break in such a way that it accidentally port forwards all incoming connection attempts to my laptop sitting behind it is not a likely bug or failure mode. If it does happen, it would likely only happen to a single machine sitting behind it.
OTOH, if my laptop and every other machine on my local subnet has a public IPv6 address on it, then I'm trusting that consumer grade router to never break in such a way that the firewall default allows all for some reason--opening up every single machine on my local subnet and every single listening port. A default deny flipping to a default allow is absolutely the kind of security bug that really happens and would keep me awake at night. And even if I don't go messing around with it and screw it up myself, there's always the possibility that a software bug in a firmware upgrade causes the problem.
I'd like to know what the solution to this is, other than blind trust in the router/firewall manufacturer or setting up your own external monitoring (and testing that monitoring periodically).
Instead of just screaming about how "NAT ISN'T SECURITY" over and over, I'd like someone to just explain how to mitigate the security concerns of firewall rulesets--when so very many of us have seen firewall rulesets be misconfigured by "professionals" at our $DAYJOBs. Just telling me that every IPv6 router should have default deny rules and nobody would be that incompetent to sell a router that wouldn't be that insecure doesn't give me warm fuzzies.
I don't necessarily trust NAT more, but a random port forward rule for all ports appearing against a given target host behind it is going to be a much more unusual kind of bug than just having a default firewall rule flipped to allow.
You could set up a monitoring solution that alerts you if one of your devices is suddenly reachable from the internet via IPv6. It will probably never fire an alert but in your case might help you sleep better. IPv6 privacy extensions could help you too.
In practice I don't think it's really an issue. The IPv6 firewall will probably not break in a way that makes your device reachable from the internet. Even if it would, someone would have to know the IPv6 address of the device they want to target - which means that you have to connect to a system that they have control of first, otherwise it's unlikely they'll ever get it. Lastly, you'd have to run some kind of software on that device that has a vulnerability which can be exploited via network. Combine all that and it gets so unlikely that you'll get hacked this way that it's not worth worrying about.
Thank you. This is the first time that someone admits here that NAT actually adds some security. IPv4 will never go away less that an important share because of it's simplicity and NAT-level security it offers to millions of professionals and amateurs that tinker with their routers.
> SLAAC is more complex than IPv4 w/ NAT w/ DHCPv4? Serious?
Yes? Has this ever been in question?
Stateful DHCP provides a _reliable_ way to configure clients, while SLAAC is anything but. It's also insufficient in itself if you want to configure things like NTP servers.
But that's not the main issue. The main issue is that with SLAAC you are supposed to hand out real routable addresses. That are _not_ controlled by you, so the end devices need to be able to handle prefix withdrawals and deprecations. This can lead to your printer not working if your ISP connection goes down and it has no more active IPv6 prefixes.
So you also need a stable ULA. But even that is not a guarantee because source IP selection rules are, shall we say, not the best.
But wait, there's more! You can trivially load-balance/failover NAT-ed IPv4 network over two WAN connections. Now try to do that with IPv6. Go on. I'll wait.
Except in the real world everyone is also running UPnP, so NAT is also one misconfiguration away from exposing something publicly. In the real world your ISP might enable IPv6 one day and suddenly you do have a public address. Relying on NAT is a bad idea because it's less explicit, a firewall is saying you only want to allow these things through, of course nothing is perfect, you can mess up, but NAT is just less clear, the expectation is not "nothing behind NAT should ever be exposed", it's "we don't have enough addresses and need to share".
Sure, and that's fine, but relying on it isn't, and it isn't a reason not to use IPv6 (if you want namespacing, there are tools for that outside hiding behind a single IPv4). Hence the advice is not to rely on NAT.
This is people talking past each other, and to be fair, saying "everyone" in my post made it unclear, I was being glib in response to "because that's not what people run IRL", when evidently people do, I've seen it happen.
I think this is where the disconnect is: the home users are precisely the ones being talked about, because they are the ones most likely to be treating NAT like it is a security system for their devices in the real world.
I've literally seen someone's ISP turn on IPv6, and then have their long-running VNC service compromised because they were just relying on NAT to hide their services.
> If an attacker can't even name a resource they're not allowed to access, that's quite a strong security property.
This is entirely incorrect. An attacker can still name a resource, it only has to guess the right port number that is mapped to that resource.
That's how NAT fundamentally works after all, it allows you to use the additional 16-bits of the port number to extend the IP address space. Any blocking of incoming traffic on a port already mapped to a local address is a firewall rule.
The reason that it offers protection is because attackers aren't going to try every single port. Compared to that IPv6 will offer more protection as an attacker would have to guess the right address in a 64-bit namespace rather than just a 16-bit one.
But the part about the undersea cable is simply wrong! Major undersea cables have been disrupted several times and never has a "continent gone dark".
I think this betrays a severe misunderstanding of what the internet is. It is the most resilient computer network by a long shot, far more so than any of these toy meshes. For starters, none of them even manage to make any intercontinental connections except when themselves using the internet as their substrate.
Now of course, if you put all your stuff in a single organization's "cloud", you don't get to benefit from all that resilience. That sort of fragile architecture is rightly criticized but this falls flat as a criticism of the internet itself.
Car enthusiasts caring about the driving experience doesn't just mean drivability. Engine sound is a huge part of it. All the classic Porsche 911 have flat-6 engines which make a distinctive sound that is totally part of the brand.
FTR I don't care about this myself, I'm happy with my EV. But the importance of this aspect is easily missed by people not part of the target demographic.
It feels like engine sound has become more important to these people since EV's entered the market. I'm sure it was there before but not to the same extent.
The huge uproar about the 718 having a flat four turbo engine was mostly about the sound. (I don’t have a problem with it.) I think it has always been there.
It became more of a selling point as regulation came for it. OPF, stricter modification control, etc. Prior it didn't matter as much since it was always decent and you could do whatever you want to it. Now, a pops and bangs tune with a straight pipe will get your car impounded in most countries the first time a cop sees/hears you.
Weeeeelllll that was mainstream a long long time before they adopted it. And I'm still annoyed that the only devices with Lightning in our house are my Airpods en iPhone mini 12 and wife's iPhone 14 Pro.
Always need to attach an adapter to my Anker chargers and powerbanks.
It's funny, I was mad at them for getting rid of magsafe for years, and super excited when they brought it back with the AS macs. Used the cable for a year or two and then decided to simplify my life but just using USB C for everything.
I hope they can forgive me for doubting their benevolent wisdom, I promise never to do it again.
Same… I love MagSafe and would prefer to use it. I’m always worried about yanking the computer with the USB-C charger in and breaking the cable or the port.
But I have a bunch of USB-C stuff and so when I go to charge my laptop it’s just easier to find that cable and use it.
The battery life is sufficient that I never feel the need to leave it umbilical-ed to an outlet across the room. I'll leave it docked at my desk, or use it wirelessly, or charge it at a conference room table, or recharge it after the day is done in my hotel room as I sleep.
Thats the real difference - it now easily lasts until I would want to take an extended break anyway.
I might as well just use the official magsafe power cable that came with my macbook if I were to do that. The point was more convenience. I have a USB-C charger at my desk, at my bed, at the couch, etc. Anywhere I am I can just plug in without fiddling with other cables (or connectors). Ultimately I'm lazy and just want to simplify my cable management :)
The tool tells you what it would do in the current situation, you take a look and confirm that that's alright. Then you run it again without --dry-run, in a potentially different situation.
That's why I prefer Terraform's approach of having a "plan" mode. It doesn't just tell you what it would do but does so in the form of a plan it can later execute programmatically. Then, if any of the assumptions made during planning have changed, it can abort and roll back.
As a nice bonus, this pattern gives a good answer to the problem of having "if dry_run:" sprinkled everywhere: You have to separate the planning and execution in code anyway, so you can make the "just apply immediately" mode simply execute(plan()).
reply