51a62a9265
In #17623 through #17627 and some follow-up PRs, we built out a framework for fuzzing lifecycle tests in order to help track down snapshot integrity violations in the Pulumi engine. All that remains now is to actually provide ways to trigger a fuzzing run in useful ways. This commit kicks this off by introducing two Go test functions that can be run with `go test` or our `Makefile`: * `TestFuzz` -- this runs the fuzzer and generates a brand new set of scenarios (1,000 by default) and checks whether any of them result in a snapshot integrity error. This test is skipped unless an environment variable is set (which the `Makefile` handles if one runs `make test_lifecycle_fuzz`). The intended purpose of this test is to back one or more CI workflows that will run periodically in order to slowly explore the state space. * `TestFuzzFromStateFile` -- this accepts a path to a JSON state file (such as that produced by a `pulumi stack export`) and uses that state to seed the fuzzer, subsequently trying to find provider and operation configurations that lead to a snapshot integrity error. This test is skipped unless a state file path is set using the relevant environment variable. The intended purpose of this test is to make it possible to find root causes for user issues when all we have is a state and we'd like to guess the program/provider configurations that led to an issue. Alongside introducing these two tests, we bulk out the fuzzing documentation a bit to help engineers run them, and link to the new sections from the existing docs on snapshot integrity issues. |
||
---|---|---|
.. | ||
README.md | ||
import.md | ||
resource-registration.md | ||
state.md |
README.md
(deployments)=
Deployment execution
The Pulumi engine is responsible for orchestrating deployments. A deployment
typically comprises a single operation, such as an update or destroy, though
it may conceptually execute more than one (if --refresh
is passed to an
up
, for example). Internally, a deployment is an event-driven process whose
lifecycle looks roughly as follows:
- The engine starts and collects necessary configuration values to construct a deployment (the stack to operate on, whether it should continue when it encounters errors, and so on).
- As part of this process, the engine decides upon a source that will provide events to drive the deployment. Events represent things like resources that need to be managed ("I want to declare an S3 bucket"), or functions that need to be called ("I want to look up the EC2 instance with this ID").
- Once the deployment has been instantiated and configured with a source, the engine iterates over the source and the events it provides in order to drive the deployment.
- Resource events result in steps, which correspond to actions that must be taken in order to arrive at the desired state for a given resource -- e.g. by calling a provider method in order to create a new resource.
- When all steps have been executed, the (hopefully desired) state of the infrastructure is persisted to the configured backend and the deployment is completed.
The choice of source depends on the operation.
Operations that do not execute the program
refresh
and destroy
operations do not currently result in program execution.
Whether or not this is desirable/useful arguably depends on the context, but as
far as the implementation is concerned at present, these operations are driven
as follows:
destroy
uses a null source (gh-file:pulumi#pkg/resource/deploy/source_null.go), which simply returns no events. This effectively simulates an empty program, which is exactly what we'd write if we wanted to destroy all our resources.refresh
uses an error source (gh-file:pulumi#pkg/resource/deploy/source_error.go), which returns an error event if it is iterated. This is to capture the invariant that arefresh
operation should not consult its source, since it is only concerned with changes in the various providers, and not the program.
Operations that execute the program
For preview
and update
operations, the Pulumi program specifies the desired
state of a user's infrastructure in a manner idiomatic to the language in which
the program is written, e.g. new Resource(...)
in TypeScript, or
Resource(...)
in Python. When performing these operations, the engine
instantiates an evaluation source
(gh-file:pulumi#pkg/resource/deploy/source_eval.go) that boots up a language
host to run the program. The classes, function calls, and so on exposed by the
particular language SDK are implemented using gRPC calls under the hood that
enable communication between the language host and the engine. Specifically:
- is the primary interface used to manage
resources. A resource monitor supports registering
resources, reading
resources, invoking
functions, and many more operations key to
the operation of a Pulumi program. An evaluation source contains a
ResourceMonitor
instance that enqueues events to be returned when it is iterated over, so the deployment ends up being driven by the program evaluation. - provides methods for loading schemata.
- exposes auxilliary operations that are not specific to a particular resource, such as logging and state management.
- allows callers to execute functions in the language host, such as transforms, which are specified as part of the program but whose execution is driven by the engine.
The engine hosts services implementing ResourceMonitor
, Loader
, and
Engine
, while language hosts implement Callbacks
. All of these come together
in a number of processes to execute a Pulumi program, as elucidated in the
following pages.
:::{toctree} :maxdepth: 1 :titlesonly:
/docs/architecture/deployment-execution/resource-registration /docs/architecture/deployment-execution/state /docs/architecture/deployment-execution/import :::