-
-
Notifications
You must be signed in to change notification settings - Fork 320
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
declarative openapi validation rules #129
Comments
These look potentially useable: via https://old.reddit.com/r/rust/comments/f5m80w/rust_support_for_openapiswagger/ |
This could be linked to #159 |
Definitely something to do in a proc_macro. https://book.kubebuilder.io/reference/markers/crd-validation.html has one api set. Though we may be able to re-use a crate for it. |
We definitely need something like this to make server side apply useful. The apiserver will default all optionals as null when missing from a yaml patch passed to server side apply otherwise. |
@clux do you think this could be useful: https://docs.rs/rweb/0.5.3/rweb/openapi/index.html |
That looks promising. Seems the related proc-macro module is here: https://github.com/kdy1/rweb/tree/master/macros/src/openapi . From a quick glance it also seems tied to a web framework for customizability, and can't tell if it's openapi v2 or v3. But that said, |
@clux cool i’m going to look at that more if I get a chance Tomorrow. I’m relatively new to rust, and haven’t worked with proc macros before. Would it make sense copy those files over and make it work for our needs? Wonder how hard that would be. |
If you're willing to try it out and see if the schema it produces is usable and can be put onto the newly required That said, I know that proc-macros are definitely not an easy entry-point into rust. It's meta code that generates real code (frequently inside other macros like The proc-macro crate we have in this repo is |
I've done some experimenting and I've learned it is the OpenAPI v3 spec. Here is what I got: Hand written description: "Issuer"
required:
- domainName
- dnsProvider
type: object
properties:
domainName:
type: string
dnsProvider:
type: object
properties:
provider:
type: string
enum:
- digtalocean
- cloudflare
key:
type: string
secretKey:
type: string
secretName:
type: string
nullable: true
namespaces:
type: array
items:
type: string Generated
type: object
properties:
domainName:
type: string
dnsProvider:
type: object
properties:
provider:
enum:
- DigitalOcean
- Cloudflare
key:
type: string
secretKey:
type: string
secretName:
type: string
nullable: true
namespaces:
type: array
items:
type: string So its already pretty close Problems:
I'm also trying to figure out how to add the required fields in automatically. Updates
|
Hey this is really promising! Sounds like this is potentially possible to port into Some quick comments on the problems for now:
|
I think this would be a great approach
I he is doing something like this for the
Agreed! Definitely not needed for POC |
Yeah, it looks like there's tons of things the proc macro could fill in by looking at the root Schema definition. (At least that's the crate rweb uses, seems to be a soft fork of https://github.com/softprops/openapi - probably because of softprops/openapi#22) |
Hey a heads up, Kubernetes has some extra rules which means some valid OpenAPIV3 schemas are actually invalid in K8s world. It has to do with structural schemas: https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#specifying-a-structural-schema Example: Rust: #[derive(Serialize, Deserialize, Clone, Debug, Schema)]
#[serde(tag = "provider")]
#[serde(rename_all = "lowercase")]
pub enum DnsProvider {
DigitalOcean(BasicAuth),
Cloudflare(BasicAuth),
Route53(Route53),
}
#[derive(Serialize, Deserialize, Clone, Debug, Schema)]
#[serde(rename_all = "camelCase")]
pub struct BasicAuth {
key: String,
secret_key: String,
}
#[derive(Serialize, Deserialize, Clone, Debug, Schema)]
#[serde(rename_all = "camelCase")]
pub struct Route53 {
access_key: String,
secret_access_key: String,
region: String,
profile: Option<String>,
hosted_zone_id: Option<String>,
} rweb produces this, but it is invalid as a structural schema: dnsProvider:
oneOf:
- type: object
properties:
key:
type: string
secretKey:
type: string
- type: object
properties:
key:
type: string
secretKey:
type: string
- type: object
properties:
accessKey:
type: string
secretAccessKey:
type: string
region:
type: string
profile:
type: string
nullable: true
hostedZoneId:
type: string
nullable: true Instead it would have to be written something like this: dnsProvider:
type: object
required: ["provider"]
properties:
provider: { "type": "string" }
key: { "type": "string" }
secretKey: { "type": "string" }
accessKey: { "type": "string" }
secretAccessKey: { "type": "string" }
region: { "type": "string" }
hostedZoneId: { "type": "string" }
oneOf:
- properties:
provider:
enum: ["digitalocean"]
required: ["provider", "key", "secretKey"]
- properties:
provider:
enum: ["cloudflare"]
required: ["provider", "key", "secretKey"]
- properties:
provider:
enum: ["route53"]
required: ["accessKey", "secretAccessKey", "region"] |
Hey, thanks for figuring this out. That is annoying that it doesn't map straight onto what we need from existing crate, but it sounds like this is possible to override for enums (which look like the offending type?). I'm wondering how much is required for this work. Technically, we don't even need to even parse anything into openapi schemas we just need to generate json from struct types that matches what kubernetes expects. But it would be good to use valid openapiv3 schemas from the definitional crate (if what kubernetes accepts is within spec, sounds like it still is?) so we can add the tons of How salvageable do you feel the rweb part is? If you have anything lying around I would love to see a PR! |
Some validations equivalent to For the remaining, I'm hoping we can work with
Supported
TODOStrings
Numeric
List
Others
I just realized we can probably implement
|
ooh, manual newtype impls like |
This comment has been minimized.
This comment has been minimized.
We should add some examples showing possible validations (#355). The above can be simplified with #[derive(Serialize, Deserialize, Debug, Default, PartialEq, Eq, Clone, Copy)]
struct Replicas(u32);
impl JsonSchema for Replicas {
// ...
fn json_schema(_: &mut SchemaGenerator) -> Schema {
serde_json::from_value(serde_json::json!({
"type": "integer",
"minimum": 0,
"maximum": 10,
}))
.unwrap()
}
} By the way, k8s.io/kube-openapi/pkg/generators (used by Operator SDK?) supports something very similar: import openapi "k8s.io/kube-openapi/pkg/common"
type Time struct {
time.Time
}
func (_ Time) OpenAPIDefinition() openapi.OpenAPIDefinition {
return openapi.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
Format: "date-time",
},
},
}
} |
That looks great. A little verbose, but definitely usable. If the pattern you've found generalises to most cases, it probably wouldn't be impossible for struct Replicas(u32);
implJsonSchema!(Replicas, ({"type": "integer", "maximum": 10, "minimum": 0}) |
an addon, and illustration of the more manual parts of #129
A small extra note; if anyone needs to specify the kubernetes specific openapi extensions for deciding merge strategy of structs/maps/lists, this can be done with the same manual |
I wanted to mention a difficulty I recently ran into while generating a CRD which used an enum similar to the following: struct MySpec {
values: Vec<VariantType>,
}
enum VariantType {
VaraintA(VariantAModel),
VariantB(VariantBModel),
}
// ... other code elided ... For brevity I'm eliding the derives and such. There were two issues I ran into with this. The list type generated for the I'm wondering if perhaps we need to use the OpenAPI discriminators for each of the individual schemas and such. Doing so would probably require that we have users follow the Thoughts? |
Update: In addition to |
As per schemars@0.8.4 you can use the validator crate to get I believe the only couple of things left are structural enums, and possibly a better way to declare |
Yeah, we should close this and open separate issues (and add an example using For structural schema, I'd look into https://github.com/kubernetes/apiextensions-apiserver/tree/release-1.22/pkg/apiserver/schema . Maybe it's possible to transform the schema to structural (https://github.com/kubernetes/apiextensions-apiserver/blob/release-1.22/pkg/apiserver/schema/convert.go), and validate (https://github.com/kubernetes/apiextensions-apiserver/blob/release-1.22/pkg/apiserver/schema/validation.go). |
* add example using validator - closes #129 Signed-off-by: clux <sszynrae@gmail.com> * fix doc link, fmt, and ignore aggressive clippy warns Signed-off-by: clux <sszynrae@gmail.com> * account for choices and add warnings Signed-off-by: clux <sszynrae@gmail.com> * also validate locally and add comments here Signed-off-by: clux <sszynrae@gmail.com>
It would be really cool if we had a way to create declarative openapi validation rules ala:
e.g. something like:
The thing this most reminds me of is serde attributes, but this feels a whole lot dumber than that. We just need to generate:
{ type: object, properties: ... }
from a struct, so it's easy to attach onto the the crd json!The problem with this is that it would necessarily conflict with serde field attributes, which are also frequently used. A dumb proc macro that wasn't aware of serde wouldn't be hard to do, but I imagine it also would cause a lot of duplication between between serde attrs like
skip_serializing_if
,default
with validation rules.The text was updated successfully, but these errors were encountered: