-
Notifications
You must be signed in to change notification settings - Fork 24.9k
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
Compatible API header parsing plugin #60516
Compatible API header parsing plugin #60516
Conversation
Some revisions to the idea of using a function
this plugin is used in both RestController as well as AbstractRestChannel the best way to make getCompatibleVersion function to be usable in both places is to inject function into a RestRequest This means that it has to be available in AbstractHttpServerTransport and all its subclasses (8 of them)
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.
A few comments around naming and what is getting passed around.
@@ -0,0 +1,17 @@ | |||
evaluationDependsOn(xpackModule('core')) |
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.
the above line should not be necessary.
@@ -277,6 +277,10 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws | |||
return builder.value(toString()); | |||
} | |||
|
|||
public Version previousMajor() { | |||
return Version.fromString(this.major - 1 + ".0.0"); |
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.
nit: can you cache this is a class variable ? (i.e. only calculate it once with a null check)
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.
hm that would lead to a recursive creation. Maybe we could cache the string to build a previous version major-1+."0.0"
or a byte (previousMajor-1)*1000000
then using Version.from* returns a cached instance
throw new IllegalStateException("Only one rest compatibility plugin is allowed"); | ||
} else if (restCompatibilityPlugins.size() == 1){ | ||
restCompatibleFunction = | ||
restCompatibilityPlugins.get(0); |
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.
This really isn't a function (ie method reference) it is an instance of the plugin which satisfies the functional interface. I don't think we should pass the plugin instance around.
I think you can just change this to
restCompatibleFunction = (a,b,c) ->
restCompatibilityPlugins.get(0).getCompatibleVersion(a,b,c);
so that you are passing around the method reference. However, it is a bit confusing to return a method reference that satifies the same interface as the object. Maybe two interfaces (one functional like you have, and another a marker interface to filter the plugins) would read better ? Maybe a functional interface CompatibleVersion
(instead of RestCompatibility) , and marker interface RestCompatibilityPlugin
?
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 gave it a go, let me know if this looks any better.
There is a CompatibleVersion field on rest request, but there is a getCompatibleVersionFunction
to return it in order to implement a copying constructor.
There is also a getCompatibleVersion
function on the RestRequest
object itself which uses the CompatibleVersion
field.
It might be a bit confusing, WDYT?
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.
also I am not sure about the marker interface. We still have to define a function on it in order to use it, right?
server/src/main/java/org/elasticsearch/plugins/NetworkPlugin.java
Outdated
Show resolved
Hide resolved
import org.elasticsearch.common.Nullable; | ||
|
||
@FunctionalInterface | ||
public interface RestCompatibility { |
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.
See other comments about maybe breaking this up into two interfaces and simplifying the method name to just get
We need to pass that method reference through quite a bit of code. Since this is really only used in a very limited context I wonder if the consuming code should use some form of SPI to load class with the method reference ? We might end up with a couple instances of the class, or have to do some custom class loading ... @pgomulka - what are your thoughts...is this bleeding too much into other areas of the codebase for the sole purpose of passing it through to where it is used ? |
@jakelandis I don't think we can use the SPI classloader. In order to load a class with |
Pinging @elastic/es-core-infra (:Core/Infra/REST API) |
@elasticmachine run elasticsearch-ci/2 |
/** | ||
* @return A function that can be used to determine the requested REST compatible version | ||
*/ | ||
private CompatibleVersion getRestCompatibleFunction() { |
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.
can we add tests for this? Maybe in NodeTests
server/src/main/java/org/elasticsearch/plugins/RestCompatibilityPlugin.java
Outdated
Show resolved
Hide resolved
server/src/main/java/org/elasticsearch/rest/CompatibleVersion.java
Outdated
Show resolved
Hide resolved
Co-authored-by: Jay Modi <jaymode@users.noreply.github.com>
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.
LGTM. Thanks for the iterations.
Co-authored-by: Jay Modi <jaymode@users.noreply.github.com>
...lugin/rest-compatibility/src/main/java/org/elasticsearch/compat/CompatibleVersionPlugin.java
Outdated
Show resolved
Hide resolved
@@ -79,11 +79,18 @@ public boolean isContentConsumed() { | |||
return contentConsumed; | |||
} | |||
|
|||
// for testing |
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.
also if we make mediatypeparser even more strict, we could remove the RestRequest.parseContentType (line 504) and line 60
WDYT?
…ka/elasticsearch into compat_plugin_inside_rest_request
Byte aVersion = XContentType.parseVersion(acceptHeader); | ||
byte acceptVersion = aVersion == null ? Version.CURRENT.major : Integer.valueOf(aVersion).byteValue(); | ||
Byte cVersion = XContentType.parseVersion(contentTypeHeader); | ||
byte contentTypeVersion = cVersion == null ? Version.CURRENT.major : Integer.valueOf(cVersion).byteValue(); |
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.
this only supports versioned media types defined in XContentType
so json, yaml, cbor, smile
What if someone sends a request like:
curl --request POST \
--url http://localhost:9200/_sql \
--header 'accept: application/vnd.elasticsearch+csv;compatible-with=7' \
--header 'authorization: Basic ZWxhc3RpYzpwYXNzd29yZA==' \
--header 'content-type: application/vnd.elasticsearch+json;compatible-with=7' \
--header 'x-opaque-id: 123' \
--data '{
"query": "SELECT * FROM library WHERE release_date < \u00272000-01-01\u0027"
}
'
the current logic is not able to parse accept: application/vnd.elasticsearch+csv;compatible-with=7
so sets acceptVersion = 8
however it can parse content-type: application/vnd.elasticsearch+json;compatible-with=7'
so contentTypeVersion = 7
that leads to an exception https://github.com/elastic/elasticsearch/pull/60516/files#diff-793fe1b13ffa171bfc74dab865948b08R47 Content-Type and Accept version requests have to match.
How will clients send these headers? will client be aware to which API should they send versioned media types?
@elastic/es-clients any thoughts?
To make it work now the request would have to be sent without versions.
curl --request POST \
--url http://localhost:9200/_sql \
--header 'accept: application/csv' \
--header 'content-type: application/json' \
--header 'x-opaque-id: 123' \
--data '{
"query": "SELECT * FROM library WHERE release_date < \u00272000-01-01\u0027"
}
'
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.
The clients are running with the assumption that we can send the vendor mimetype
to all API's.
That way a client running 7.11
can start sending application/vnd.elasticsearch+json;compatible-with=7
to API's even if that API does not have any special backward compatibility handling yet.
If the user upgrades the server first to 7.11
and the API now does have backward compatibility baked in nothing really changes when the client is updated afterwards.
If the user updates to 8.x
before the API gets bwc
handling the client will still send and receive v7 mime types.
-> accept: application/vnd.elasticsearch+json;compatible-with=7
-> content-type: application/vnd.elasticsearch+json;compatible-with=7
If the client upgrades to 8.x:
-> accept: application/vnd.elasticsearch+json;compatible-with=8
-> content-type: application/vnd.elasticsearch+json;compatible-with=8
If nothing changes in the server 10.x
and the API still has no backward compatibly layer. (and the client is still on 8.x)
-> accept: application/vnd.elasticsearch+json;compatible-with=8
-> content-type: application/vnd.elasticsearch+json;compatible-with=8
Will fail, even if nothing actually changed in between these different versions. Adhering to supported backward compatibility guides.
the current logic is not able to parse accept: application/vnd.elasticsearch+csv;compatible-with=7 so sets acceptVersion = 8 however it can parse content-type: application/vnd.elasticsearch+json;compatible-with=7' so contentTypeVersion = 7
What are the cases where it can happen that json
will have a compatible response but csv
does not? Don't they flow through the same xcontent builders/parsers?
The behavior of setting acceptVersion
to something different then what the user/client explicitly provided should not happen. The resulting version mismatch error will be very confusing.
x-pack
RestCompatibilityPlugin
defines a function that takes information aboutRestRequest
(Accept
,Content-Type
andhasContent
) and returns aVersion
object indicating what compatible api version was requested.Function also validates the values provided and throws
ElasticsearchStatusException
when incorrect combination of header values was used.In oss distribution a simplistic version of a function, always returning
Version.CURRENT
is used.That function is abstracted as
CompatibleVersion
functional interface.It is being used inRestController
- for routing and inAbstractRestChannel
for response shape serialisation. To make it available onAbstractRestChannel
, it had to be added toResourceHandlingHttpChannel