feat(toolkit-lib): simplify deploy approval message and surface auto-confirm#1585
feat(toolkit-lib): simplify deploy approval message and surface auto-confirm#1585sai-ray wants to merge 16 commits into
Conversation
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Scanned FilesNone |
mrgrain
left a comment
There was a problem hiding this comment.
Need to look at this in more detail. It was by design that toolkit lib doesn't get interactions.
rix0rrr
left a comment
There was a problem hiding this comment.
While wer'e doing this, can we rename it to require-confirmation ?
"Approval" is actually something else than approval.
mrgrain
left a comment
There was a problem hiding this comment.
Right, I don't think I want to encode this into toolkit-lib. We are already ask for confirmation, it is up to the implementor to decide how to proceed here.
For now, we should improve the message/motivation to simply be something like
Changes detected. Do you wish to deploy these changes?
Additionally, the NonInteractiveIoHost should be clearer about what's happening, e.g. print the auto-acceptance to the output like the --yes flag does.
In future, we should have an easy way to drop a specific message:
const ioHost = new NonInteractiveIoHost();
ioHost.on('I5060', (msg) => {
// do nothing
});|
I don't disagree @mrgrain, but then shouldn't we pull this all the way through and require the confirmation always, regardless of the change type? And give enough information for the client to correlate the change impact with the command line flag? |
|
Thanks @mrgrain and @rix0rrr. Dropped the @rix0rrr on your second point : if I understand correctly, you're saying the I5060 payload should give the IoHost enough to make its own decision. |
| // in the non-interactive IoHost, no requests are promptable | ||
| await this.notify(msg); | ||
| const annotation = typeof msg.defaultResponse === 'boolean' | ||
| ? '(auto-confirmed)' |
There was a problem hiding this comment.
You are assuming that all booleans are always true, apparently.
The defaultResponse boolean could have been false, in which case auto-confirmed is not correct. It could have been auto-denied as well, you don't have enough information to tell.
There was a problem hiding this comment.
Fixed. (auto-confirmed) for true, (auto-denied) for false, (auto-responded with default: <value>) for non-boolean.
| const deployMotivation = hasSecurityChanges | ||
| ? '"--require-approval" is enabled and stack includes security-sensitive updates.' | ||
| : '"--require-approval" is enabled and stack includes updates.'; | ||
| const deployMotivation = 'Changes detected.'; |
There was a problem hiding this comment.
I think I prefer the specificy of the original message. If the problem is that you cannot refer to --require-approval, then remove that part. But now you've removed all specificity.
Here is the question I want you to ask yourself, at all times: "What is better/clearer for the user?"
So, now answer that question: why is it better for the user to get a message like:
Changes detected. Do you wish to deploy these changes?
Put yourself in the shoes of the user. You've just made changes. You're now running cdk deploy to deploy your changes. Then the tool triumphantly tells you "there are changes!". What would you think at that point?
There was a problem hiding this comment.
I think the point was: The original message was good and to the point. But the authority that comes up with that message is in the wrong place, because the toolkit-lib has no business talking about command-line arguments.
So:
- You need to produce an accurate message here that does not refer to command line arguments.
- You then need to produce an even more accurate message in the CLI, that does refer to command line arguments.
There was a problem hiding this comment.
Restored the security-vs-non-security branching, just dropped the --require-approval reference. Library now says Stack includes security-sensitive updates. / Stack includes updates..
…d from auto-denied
| ? 'Stack includes security-sensitive updates.' | ||
| : 'Stack includes updates.'; | ||
| const diffOutput = hasSecurityChanges ? securityDiff.formattedDiff : stackDiff.formattedDiff; |
There was a problem hiding this comment.
Please put the messaging about the flags back in the CliIoHost.
| return msg; | ||
| } | ||
|
|
||
| const data = (msg.data ?? {}) as { motivation?: string; permissionChangeType?: string }; |
There was a problem hiding this comment.
Is there not an existing type definition that models the data field here? I feel like we require this.
In fact, there should be a CDK_TOOLKIT_I5060.is(msg) that will enforce a type guard that will automatically make msg.data have the right type.
There was a problem hiding this comment.
Yeah, the type already exists DeployConfirmationRequest in payloads/deploy.ts. It's been wired to I5060 via make.confirm<DeployConfirmationRequest>(...) in messages.ts since before this PR. The untyped cast you commented on is gone and is replaced by the suggested type guard.
Added is() on IoRequestMaker in message-maker.ts (it was missing, only IoMessageMaker had it) and hasSecurityChanges: boolean on DeployConfirmationRequest. The augmenter now uses IO.CDK_TOOLKIT_I5060.is(msg) to narrow msg.data and reads hasSecurityChanges to pick the right phrasing. So the prompt branch is driven by a typed payload field instead of re-deriving from permissionChangeType.
| let prefix: string; | ||
| switch (this.requireDeployApproval) { | ||
| case RequireApproval.ANYCHANGE: | ||
| prefix = `"--require-approval" is set to '${RequireApproval.ANYCHANGE}'. `; |
There was a problem hiding this comment.
Because you want to reuse the original message, and both need to make sense in their own context, the final sentence here will be very staccato:
require-approval is set to anychange. Stack contains updates. Do you want to continue?
Instead, you can include hasSecurityChanges: true|false into the data object and format a complete sentence here, rather than scrapping about with fragments.
You might have noticed already, but I care quite a lot how CDK "feels". Not just: what the CLI says is technically accurate, but also that it speaks in a normal voice to a normal user. No formal IBM-speak, no vague Microsoft-speak.
There was a problem hiding this comment.
Done. Added hasSecurityChanges: boolean to the data object, and the augmenter now writes one of three full sentences instead of stitching fragments onto the library's text:
- BROADENING + has-security:
Stack includes security-sensitive updates and "--require-approval" is set to 'broadening'.
Do you wish to deploy these changes?
- ANYCHANGE + has-security:
Stack includes security-sensitive updates and "--require-approval" is set to 'any-change'.
Do you wish to deploy these changes?
- ANYCHANGE + non-security:
Stack includes updates and "--require-approval" is set to 'any-change'.
Do you wish to deploy these changes?
On the voice, this was what I thought as being both natural and not too informal. Any thoughts/ suggestions?
| // The library emits I5060 with a flag-free motivation. The CLI is the | ||
| // layer that knows about `--require-approval` and adds it to the | ||
| // user-facing message before any downstream rendering. | ||
| const promptMessage = this.augmentDeployApprovalMessage(msg).message; |
There was a problem hiding this comment.
No need for this function to return a full IoRequest object if all we're going to do is pull out a single field from that object and throw the rest away. You can just have this return a string.
…notice-suppression # Conflicts: # packages/aws-cdk/lib/cli/cdk-toolkit.ts
| /** | ||
| * Add the `--require-approval` flag to the deploy approval prompt. | ||
| */ | ||
| private augmentDeployApprovalMessage(msg: IoRequest<any, any>): string { |
| /** | ||
| * True when the change includes security-sensitive updates (IAM or security | ||
| * group changes). IoHosts use this to phrase the prompt accordingly. | ||
| */ | ||
| readonly hasSecurityChanges: boolean; |
There was a problem hiding this comment.
this is redundant, permissionChangeType already includes this.
| /** | |
| * True when the change includes security-sensitive updates (IAM or security | |
| * group changes). IoHosts use this to phrase the prompt accordingly. | |
| */ | |
| readonly hasSecurityChanges: boolean; |
| motivation: deployMotivation, | ||
| concurrency, | ||
| permissionChangeType: securityDiff.permissionChangeType, | ||
| hasSecurityChanges, |
There was a problem hiding this comment.
redundant permissionChangeType: securityDiff.permissionChangeType has the same info
| hasSecurityChanges, |
| return msg.message; | ||
| } | ||
|
|
||
| const hasSecurityChanges = msg.data.hasSecurityChanges; |
There was a problem hiding this comment.
check msg.data.permissionChangeType instead
| motivation, | ||
| concurrency: this.options.concurrency, | ||
| permissionChangeType: securityDiff.permissionChangeType, | ||
| hasSecurityChanges, |
| @@ -2347,6 +2349,7 @@ class WorkGraphDeploymentActions implements WorkGraphActions { | |||
There was a problem hiding this comment.
nit: For consistency with toolkit-lib change : to .?
| } | ||
| if (this.requireDeployApproval === RequireApproval.ANYCHANGE && !hasSecurityChanges) { | ||
| return 'Stack includes updates and "--require-approval" is set to \'any-change\'.\nDo you wish to deploy these changes?'; | ||
| } |
There was a problem hiding this comment.
Hmm. I feel this much harder to read than the old system. I had to read three times to see the differences here. Also this if is going to be annoying if we are adding more states.
I suggest you keep this as a templating string:
const updateTypeText = hasSecurityChanges ? "security-sensitive updates" : "updates";
const requireApproval = this.requireDeployApproval; // maybe? not sure.
return `Stack includes ${updateTypeText} and "--require-approval" is set to '${requireApproval}'.\nDo you wish to deploy these changes?`;
Closes #828.
@aws-cdk/toolkit-lib'sdeploy()previously printed"--require-approval" is enabled and stack includes security-sensitive updates. Do you wish to deploy these changesand then silently auto-confirmed via the defaultNonInteractiveIoHost. The question-shaped text without a visible answer-shaped action confused programmatic users.This PR:
keeps the I5060 confirmation request in
_deploy(it's the IoHost's job to decide what to do with it).drops the
--require-approvalreference from the library's vocabulary.makes
NonInteractiveIoHost's auto-response visible.centralizes the CLI-side
--require-approvalframing inCliIoHost.Changes
@aws-cdk/toolkit-lib/lib/payloads/deploy.ts: addedhasSecurityChanges: booleantoDeployConfirmationRequestso IoHosts can phrase prompts based on whether the change touches IAM/SG.@aws-cdk/toolkit-lib/lib/api/io/private/message-maker.ts: addedis()type guard toIoRequestMaker(mirroring the existingIoMessageMaker.is()) so consumers can narrowmsg.datato the request's payload type.@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts: motivation drops the--require-approvalreference but keeps the existing security-sensitive vs non-security-sensitive branching:Stack includes security-sensitive updatesStack includes updates@aws-cdk/toolkit-lib/lib/toolkit/non-interactive-io-host.ts:requestResponseannotates the message based on the actual default response value:defaultResponse: true:(auto-confirmed)defaultResponse: false:(auto-denied)(auto-responded with default: <value>)aws-cdk/lib/cli/io-host/cli-io-host.ts: newaugmentDeployApprovalMessagehelper. UsesIO.CDK_TOOLKIT_I5060.is(msg)to narrow the request, readshasSecurityChangesandrequireDeployApproval, and returns a complete sentence (not a prefix glued onto the existing motivation):BROADENING+ security change:Stack includes security-sensitive updates and "--require-approval" is set to 'broadening'. Do you wish to deploy these changes?ANYCHANGE+ security change:Stack includes security-sensitive updates and "--require-approval" is set to 'any-change'. Do you wish to deploy these changes?ANYCHANGE+ non-security change:Stack includes updates and "--require-approval" is set to 'any-change'. Do you wish to deploy these changes?aws-cdk/lib/cli/cdk-toolkit.ts: emits the bare-fact motivation and the newhasSecurityChangesfield;CliIoHost.augmentDeployApprovalMessageowns the user-facing rendering. Single source of truth for the flag vocabulary.@aws-cdk/toolkit-lib/test/actions/deploy.test.ts,aws-cdk/test/cli/cdk-toolkit.test.ts: motivation assertions updated for the new bare-fact text.Example
Same stack, before vs after this PR:
Before
After (
cdk deploy, default--require-approval=broadening, broadening change)After (
cdk deploy --require-approval=any-change, broadening change)After (
cdk deploy --require-approval=any-change, non-security change)After (
await toolkit.deploy()programmatic, defaultNonInteractiveIoHost, security-sensitive change)After (
await toolkit.deploy()programmatic, defaultNonInteractiveIoHost, non-security change)Integrators who want to suppress the message entirely can implement their own
IIoHostand drop theCDK_TOOLKIT_I5060request:Test plan
test/actions/deploy.test.ts(27/27 pass; motivation assertions updated).test/cli/io-host/cli-io-host.test.tsandtest/cli/cdk-toolkit.test.ts(147/147), including 4 new tests covering eachaugmentDeployApprovalMessagebranch.cdk deploy --yesandawait toolkit.deploy()against broadening (IAM role) and non-security (S3 bucket) stacks under--require-approval=broadeningand--require-approval=any-change. Each produces a single, correctly-phrased prompt.NonInteractiveIoHostannotation paths (true,false, string, number) via a probe script: each produces the expected annotation and returns the correct value.Checklist
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license