feat(gateway): add update commands - #1900
Conversation
44d48b6 to
a7eafd8
Compare
a7eafd8 to
1fd0230
Compare
1fd0230 to
c8c57a0
Compare
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## refactor #1900 +/- ##
============================================
+ Coverage 96.78% 96.81% +0.03%
============================================
Files 326 330 +4
Lines 18061 18758 +697
============================================
+ Hits 17480 18161 +681
- Misses 581 597 +16 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
c8c57a0 to
7929436
Compare
7929436 to
4bc43ed
Compare
4bc43ed to
d7e30f7
Compare
6774fc3 to
87ae909
Compare
87ae909 to
6629f3f
Compare
6629f3f to
3b72c3d
Compare
3b72c3d to
f247150
Compare
f247150 to
09c53af
Compare
| } | ||
|
|
||
| describe("GatewayClient updateGateway", () => { | ||
| test("preserves required and omitted fields while replacing selected configuration", async () => { |
There was a problem hiding this comment.
what coverage or guarantees do we get from these tests that we don't get from the top level handler tests that use fixtures? I'm wondering if testing on the client is focusing too heavily on implementation details, rather than behavior.
There was a problem hiding this comment.
That’s fair. The fixture-backed flow already covers the normal GET-to-Update path, so the simple preservation tests are redundant. I’ll trim this file to the clear, merge, shorthand, and classification behaviors the fixture flow does not cover.
| ): Promise<UpdateGatewayResponse> { | ||
| const control = this.clients.control(toClientConfig(options)); | ||
| const current = await control.send(new GetGatewayCommand({ gatewayIdentifier: patch.id })); | ||
| const name = GatewayClient.required(current.name, patch.id, "name"); |
There was a problem hiding this comment.
would a get gateway response ever return a response without these fields? If we did get a response without these, should that be classified as a service error instead of client?
There was a problem hiding this comment.
Good catch. name and authorizerType are modeled as required, and roleArn is required by Create and Update. If GET omits them, that is a service-response issue rather than invalid user input. I changed this guard, and the equivalent required Target configuration guard, to report source: service.
| const control = this.clients.control(toClientConfig(options)); | ||
| const current = await control.send(new GetGatewayCommand({ gatewayIdentifier: patch.id })); | ||
| const name = GatewayClient.required(current.name, patch.id, "name"); | ||
| const roleArn = GatewayClient.required(current.roleArn, patch.id, "role ARN"); |
There was a problem hiding this comment.
would it be simpler to deep merge current and patch then let the API reject? I'm wondering if this could get difficult to maintain as the fields change/grow.
Something like
const current = await.control.send(new GetGatewayCommand({ gatewayIdentifier: patch.id }));
const request = deepReplace(current, patch);
return control.send(new UpdateGatewayCommand(request));
where deepReplace walks the object tree calling the existing replace method.
The downside is we lose the custom error messages, but I wonder if we could rely on the service validation for this?
There was a problem hiding this comment.
I considered this, but deep merging would treat every nested object as a patch. Our JSON flags are full replacements, so it could silently preserve fields the customer intended to remove. Policy Engine is the one intentional partial merge because we expose Policy Engine as separate --policy-engine-arn and --policy-engine-mode flags. An explicit mapper keeps those preserve, replace, and clear semantics unambiguous.
| return this.clients.control(toClientConfig(options)).send(new UpdateGatewayRuleCommand(input)); | ||
| } | ||
|
|
||
| private async updateTarget( |
There was a problem hiding this comment.
similar question here. I wonder if we can avoid maintaining all this client side validation.
There was a problem hiding this comment.
I kept the checks that enforce CLI-only semantics: connector update must remain connector-backed, and --endpoint only applies to an existing MCP server Target. The service cannot infer those command and shorthand contracts. Missing required targetConfiguration from GET now reports a service error instead of a user validation error.
| z.string().optional(), | ||
| ), | ||
| flag( | ||
| "private-endpoint", |
There was a problem hiding this comment.
one day I would love to generate these from the api spec, but that day is not today lol.
| throw new InputValidationError("required option '--target-id <target-id>' not specified"); | ||
| } | ||
|
|
||
| for (const [name, value, clear] of [ |
There was a problem hiding this comment.
i like this pattern for handling mutually exclusive. Do you see a natural way to generalize it a bit to re-use across these few handlers that use it in this PR?
There was a problem hiding this comment.
Implemented as validateSetClearConflicts in the shared handler utilities and reused it across Gateway, Target, Connector, and Rule updates. Resource-specific validation remains in each handler.
|
|
||
| export type CreateGatewayRuleInput = CreateGatewayRuleRequest; | ||
|
|
||
| export type GatewayUpdatePatch = { |
There was a problem hiding this comment.
is there a strong motivation to define our own types here, instead of derive it from the SDK types?
There was a problem hiding this comment.
We need a distinct patch type because it includes CLI-only semantics such as clear sentinels and endpoint shorthand. However, fields that map directly to the service should derive their types from UpdateGatewayRequest and UpdateGatewayTargetRequest. I’ll update those while keeping only the CLI-specific additions explicit.
09c53af to
dd11915
Compare
| return JSON.parse(raw) as T; | ||
| } catch (error) { | ||
| throw new InputValidationError( | ||
| `Invalid JSON for option '--${name}': ${error instanceof Error ? error.message : String(error)}`, |
There was a problem hiding this comment.
OOS here, but I feel like we should rename this to avoid it becoming a dumping ground of a bunch of functionality? Looks mostly parsing related? maybe src/handlers/parsing.tsx?
There was a problem hiding this comment.
Agreed that utils.tsx is broad and could become a dumping ground. I kept the file rename out of this PR since this change only adds one handler-validation helper. Splitting the existing parsing utilities can be a focused follow-up.
There was a problem hiding this comment.
I also think that the parsing stuff should be moved into a class. There is only one function here that is not parsing.
| return parsed as T[]; | ||
| } | ||
|
|
||
| export function validateSetClearConflicts( |
There was a problem hiding this comment.
nit: my understanding is that the existing logic holds for any mutually exclusive input (not just set and clear). Does it make sense to name it something to capture that? Ex. assertMutuallyExclusiveInputs or something.
There was a problem hiding this comment.
Updated this to assertMutuallyExclusiveInputs. It now accepts both input names and values, and is used for ordinary pairs such as --endpoint/--target-configuration and --connector/--connector-configuration, as well as set/clear pairs.
dd11915 to
7bca271
Compare
jariy17
left a comment
There was a problem hiding this comment.
nit: These are nits so make follow up prs for this.
| const control = this.clients.control(toClientConfig(options)); | ||
| const current = await control.send(new GetGatewayCommand({ gatewayIdentifier: patch.id })); | ||
| const resource = `Gateway "${patch.id}"`; | ||
| const name = GatewayClient.required(current.name, resource, "name"); |
There was a problem hiding this comment.
Please check the smithy model, I'm pretty sure the name is required in the response. AWS SDK v3's response parameters are all | undefined for some reason.
| let policyEngineConfiguration = current.policyEngineConfiguration; | ||
| if (patch.policyEngineConfiguration === null) { | ||
| policyEngineConfiguration = undefined; | ||
| } else if (patch.policyEngineConfiguration !== undefined) { |
There was a problem hiding this comment.
Lets choose one null or undefined.
| return control.send(new UpdateGatewayTargetCommand(request)); | ||
| } | ||
|
|
||
| private static replace<T>( |
There was a problem hiding this comment.
Is there a way we can use this.replace instead of GatewayClient.replace?
Summary
IAM scope
--role-arnupdates only the Gateway service configurationStack
Testing