Skip to content
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

rewrite api module to use provided traits from k8s_openapi #157

Merged
merged 28 commits into from
Mar 1, 2020

Conversation

clux
Copy link
Member

@clux clux commented Feb 29, 2020

This has grown to be a semi-rewrite of Api...

Fixes #150, #158, #25, #96

  • RawApi renamed to Resource (much better tests now)
  • Api revamped
  • openapi feature removed (now always depend on non-default-features version)
  • runtime now use MetaContent trait for types with ObjectMeta
  • examples + readme updated to showcase new api
  • complications (below) resolved

so glad I'm doing this before kubecon... this is such a huge change.

Notably, this makes our Api work with ANY kubernetes type exported by k8s_openapi, removes a bunch of duplicated structs from apimachinery so we can focus on what's unique to this library; the runtime.

A few cons:

  • openapi versions of apimachinery are quite Option heavy, leading to less ergonomic manipulation of their values (we can impl helpers later)
  • Object<P, U> is currently broken - it needs to implement k8s_openapi::Resource via codegen now
  • codegen bundled with k8s-openapi-derive has drawbacks (see comment below)

@clux
Copy link
Member Author

clux commented Feb 29, 2020

Tried to make some compatibility layer thing with:

#[cfg(feature = "openapi")]
impl<P, U> k8s_openapi::Metadata for Object<P, U>
where
    P: Clone,
    U: Clone,
{
    type Ty = ObjectMeta;
    fn metadata(&self) -> Option<&Self::Ty> {
        Some(&self.metadata)
    }
}

but that won't work because the Metadata trait seems to require the Resource trait implemented (and we need to do that separately.

It's also a bit awkward with the Metadata trait returning Option<MetadataType> (because it's catering to list type i guess?), even though in all normal object structs the Metadata is there. That's the one thing we have relied on, even for snowflakes (which are gone in this branch).

@clux
Copy link
Member Author

clux commented Feb 29, 2020

Currently fixes #158, fixes #150, fixes #25, fixes #96

@clux clux changed the title change root details of RawApi to match k8s_openapi::Resource rewrite api module to use provided traits from k8s_openapi Mar 1, 2020
@Arnavion
Copy link

Arnavion commented Mar 1, 2020

It's also a bit awkward with the Metadata trait returning Option<MetadataType> (because it's catering to list type i guess?)

No. List<T> is also a resource with metadata, and thus also impls Metadata with Ty = ListMeta.

The reason that Metadata::metadata() returns an Option is that metadata is optional. Eg Pod::metadata and List::metadata. And that in turn is because the openapi spec says it's optional. (That said, I've always wondered exactly when the metadata can not be specified, since it's required for create / patch from the user, and always sent in responses from the API server. Is there a situation where the API server doesn't send it.)

@clux
Copy link
Member Author

clux commented Mar 1, 2020

The reason that Metadata::metadata() returns an Option is that metadata is optional. Eg Pod::metadata and List::metadata. And that in turn is because the openapi spec says it's optional. (That said, I've always wondered exactly when the metadata can not be specified, since it's required for create / patch from the user, and always sent in responses from the API server. Is there a situation where the API server doesn't send it.)

Yeah, that makes sense. Having investigated more, it looks like the kubernetes api is just super-conservative on optionality. Just like PodSpec on Pod. How can that ever be optional? The API definitely hard limits this on even input.

Interestingly, trying to create a Pod one without PodSpec will complain that spec.containers: Required value., which is the same error you get if you supply spec: {}. Maybe it's conflating optionality and defaulting, or maybe their internal data type they accept through the api, and the type they really must have before creating it differ.

At any rate, it's a shame because this causes so many guaranteed unwraps in controllers, and I really would just like to fail to even deserialize an object if it doesn't satisfy my assumptions about it.

@clux
Copy link
Member Author

clux commented Mar 1, 2020

No. List<T> is also a resource with metadata, and thus also impls Metadata with Ty = ListMeta.

(What I meant is that types with ListMeta has a much more restrictive offering in their metadata. I was speculating that maybe the optionality of metadata somehow was somehow related to the sparseness of metadata in LIST calls, but as you've pointed out, it's just plain marked optional even for types with Metadata<Ty=ObjectMeta>)

@Arnavion
Copy link

Arnavion commented Mar 1, 2020

Interestingly, trying to create a Pod one without PodSpec will complain that spec.containers: Required value., which is the same error you get if you supply spec: {}.

The golang code defines Pod.spec as type PodSpec, so its zero value is an actual object with zeroed fields rather than nil. So both {} and { "spec": {} } will deserialize the same way - to an object with zeroed fields.

@clux
Copy link
Member Author

clux commented Mar 1, 2020

Have updated all examples, and fixed all tests to work with the current setup. Removed Object because there was no way to actually implement the required Resource trait except via codegen, so need to go down that route (which is fine, that's a nice pattern).

Have tried out the k8s-openapi-derive setup first. This gets most of the way there, and actually lets us have a custom thing that implements k8s_openapi::Resource. See crd_api example.

However, it's not super customizable atm and merging as it stands would (afaikt) make us drop support for manipulating status objects. Have raised Arnavion/k8s-openapi#62 upstream. But might explore an additional approach.

@teozkr how do you feel about this pr?

@clux clux marked this pull request as ready for review March 1, 2020 13:59
@nightkr
Copy link
Member

nightkr commented Mar 1, 2020

Big fan of this in concept, but I'm a bit out of order right now so I haven't done a proper review yet (pizzas are bloody dangerous, as it turns out..).

That said, I don't think this resolves (my interpretation of) #150, although it did end up getting a bit derailed in this direction (sorry!). In particular, namespace is still implied (which bubbles up from Resource aka RawApi). It would also be great IMO if Api's K was given to the operations (so the path became Api::get::<K> rather than Api::<K>::get), so that a single Api instance could be shared. I'll sketch up a PR for that when I feel a bit better.

@nightkr
Copy link
Member

nightkr commented Mar 1, 2020

And for the record, 👍 from me on requiring k8s-openapi and dropping Object.

@clux
Copy link
Member Author

clux commented Mar 1, 2020

In particular, namespace is still implied (which bubbles up from Resource aka RawApi). It would also be great IMO if Api's K was given to the operations (so the path became Api::get::<K> rather than Api::<K>::get), so that a single Api instance could be shared. I'll sketch up a PR for that when I feel a bit better.

Yeah, I realised that this only addresses the "derailed part" of this issue, and didn't want to change that world in one go, as this PR is already quite big. But I agree that this could be quite nice.

That said, I am not sure I understand how connection pooling would be improved given that the underlying connection is already shared.

@nightkr
Copy link
Member

nightkr commented Mar 1, 2020

Right, forgot about the distinction between Api (which is currently (at least) unique per K) and APIClient (which is shared and owns the connection pool). My bad.

///
/// This is the smallest amount of info we need to run the API against a CR
/// The version, and group must be set by the user.
pub struct CustomResource {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like a leftover from the days of feature-gated k8s-openapi.

Copy link
Member Author

@clux clux Mar 1, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a temporary new thing until I have a fully reliable derive crate. This will be removed at some point. (All the other autogenerated Api::v1Pod disappeared, but the manual Api::customResource had to go somewhere temporaily).

}

/// Make Resource useable on CRDs without k8s_openapi
impl From<CustomResource> for Resource {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They seem to match 1:1, so.. why not just have CrBuilder build a Resource directly? (Assuming that this is still worthwhile to keep.)

Copy link
Member Author

@clux clux Mar 1, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could've called it a ResourceBuilder perhaps - might be more appropriate - but it's only really useful for custom resources, that was why. It's almost the same, it infers the api_version from version and kind.

Comment on lines 24 to 26
fn resource_ver(&self) -> Option<String>;
fn name(&self) -> String;
fn namespace(&self) -> Option<String>;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very minor nitpick, but.. I think all of these could borrow the String instead?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, can do. Was flipping between when implementing it. Switching now. Want to rename the trait to something shorter anyway. Maybe just straight Meta.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I might still clone in the non-expensive getters like Meta::name , because it's really conflicting in all the examples where we need to burrow multiple parts from the main object:

        let name = Meta::name(&pod);
        let phase = pod.status.as_ref().unwrap().phase.as_ref().unwrap();
        let containers = pod.spec.as_ref().unwrap()
                .containers
                .into_iter()
                .map(|c| c.name)
                .collect::<Vec<_>>();
        info!("Found initial pod {} ({}) with {:?}", name, phase, containers);

If we all burrowed from pod in Meta, then the least significant burrow for the name would prevent us from burrowing pod.spec (cannot move out of shared reference).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The client code already knows what Metadata::Ty will be, so it should just be able to access the field directly (let meta = &pod.metadata.name;). Then again, I can see that it would be a pain to change all the examples again.. :/

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once you do that you quickly hit move out of burrowed context since metadata is optional. I was trying a bunch of different configurations, and have gone for the most ergonomic one so far, maybe there is a better one, but also kind of just want to get this done now :D

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough!

Added(K),
Modified(K),
Deleted(K),
Error(ErrorResponse),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This wasn't really added here, but.. maybe the error conditions should be moved out into a Result<WatchEvent<K>> instead? This should probably be a separate issue rather than blocking this PR.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that might be something worth raising. Have also considered picking up the upstream WatchEvent from k8s_openapi which in its latest version is very similar to the one we have (although with even more error separation).

/// Note that this is only used internally within reflectors and informers,
/// and is generally produced from list/watch/delete collection queries on an `Resource`.
///
/// This is now equivalent to k8s_openapi::List<T>, but with extra convenience impls
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this machinery worth it to be able to say for x in list.iter() {} rather than for x in list.items.iter() {}? Personally, I almost prefer the latter..

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if feels nicer to beginners who can do node like things like:

for pod in api.list() {
     println!("{:?}", pod)
}

and it's common pattern to implement for wrapper types like this. I don't think there's a real cost other than the couple of extra lines in here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For one, k8s_openapi::List implements a bunch of traits that kube::api::List doesn't (Debug, PartialEq, Serialize, ...).

Copy link
Member Author

@clux clux Mar 1, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. that is true. Probably worth moving this over as well 😪

src/api/openapi.rs Show resolved Hide resolved
this reduces a bunch of unwrap mechanics on metadata in examples.
@clux
Copy link
Member Author

clux commented Mar 1, 2020

This thing is too big to fail.

@clux clux merged commit 52e04a3 into master Mar 1, 2020
@clux clux deleted the openapi-traits branch March 1, 2020 19:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Remove implicit state from api::Api
3 participants