Adds declarative-friendly state transition guidance - #1079
Conversation
This PR introduces a new `desired_state` field to accommodate declarative-friendly tooling that needs to be able to modify state.
bgrant0607
left a comment
There was a problem hiding this comment.
Thanks for taking a stab at this. It's been on the backlog for a long time.
Do you have a specific, urgent need for this?
In general, AIP 128's prohibition on custom methods isn't viable, even if we exclude data-plane methods. We need more pragmatic, nuanced guidance regarding when to use custom methods and when to use alternatives.
Custom methods can be appropriate for making some operational changes or to perform other control actions. RestartVM is the canonical example. In those cases, we want to ensure that the resource desired state can be configured using the surface of choice (declarative, CLI, UI, chatbot, voice assistant, terminal console, IDE, etc.) and that the operational changes can be made independently with the surface or automation of choice (e.g., runtime controller, rollout process, operational console). This can also simplify more restricted permissions and narrower blast radius for such common administrative or operational actions.
| } | ||
|
|
||
| State state = 1 [(google.api.field_behaviopr = OUTPUT_ONLY)]; | ||
| State desired_state = 2; |
There was a problem hiding this comment.
Issues with this:
- Many State values (e.g., transitional ones) wouldn't be valid for the desired state.
- Enums can't be extended without potentially breaking clients.
- Per-service state enums prevent cross-service lifecycle management. This is a major gap in AIP 152 (Jobs), as one example. Also in 164 (soft delete).
Separate attributes are often better. For instance, Kubernetes Deployment has a "paused" field:
https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.27/#deploymentspec-v1-apps
There was a problem hiding this comment.
- As mentioned below, transitional states would be
INVALID_ARGUMENTif supplied todesired_state. - I feel like I've known what scenario this breaks before but can't recall it - can you give an example?
- It sounds like you're saying that this entire AIP is a mistake and should be deprecated -- if that's the case we really need guidance to replace it.
There was a problem hiding this comment.
I feel like I've known what scenario this breaks before but can't recall it - can you give an example?
@bgrant0607 I feel like we should just update google.aip.dev/180 to explain this one. One reason is that clients unaware of the new enum value will receive the unspecified value, and send that over the wire potentially erasing the currently set parameter.
It sounds like you're saying that this entire AIP is a mistake and should be deprecated -- if that's the case we really need guidance to replace it.
I'm not sure if it's that strong of a case, but we do need updated guidance that enables known gaps like cross-service lifecycle management.
I definitely agree guidance needs be updated, but it's on a longer backlog. At minimum I think anything that removes guidance from AIP-128 and reduces the gap between declarative-friendly and non-declarative-friendly APIs is an improvement. Thanks for authoring this!
There was a problem hiding this comment.
To clarify I think Brian is arguing for more granular switches on fields (paused with a boolean vs desired_state that has the same set of possible enum values). I do agree that, at minimum, desired_state shouldn't have the ability to be set to transient states (RUNNING)
There was a problem hiding this comment.
I'd suggest this actually isn't a recommendation we should make. It may even be an anti-pattern, although it's understandable why it comes up as an idea every so often.
-
It's true that custom methods are essentially invisible to desired-state systems
-
It's also true that some custom methods have the effect of moving the output-only state field (eventually) to a similarly-named value.
So it seems like there's a connection between the two. But having an assignable field of the output-only enum is very rarely the best way to design the new hand-free resource control feature.
A different question to ask is "What config-feature on this resource would I add that, when optionally configured by the customer, would tell the backend when these custom methods should be called automatically?"
Looking at VM instance, some custom methods that can't be reached are start/stop/reset/suspend/resume... A quick hypothetical of those...
message Instance {
...
// Assign PowerControl details to enable automated control of running state.
// Default behavior when nil is to leave all running-state to imperative method calls.
PowerControl power_control;
message PowerControl {
// When specified, indicates that the appropriate custom method should be called as needed.
// Calls to the custom methods "stop" "start" "suspend" and "resume" will fail if they contradict
// the configured goal.
enum goal = UNSPECIFIED | STARTED | SUSPENDED | STOPPED;
// If the instance has been running since before this time, and it is not after this time,
// then the instance will restart. Setting it to "now" is an effective way to cause a single reset.
// Setting to a future time will schedule a single reset for when that time arrives.
Timestamp restart_when;
// other control features added here as they become necessary
}
}
... just a gist, but you get the idea.
- if unassigned the current behavior (non-automated, custom method driven only) is fully in effect
- if there is an enum goal, the values can be only what's needed and meaningful. they don't even necessarily have to align with actual output_only state enum.
- any other resource-type-specific subtlety about rules and conditions of when those automatic effects can/should take place can be taken into account with additional fields in the new structure (and more can be added in the future to make the hands-free control more sophisticated)
| PUBLISHED = 3; | ||
| } | ||
|
|
||
| State state = 1 [(google.api.field_behaviopr = OUTPUT_ONLY)]; |
| reached the `desired_state` (or **must** error if it cannot). | ||
| - If a resource supports both declarative `desired_state` and imperative | ||
| [state transition methods](#state-transition-methods), executing a state | ||
| transition method **should** modify the desired state appropriately. |
There was a problem hiding this comment.
This will create drift in declarative clients. It would be better to not have the desired state field in that case.
There was a problem hiding this comment.
Yes, it will create drift - but I'm trying to consider:
- Existing APIs with custom methods that might migrate to this pattern and have both for that reason.
- Flexibility for clients to use a more imperative state transition method if they prefer (presumably the client is not generally declarative in this scenario).
Declarative tooling expects to be the only manager of a given resource, so out of band updates of any kind create drift problems. This is something that API consumers have to manage for themselves.
There was a problem hiding this comment.
maybe this is why it's best to not have desired_state, but rather individual flags that limit state transitions?
e.g. suspended would set the resource to be suspended, and not allow other state transitions.
Or maybe have a way to clear desired state (unspecified) that allows custom methods to mutate the state? effectively a lock.
| - If a resource supports both declarative `desired_state` and imperative | ||
| [state transition methods](#state-transition-methods), executing a state | ||
| transition method **should** modify the desired state appropriately. | ||
| - If a resource is created without `desired_state`, the field **must** default |
There was a problem hiding this comment.
We're working on guidance around how to handle default values. When services change values set by clients, it causes problems for declarative and other state-driven clients. It would be better to leave the value unspecified.
There was a problem hiding this comment.
I can buy an argument for leaving it unspecified.
There was a problem hiding this comment.
+1 to unspecified. unspecified IMO means "I'm managing this state outside of the resource". If i saw a resting value I'd read that to be "keep this resource in the resting state".
mbleigh
left a comment
There was a problem hiding this comment.
Nothing urgent blocked by this, but this pattern occurs very regularly in APIs I am reviewing and it's really become a problem that there's no declarative-friendly way to solve it.
I think this proposal is entirely compatible with a :restart type custom method - the desired_state would remain unchanged, state would move to a temporary RESTARTING value and then back to ACTIVE or similar. I specifically called out that out-of-band changes to state that will transition back to the desired state don't need to mutate desired state.
Communication of resource state is an important and very common pattern. If the current AIP needs to be entirely reworked, I'm happy to pitch in and help write replacement guidance but it's been frustrating to feel like I have no good advice to give when I'm asked about how to do this "properly" for declarative-friendly APIs.
| } | ||
|
|
||
| State state = 1 [(google.api.field_behaviopr = OUTPUT_ONLY)]; | ||
| State desired_state = 2; |
There was a problem hiding this comment.
- As mentioned below, transitional states would be
INVALID_ARGUMENTif supplied todesired_state. - I feel like I've known what scenario this breaks before but can't recall it - can you give an example?
- It sounds like you're saying that this entire AIP is a mistake and should be deprecated -- if that's the case we really need guidance to replace it.
| reached the `desired_state` (or **must** error if it cannot). | ||
| - If a resource supports both declarative `desired_state` and imperative | ||
| [state transition methods](#state-transition-methods), executing a state | ||
| transition method **should** modify the desired state appropriately. |
There was a problem hiding this comment.
Yes, it will create drift - but I'm trying to consider:
- Existing APIs with custom methods that might migrate to this pattern and have both for that reason.
- Flexibility for clients to use a more imperative state transition method if they prefer (presumably the client is not generally declarative in this scenario).
Declarative tooling expects to be the only manager of a given resource, so out of band updates of any kind create drift problems. This is something that API consumers have to manage for themselves.
| - If a resource supports both declarative `desired_state` and imperative | ||
| [state transition methods](#state-transition-methods), executing a state | ||
| transition method **should** modify the desired state appropriately. | ||
| - If a resource is created without `desired_state`, the field **must** default |
There was a problem hiding this comment.
I can buy an argument for leaving it unspecified.
toumorokoshi
left a comment
There was a problem hiding this comment.
Thanks for the changes! some high-level feedback so we can align on design.
| - The comment for the field **should** document the resource pattern. | ||
| - Other fields **may** be included. | ||
|
|
||
| ### Declarative-friendly state transitions |
There was a problem hiding this comment.
we shouldn't call this "declarative-friendly": there really shouldn't be a different between declarative-friendly APIs and non-declarative friendly APIs.
Is there a way to make the guidance more generic?
There was a problem hiding this comment.
I'd say this guidance is more "managing state with resource fields" than declarative.
| } | ||
|
|
||
| State state = 1 [(google.api.field_behaviopr = OUTPUT_ONLY)]; | ||
| State desired_state = 2; |
There was a problem hiding this comment.
I feel like I've known what scenario this breaks before but can't recall it - can you give an example?
@bgrant0607 I feel like we should just update google.aip.dev/180 to explain this one. One reason is that clients unaware of the new enum value will receive the unspecified value, and send that over the wire potentially erasing the currently set parameter.
It sounds like you're saying that this entire AIP is a mistake and should be deprecated -- if that's the case we really need guidance to replace it.
I'm not sure if it's that strong of a case, but we do need updated guidance that enables known gaps like cross-service lifecycle management.
I definitely agree guidance needs be updated, but it's on a longer backlog. At minimum I think anything that removes guidance from AIP-128 and reduces the gap between declarative-friendly and non-declarative-friendly APIs is an improvement. Thanks for authoring this!
| } | ||
|
|
||
| State state = 1 [(google.api.field_behaviopr = OUTPUT_ONLY)]; | ||
| State desired_state = 2; |
There was a problem hiding this comment.
To clarify I think Brian is arguing for more granular switches on fields (paused with a boolean vs desired_state that has the same set of possible enum values). I do agree that, at minimum, desired_state shouldn't have the ability to be set to transient states (RUNNING)
|
|
||
| - The `state` field's output **may** change to more granular | ||
| [active states](#common-states) to indicate progress toward the `desired_state`. | ||
| - The `desired_state` field **must** only allow [resting state](#common-states) |
There was a problem hiding this comment.
As discussed above, the best way to enforce this is to use a separate enum entirely.
Doing so eliminates the need to require the runtime behavior outlined below (preconditions, invalid_argument)
| error with `INVALID_ARGUMENT` (HTTP 400). | ||
| - Attempting to set `desired_state` to a state that cannot be transitioned to | ||
| from the current state **must** error with `FAILED_PRECONDITION` (HTTP 400). | ||
| - A [long-running][] operation **must not** complete until the `state` has |
There was a problem hiding this comment.
would this apply for custom methods, or for updates?
In either case I kind of feel like this shouldn't be the case - the client can poll the resource state if desired, and if the operation takes hours, have a several-hour-long LRO just to see if your parameter was accepted feels like it'll limit it's utility for local clients like gcloud and Terraform.
| reached the `desired_state` (or **must** error if it cannot). | ||
| - If a resource supports both declarative `desired_state` and imperative | ||
| [state transition methods](#state-transition-methods), executing a state | ||
| transition method **should** modify the desired state appropriately. |
There was a problem hiding this comment.
maybe this is why it's best to not have desired_state, but rather individual flags that limit state transitions?
e.g. suspended would set the resource to be suspended, and not allow other state transitions.
Or maybe have a way to clear desired state (unspecified) that allows custom methods to mutate the state? effectively a lock.
| - If a resource supports both declarative `desired_state` and imperative | ||
| [state transition methods](#state-transition-methods), executing a state | ||
| transition method **should** modify the desired state appropriately. | ||
| - If a resource is created without `desired_state`, the field **must** default |
There was a problem hiding this comment.
+1 to unspecified. unspecified IMO means "I'm managing this state outside of the resource". If i saw a resting value I'd read that to be "keep this resource in the resting state".
|
@mbleigh Are you still planning to get this merged in the short to medium term? Or would it make sense to change the status to "draft"? |
This PR introduces a new
desired_statefield to accommodate declarative tooling that needs to be able to modify state.Problem: Without this guidance,
stateguidance says "make custom methods" and declarative-friendly guidance says "don't make custom methods".Solution: Allow a
desired_statefield that declarative tooling can use to perform state transitions. Thestatefield can transition through temporary active states before arriving at the requested resting state.