-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Bug/update follower index validation cr #24
base: bug/update-follower-index-validation
Are you sure you want to change the base?
Bug/update follower index validation cr #24
Conversation
I added your API integration test via fdc36a5 (thanks!). Can you approve that PR so I can merge the fix without blocking on this proposal? |
@@ -161,21 +185,10 @@ export const registerFollowerIndexRoutes = ({ router, __LEGACY }: RouteDependenc | |||
*/ | |||
router.put( | |||
{ | |||
path: `${API_BASE_PATH}/follower_indices/{id}`, | |||
path: getEndpoint('followerIndex', 'edit').path, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I see the benefit of the added getEndpoint
machinery.
It seems like another thing we need to build out (replacePlaceholders
👀) and share properly across apps and without a clear benefit over statically declaring these values (at least the path pattern, not convinced about the method) and importing them from a common folder.
If we issue a request (in tests or in app code) against a server on a path that does not exist we should be met with a 404 and so the server is already the source of truth, so this does not seem like a major current pain point.
My understanding of the bug we had here was that we required a body but the logic inside the route handler accepted a body with all undefined
values - combined with NP route validation the oversight snuck in because body without route validation is {}
(i.e., an acceptable value!).
The best way, in my view, to cover this specific case would be the addition of an API integration test that issues a request against elastic with a value set and checking the response (like what you have added here x-pack/test/api_integration/apis/management/cross_cluster_replication/follower_indices.js
)
The major take away for me, from this bug, besides the added tests is that we need to be more thorough in our route validations and explicitly testing sending unexpected values to routes.
Can you explain the "share properly"? not sure I understand, this is a pure, static function from a lib. Let's leave out, for now, the details of naming or where it will live.
Can you explain?
I think I did not do a good job of presenting the problem we are trying to solve. Let me rephrase: Currently, if we make any change to the route handler definition (the endpoint, method or more importantly the contract). The CI will fail for the API integration tests. We then fix the API integration test, we think we are OK, but we have now a broken UI. How do we make sure that the requests we mock in our component integration tests are the correct ones and avoid having to manually update the API integration tests, the component integration tests, and the UI consuming code when touching the server routes? By having a single place to declare the endpoints. If we have a common place to declare our API routes, then making any change on the route will break both our API integration tests AND the component integration tests. This proposal is a first step to solve this. A second step that I have in mind is to save in a folder the payloads sent by the UI in the component integration tests (the same way jest saves snapshots). And use those payloads in our API integration tests. [EDIT]: See my answer here for a concrete example elastic#62890 (comment) |
I think some amount of the work here will be designing an API that expresses the intent of this mechanism. Even once that is done, I still query the value of the mechanism vs the indirection it introduces. Is this, historically, something that developers have struggled with and has lead to bugs? When updating a server route the dev workflow/pattern is typically:
Which tends to cover our bases. Even if we deviate from the pattern above, I would suggest that convention can win out over configuration. If we statically declare our server payload types and share that between server and client we will leverage TS to do the checking for us -- instead of needing to build a mechanism.
I would argue this is detrimental to DX: // It feels like I am looking at docs 😅
return supertest[method](path)
👆🏻sounds like an excellent idea that is fully orthogonal to |
So this is what I am talking about to declare an app endpoints, it took me 3 minutes to gather those // Reusable lib accross our apps
import { EndpointDefinition, createEndPointsGetter } from './my-lib';
import { API_BASE_PATH } from './constants';
// Follower indices
const followerIndexEndpoints = {
list: {
method: 'get',
path: `${API_BASE_PATH}/follower_indices`,
} as EndpointDefinition,
get: {
method: 'get',
path: `${API_BASE_PATH}/follower_indices/{id}`,
} as EndpointDefinition,
create: {
method: 'post',
path: `${API_BASE_PATH}/follower_indices`,
} as EndpointDefinition,
edit: {
method: 'put',
path: `${API_BASE_PATH}/follower_indices/{id}`,
} as EndpointDefinition,
delete: {
method: 'delete',
path: `${API_BASE_PATH}/follower_indices/{id}`,
} as EndpointDefinition,
};
const autoFollowPatternEndpoints = {
list: {
method: 'get',
path: `${API_BASE_PATH}/auto-follow-patterns`,
} as EndpointDefinition,
get: {
method: 'get',
path: `${API_BASE_PATH}/auto-follow-patterns/{id}`,
} as EndpointDefinition,
create: {
method: 'post',
path: `${API_BASE_PATH}/auto-follow-patterns`,
} as EndpointDefinition,
edit: {
method: 'put',
path: `${API_BASE_PATH}/auto-follow-patterns/{id}`,
} as EndpointDefinition,
delete: {
method: 'delete',
path: `${API_BASE_PATH}/auto-follow-patterns/{id}`,
} as EndpointDefinition,
pause: {
method: 'post',
path: `${API_BASE_PATH}/auto-follow-patterns/{id}/pause`,
} as EndpointDefinition,
resume: {
method: 'post',
path: `${API_BASE_PATH}/auto-follow-patterns/{id}/resume`,
} as EndpointDefinition,
};
// Redux like reducer composition
const ccrRoutesEndpoints = {
followerIndex: followerIndexEndpoints,
autoFollowPattern: autoFollowPatternEndpoints,
};
export const endPoints = {
get: createEndPointsGetter(ccrRoutesEndpoints),
}; And this is what I get out of it, everywhere I consume them thanks to TS I am surprised you don't see the benefit of having a single source of truth TBH. Current solution:We declared in 4 different places the same API
And like I mentioned, it is not only about declaring and maintaining 4 different places, it is about automating the whole testing process. |
@sebelga @jloleysens FYI, here's a real-life example of how Ingest Manager has implemented a single source of truth for their API routes:
|
I am glad you shard this CJ! :)
I have been giving this some more thought since sebs proposal.
Primary difference for me with what he was proposing and what ingest
manager are doing are quite small. Sebs implementation of the replace logic
was just more generic and he used namespaces; but it comes down to a very
similar mechanism.
I just didn’t like the idea of more indirection except for where it has
maximal impact. It’s still something I am hesitant about tbh. When I see
this kind of thing I instantly think about edge cases we are not
considering like query params and correctly parsing for ? and &. I don’t
want to just consider green fields when it comes to abstractions - there is
always a cost too and I want to understand what the cost is before I can
make a proper decision. (Not that we use query params in our apis much it’s
just that they are part of URLs, also we need to properly encode and decode
user input)
However, having thought about it a bit more I have come to realize that the
added indirection is really very small and the benefit is decent enough so
i have changed my position since my initial response.
I say let’s try it in ingest pipelines since it is really new and is
probably very low effort to do.
Cheers,
JL
…On Fri, 29 May 2020 at 23:18, CJ Cenizal ***@***.***> wrote:
@sebelga <https://github.com/sebelga> @jloleysens
<https://github.com/jloleysens> FYI, here's a real-life example of how
Ingest Manager has implemented a single source of truth for their API
routes:
- Route patterns are defined as constants here:
https://github.com/elastic/kibana/blob/master/x-pack/plugins/ingest_manager/common/constants/routes.ts
- They're then accessed and interpolated with variable route params
through getter methods here:
https://github.com/elastic/kibana/blob/master/x-pack/plugins/ingest_manager/common/services/routes.ts
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#24 (comment)>, or
unsubscribe
<https://github.com/notifications/unsubscribe-auth/AB6G67C2H2YR26VE53QQTL3RUARA3ANCNFSM4MJWDGNA>
.
|
@jloleysens Thank you for sharing your updated perspective and thoughtful line of reasoning. I'm generally aligned with the approach you describe. I would summarize it as: "Code comes with a cost (greater surface area for introducing bugs, required maintenance, cognitive overhead), and the benefit conveyed by the code must outweigh the cost of adding it." ++ To your ultimate decision to try out a new idea, too. To you and @sebelga, my 2 cents are that in terms of implementation I would prefer to work with an interface that shifted the responsibility of configuration from passing the right arguments to calling the right method. For example, this: const { path, method } = getEndpoint('followerIndex', 'edit', { id: encodeURIComponent(id) }); Would become one of these: const { path, method } = getEditFollowerIndexRoute({ id: encodeURIComponent(id) });
const { path, method } = followerIndexRoute.getEdit({ id: encodeURIComponent(id) }); To me, this interface says what it does: "get the route for editing a follower index". I can understand what it does by reading it, with little thought. In contrast, when I read
|
I think there are 2 things to consider on API design.
I put more emphasis at the second. I need to train my brain only a few seconds to read this const { path, method } = getEndpoint('followerIndex', 'edit', { id: encodeURIComponent(id) }); And once I did, I know exactly where to look to read it ('edit' -> 'followerIndex'), so that's where my eyes are going immediately. With that, I can now declare all my routes, in all our apps with the same interface. This is a huge win as I don't spend any brain power to parse it. Nothing is specific to an app, this could even come from a JSON const followerIndexEndpoints = {
list: {
method: 'get',
path: `${API_BASE_PATH}/follower_indices`,
} as EndpointDefinition,
get: {
method: 'get',
path: `${API_BASE_PATH}/follower_indices/{id}`,
} as EndpointDefinition,
create: {
method: 'post',
path: `${API_BASE_PATH}/follower_indices`,
} as EndpointDefinition,
edit: {
method: 'put',
path: `${API_BASE_PATH}/follower_indices/{id}`,
} as EndpointDefinition,
delete: {
method: 'delete',
path: `${API_BASE_PATH}/follower_indices/{id}`,
} as EndpointDefinition,
}; Aside, the endpoints discoverability is very pleasant DX |
What I am suggesting is to have a system in place to:
If you look at the code needed to write an app endpoint, you see that there are only objects and one handler export const endPoints = {
get: createEndPointsGetter(ccrRoutesEndpoints),
}; IMO you can't get cleaner than that.
If you look at those 2 lists import {
getEditFollowerIndexRoute,
getCreateAutoFollowPatternRoute,
getListFollowerIndexRoute,
getGetAutoFollowerPatternIndexRoute,
getDeleteFollowerIndexRoute,
getUpdateAutoFollowPatternRoute,
} from './my-routes';
const { path, method } = getEditFollowerIndexRoute();
const { path, method } = getCreateAutoFollowPatternRoute();
const { path, method } = getListFollowerIndexRoute();
const { path, method } = getGetAutoFollowerPatternIndexRoute();
const { path, method } = getDeleteFollowerIndexRoute();
const { path, method } = getUpdateFollowernRoute(); import { endPoints } from './my-routes';
const { path, method } = endPoints.get('followerIndex', 'edit');
const { path, method } = endPoints.get('autofollowPattern', 'create');
const { path, method } = endPoints.get('followerIndex', 'list');
const { path, method } = endPoints.get('autofollowPattern', 'get');
const { path, method } = endPoints.get('followerIndex', 'delete');
const { path, method } = endPoints.get('followerIndex', 'edit'); Which one do you find quicker the "Create Autofollow pattern" route? By adding space around the important term, I find the second list much faster to parse. |
I tend to stand more in alignment with @sebelga 's implementation. It contains the route + method which nicely encapsulates endpoint concerns. Additionally, changes to to the replacement mechanism only happen in one place in Seb's implementation where they happen in every function of the ingest implementation - so I think it will scale a lot better. I do think we need to Using this in TS will also have good DX, I agree :) |
Y'all will both be working with this code a lot more than I will, so it's more important that you two are comfortable with its design than I am. 😄 |
Good point. Should we automatically do it for the consumer to create URL safe paths? Should it be optional through an option prop? endPoints.get('followerIndex', 'update', { id: 'someTextToEncode' }, { encode: true }); I wonder if we return all paths with If you look at ccr, we had to both |
This makes sense to me 👍 |
A note on real-life use case of the problem faced by not having a central place to declare routes: Alison asked me to update "templates" to "index-templates" in our API route (elastic#67282 (review)). Seems like a simple task? Yet I had to locate and modify 7 files (elastic@d0ef2fb) and of course, I forgot some, and the CI failed (https://github.com/elastic/kibana/pull/67282/checks?check_run_id=734757207) We'll all agree that it is not the best use of our time and CI power 😊 |
This PR adds the test coverage for the bug found on follower indices not being updated.
The minimum test that was required to avoid the regression is the API integration test.
I then gave it some thoughts on how we could improve the confidence in our test coverage and build a convention for future apps. We need to make sure that both the API is working as expected, but also that the component is calling the correct API with the correct payload. For that, we need a single source of truth to declare the API endpoints of our apps.
The server code, the client code and the API integration code ALL read from that single source of truth. So we are sure everything is connected.
API integration test
400
Bad request with a wrong payloadComponent integration tests
Server route handler & Client API handlers
They both read from the source of truth to declare their route or HTTP request.