Giter Site home page Giter Site logo

kopium's Introduction

kube-rs

Crates.io Rust 1.75 Tested against Kubernetes v1_25 and above Best Practices Discord chat

A Rust client for Kubernetes in the style of a more generic client-go, a runtime abstraction inspired by controller-runtime, and a derive macro for CRDs inspired by kubebuilder. Hosted by CNCF as a Sandbox Project

These crates build upon Kubernetes apimachinery + api concepts to enable generic abstractions. These abstractions allow Rust reinterpretations of reflectors, controllers, and custom resource interfaces, so that you can write applications easily.

Installation

Select a version of kube along with the generated k8s-openapi structs at your chosen Kubernetes version:

[dependencies]
kube = { version = "0.91.0", features = ["runtime", "derive"] }
k8s-openapi = { version = "0.22.0", features = ["latest"] }

See features for a quick overview of default-enabled / opt-in functionality.

Upgrading

See kube.rs/upgrading. Noteworthy changes are highlighted in releases, and archived in the changelog.

Usage

See the examples directory for how to use any of these crates.

Official examples:

For real world projects see ADOPTERS.

Api

The Api is what interacts with Kubernetes resources, and is generic over Resource:

use k8s_openapi::api::core::v1::Pod;
let pods: Api<Pod> = Api::default_namespaced(client);

let pod = pods.get("blog").await?;
println!("Got pod: {pod:?}");

let patch = json!({"spec": {
    "activeDeadlineSeconds": 5
}});
let pp = PatchParams::apply("kube");
let patched = pods.patch("blog", &pp, &Patch::Apply(patch)).await?;
assert_eq!(patched.spec.active_deadline_seconds, Some(5));

pods.delete("blog", &DeleteParams::default()).await?;

See the examples ending in _api examples for more detail.

Custom Resource Definitions

Working with custom resources uses automatic code-generation via proc_macros in kube-derive.

You need to add #[derive(CustomResource, JsonSchema)] and some #[kube(attrs..)] on a spec struct:

#[derive(CustomResource, Debug, Serialize, Deserialize, Default, Clone, JsonSchema)]
#[kube(group = "kube.rs", version = "v1", kind = "Document", namespaced)]
pub struct DocumentSpec {
    title: String,
    content: String,
}

Then you can use the generated wrapper struct Document as a kube::Resource:

let docs: Api<Document> = Api::default_namespaced(client);
let d = Document::new("guide", DocumentSpec::default());
println!("doc: {:?}", d);
println!("crd: {:?}", serde_yaml::to_string(&Document::crd()));

There are a ton of kubebuilder-like instructions that you can annotate with here. See the documentation or the crd_ prefixed examples for more.

NB: #[derive(CustomResource)] requires the derive feature enabled on kube.

Runtime

The runtime module exports the kube_runtime crate and contains higher level abstractions on top of the Api and Resource types so that you don't have to do all the watch/resourceVersion/storage book-keeping yourself.

Watchers

A low level streaming interface (similar to informers) that presents Applied, Deleted or Restarted events.

let api = Api::<Pod>::default_namespaced(client);
let stream = watcher(api, Config::default()).applied_objects();

This now gives a continual stream of events and you do not need to care about the watch having to restart, or connections dropping.

while let Some(event) = stream.try_next().await? {
    println!("Applied: {}", event.name_any());
}

NB: the plain items in a watcher stream are different from WatchEvent. If you are following along to "see what changed", you should flatten it with one of the utilities from WatchStreamExt, such as applied_objects.

Reflectors

A reflector is a watcher with Store on K. It acts on all the Event<K> exposed by watcher to ensure that the state in the Store is as accurate as possible.

let nodes: Api<Node> = Api::all(client);
let lp = Config::default().labels("kubernetes.io/arch=amd64");
let (reader, writer) = reflector::store();
let rf = reflector(writer, watcher(nodes, lp));

At this point you can listen to the reflector as if it was a watcher, but you can also query the reader at any point.

Controllers

A Controller is a reflector along with an arbitrary number of watchers that schedule events internally to send events through a reconciler:

Controller::new(root_kind_api, Config::default())
    .owns(child_kind_api, Config::default())
    .run(reconcile, error_policy, context)
    .for_each(|res| async move {
        match res {
            Ok(o) => info!("reconciled {:?}", o),
            Err(e) => warn!("reconcile failed: {}", Report::from(e)),
        }
    })
    .await;

Here reconcile and error_policy refer to functions you define. The first will be called when the root or child elements change, and the second when the reconciler returns an Err.

See the controller guide for how to write these.

TLS

By default rustls is used for TLS, but openssl is supported. To switch, turn off default-features, and enable the openssl-tls feature:

[dependencies]
kube = { version = "0.91.0", default-features = false, features = ["client", "openssl-tls"] }
k8s-openapi = { version = "0.22.0", features = ["latest"] }

This will pull in openssl and hyper-openssl. If default-features is left enabled, you will pull in two TLS stacks, and the default will remain as rustls.

musl-libc

Kube will work with distroless, scratch, and alpine (it's also possible to use alpine as a builder with some caveats).

License

Apache 2.0 licensed. See LICENSE for details.

kopium's People

Contributors

alex-hunt-materialize avatar antifuchs avatar aryan9600 avatar clux avatar dav1dde avatar dependabot[bot] avatar felipesere avatar flaper87 avatar github-actions[bot] avatar imp avatar kixcord avatar sebhoss avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

kopium's Issues

Invalid generation for maps of integers

I'm quite new to rust so forgive me if this is stupid.

I've generated a file for Istio's DestinationRule type: kopium destinationrules.networking.istio.io --docs > destinationrule.rs

The CRD has various fields typed as integer in different ways (I'm also not an OpenAPI expert).
Mostly these work fine:

                        port:                                                                 
                          properties:                                                         
                            number:                                                           
                              type: integer   

becomes

pub number: Option<i64>,

Something like

                            consecutiveErrors:                                                
                              format: int32                                                   
                              type: integer 

becomes

    pub consecutiveErrors: Option<i32>,

The problem seems to be objects with additionalProperties eg

                                      to:                                                     
                                        additionalProperties:                                 
                                          type: integer                                       
                                        description: Map of upstream localities to traffic distribution weights.                       
                                        type: object   

which becomes

    pub to: Option<BTreeMap<String, Integer>>,

This doesn't compile because the Integer type is unknown.
I assume this is really meant to be i64 (or i32 if a format is given).
I did try a couple of things under the assumption it is really meant to be Integer and we're just missing a use, but serde_json doesn't have an Integer, and it's not num::Integer because that's a trait and they're not used like trait objects.

runtime.RawExtension processing

runtime.RawExtensions serves as a blob in the k8s CRD afaik and is reflected as an object in the openapi spec, like this.

The specific objects are origin and target, which are defined in go as following

// TransformInputSpec struct
type TransformInputSpec struct {
	Origin runtime.RawExtension `json:"origin,omitempty"`
	Target runtime.RawExtension `json:"target,omitempty"`
}
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  annotations:
    controller-gen.kubebuilder.io/version: v0.9.0
  creationTimestamp: null
  name: transforminputs.app.yndd.io
spec:
  group: app.example.io
  names:
    categories:
    - app
    kind: TransformInput
    listKind: TransformInputList
    plural: transforminputs
    singular: transforminput
  scope: Namespaced
  versions:
  - name: v1alpha1
    schema:
      openAPIV3Schema:
        description: TransformInput is the Schema for the TransformInput API
        properties:
          apiVersion:
            description: 'APIVersion defines the versioned schema of this representation
              of an object. Servers should convert recognized schemas to the latest
              internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
            type: string
          kind:
            description: 'Kind is a string value representing the REST resource this
              object represents. Servers may infer this from the endpoint the client
              submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
            type: string
          metadata:
            type: object
          spec:
            description: TransformInputSpec struct
            properties:
              origin:
                type: object
              target:
                type: object
            type: object
        type: object
    served: true
    storage: true

when I use kopium to generate the rust code I get this.

use kube::CustomResource;
use schemars::JsonSchema;
use serde::{Serialize, Deserialize};

/// TransformInputSpec struct
#[derive(CustomResource, Serialize, Deserialize, Clone, Debug, JsonSchema)]
#[kube(
    group = "app.example.io",
    version = "v1alpha1",
    kind = "TransformInput",
    plural = "transforminputs"
)]
#[kube(namespaced)]
pub struct TransformInputSpec {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub origin: Option<TransformInputOrigin>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub target: Option<TransformInputTarget>,
}

#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema)]
pub struct TransformInputOrigin {
}

#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema)]
pub struct TransformInputTarget {
}

As you can see the struct TransformInputOrigin and TransformInputTarget are empty. The issue is that when you serializer/deserializerl it in json/yaml the data behind these object is no longer accessible.
When I change it to the following manually the marshal/unmarshal function works as the serde_json::Value is seen as a generic object in the serializer/deserializer afaik.

use kube::CustomResource;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::Value;

/// TransformInputSpec struct
#[derive(CustomResource, Serialize, Deserialize, Clone, Debug, JsonSchema)]
#[kube(
    group = "app.example.io",
    version = "v1alpha1",
    kind = "TransformInput",
    plural = "transforminputs"
)]
#[kube(namespaced)]
pub struct TransformInputSpec {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub origin: Option<Value>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub target: Option<Value>,
}

Can this be accommodated?

Combine serde field attributes with smarter OutputMember::field_annot

With the -z option; we end up with code as:

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ServerPodSelector {
    #[serde(rename = "matchExpressions")]
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub match_expressions: Option<Vec<ServerPodSelectorMatchExpressions>>,
    #[serde(rename = "matchLabels")]
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub match_labels: Option<BTreeMap<String, serde_json::Value>>,
}

where the sufficient output would have been:

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ServerPodSelector {
    #[serde(default, skip_serializing_if = "Option::is_none",  rename = "matchExpressions")]
    pub match_expressions: Option<Vec<ServerPodSelectorMatchExpressions>>,
    #[serde(default, skip_serializing_if = "Option::is_none", rename = "matchLabels")]
    pub match_labels: Option<BTreeMap<String, serde_json::Value>>,
}

change field_annot (which is currently a dumb string) to take a vector of serde annotations so we can add more elsewhere.

CRD can't use on k8s_openapi::List, when it not trait.

Hi All,
I think this is a issue of listed all Objects in a CRD
Like the output of kubectl get <CRD> -o json shows
kopium provides CRD conversion Struct provides k8s_openapi for use
But when the items of k8s_openapi::List::default() map the CRD output will got some issue

The expected output behavior should be something like

It will appear the trait ``ListableResource`` is not implemented for <CRD-Name>

image

In my solution is following information needs to be added to achieve List output

According to k8s_openapi's impl / trait operations
https://docs.rs/k8s-openapi/latest/k8s_openapi/struct.List.html
https://docs.rs/k8s-openapi/latest/k8s_openapi/trait.ListableResource.html
https://docs.rs/k8s-openapi/latest/k8s_openapi/trait.Resource.html

  • impl the ListableResource && Resource

Resource Info is Mapping to Your CRD

image

  • Add the derive Attribute on CRD Struct

image

For this Issue, Does anyone have a better solution?

struct members sometimes refer to wrong struct types

Tried to run kopium on:

kopium applications.argoproj.io --docs > argoapp.rs

Get the following due to some of the CRD's being self-referential:

error[E0072]: recursive type `ApplicationStatusSyncComparedToSourceDirectory` has infinite size
    --> src/argoapp.rs:1330:1
     |
1330 | pub struct ApplicationStatusSyncComparedToSourceDirectory {
     | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ recursive type has infinite size
...
1336 |     pub jsonnet: Option<ApplicationStatusSyncComparedToSourceDirectory>,
     |                  ------------------------------------------------------ recursive without indirection
     |
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to make `ApplicationStatusSyncComparedToSourceDirectory` representable
     |
1336 |     pub jsonnet: Box<Option<ApplicationStatusSyncComparedToSourceDirectory>>,
     |                  ++++                                                      +

I think I can fixup these myself by Boxing them, any other ideas?

pathological naming schemes can create clashing structs

Problem

suppose you have a struct endpoints in the schema with properties:

  • metrics_relabelings (generating EndpointsMetricsRelabelings struct)
  • metrics (generating EndpointsMetrics struct)

suppose further that the metrics property in the schema has further properties:

  • relabelings (generating EndpointsMetricsRelabelings struct - a second time!)

I.e. it is possible to have multiple ways to reach the same struct names, and kopium does not handle this (it will emit multiple structs with the same name).

Potential workarounds

  • use smaller modules (mod Endpoints { mod Metrics {... to create a namespace for each struct level
  • naming disambiguation at insert time (we have the full list of OutputStruct available when adding another)

Leaning towards the latter, even though it will likely just be us suffixing numbers (EndpointsMetricsRelabelings2) to the struct and hoping the users do manual disambiguation where possible.

handle validation

kube-derive supports using schemars + validator to inject validation rules into the schema.

We should similarly parse the validation attributes from the schema, and store them as properties on OutputMember, so we can (optionally) add them to the generated code. (See crd_api example for how the output is supposed to look (ignore the deprecation attrs)).

Fields to look for (that we currently do not cover):

  • exclusiveMaximum
  • exclusiveMinimum
  • maximum
  • maxItems
  • maxLength
  • minimum
  • minItems
  • minLength
  • multipleOf
  • pattern
  • uniqueItems

These are mentioned in the kubernetes crd docs.

Don't think we can verify all of them, but should be able to do min/max stuff + patterns with some of the basics from validator. Partial attempts at some of the properties is welcome.

At the very least this needs:

  • flags for OutputMember to store information found during analysis
  • --validation bool flag for cli to optionally enable it
  • crd with a test case for CI

Cannot use derived schemas when IntOrString present

Minor issue when using the fully automatic schemas against something using IntOrString:

error[E0277]: the trait bound `IntOrString: JsonSchema` is not satisfied
   --> tests/./gen.rs:15:15
    |
15  |     pub port: IntOrString,
    |               ^^^^^^^^^^^ the trait `JsonSchema` is not implemented for `IntOrString`
    |
note: required by a bound in `SchemaGenerator::subschema_for`
   --> /home/clux/.cargo/registry/src/github.com-1ecc6299db9ec823/schemars-0.8.8/src/gen.rs:218:38
    |
218 |     pub fn subschema_for<T: ?Sized + JsonSchema>(&mut self) -> Schema {
    |                                      ^^^^^^^^^^ required by this bound in `SchemaGenerator::subschema_for`

error[E0277]: the trait bound `IntOrString: JsonSchema` is not satisfied
 --> tests/./gen.rs:7:64
  |
7 | #[derive(CustomResource, Serialize, Deserialize, Clone, Debug, JsonSchema)]
  |                                                                ^^^^^^^^^^ the trait `JsonSchema` is not implemented for `IntOrString`
  |
  = note: this error originates in the derive macro `JsonSchema` (in Nightly builds, run with -Z macro-backtrace for more info)

some potential solutions:

  • get an impl in k8s-openapi
  • inline IntOrString with JsonSchema derive (awkward, but avoids user dep on k8s-openapi in some cases perhaps)

This isn't a big deal because it's only the schema part, we can still generate the structs.

Workaround is not use -A or --schema=derived with such schemas, but use --schema=manual or the default and read the schema from a file if needed (if you need to overwrite the crd at some point).

handle simple enum types

Currently, I think we will error out, or omit enum parts of the schema.

It would be helpful to either find a good / official CRD from somewhere that relies on enums, or even better, a POC PR that showcases how to parse them.

Lack of mangling for non-identifier properties

I encountered a crd in the wild that contains the following snippet:

              jvmOptions:
                description: JVM Options for pods.
                properties:
                  -XX:
                    description: A map of -XX options to the JVM.
                    type: object
                    x-kubernetes-preserve-unknown-fields: true
                  -Xms:
                    description: -Xms option to to the JVM.
                    pattern: ^[0-9]+[mMgG]?$
                    type: string

(Read with care, Github's highlighter is wrong here. This is not a list.)

which results in the following panic:

thread 'main' panicked at '"-XX" is not a valid Ident', /root/.cargo/registry/src/github.com-1ecc6299db9ec823/proc-macro2-1.0.36/src/fallback.rs:708:9
stack backtrace:
   0: rust_begin_unwind
             at /rustc/1.59.0/library/std/src/panicking.rs:498:5
   1: core::panicking::panic_fmt
             at /rustc/1.59.0/library/core/src/panicking.rs:116:14
   2: proc_macro2::fallback::validate_ident
             at /root/.cargo/registry/src/github.com-1ecc6299db9ec823/proc-macro2-1.0.36/src/fallback.rs:708:9
   3: proc_macro2::fallback::Ident::_new
             at /root/.cargo/registry/src/github.com-1ecc6299db9ec823/proc-macro2-1.0.36/src/fallback.rs:642:9
   4: proc_macro2::fallback::Ident::new
             at /root/.cargo/registry/src/github.com-1ecc6299db9ec823/proc-macro2-1.0.36/src/fallback.rs:652:9
   5: proc_macro2::imp::Ident::new
             at /root/.cargo/registry/src/github.com-1ecc6299db9ec823/proc-macro2-1.0.36/src/wrapper.rs:691:50
   6: proc_macro2::Ident::new
             at /root/.cargo/registry/src/github.com-1ecc6299db9ec823/proc-macro2-1.0.36/src/lib.rs:952:21
   7: quote::__private::mk_ident
             at /root/.cargo/registry/src/github.com-1ecc6299db9ec823/quote-1.0.15/src/runtime.rs:404:17
   8: kopium::Kopium::generate::{{closure}}
             at ./src/main.rs:189:29
   9: <core::future::from_generator::GenFuture<T> as core::future::future::Future>::poll
             at /rustc/1.59.0/library/core/src/future/mod.rs:84:19
  10: kopium::Kopium::dispatch::{{closure}}
             at ./src/main.rs:116:41
  11: <core::future::from_generator::GenFuture<T> as core::future::future::Future>::poll
             at /rustc/1.59.0/library/core/src/future/mod.rs:84:19
  12: kopium::main::{{closure}}
             at ./src/main.rs:106:35
[โ€ฆtokio cruftโ€ฆ]

I think, in general, it would be nice to have some kind of name mangling that converts all identifiers to nice camel_case. Might require #[serde(rename = "โ€ฆ")] on all fields.

Enum members need to be PascalCased

Currently, we leave enum names in place, resulting in awkward enum names such as:

#[derive(Serialize, Deserialize, Clone, Debug)]
pub enum ServiceMonitorEndpointsMetricRelabelingsAction {
    replace,
    keep,
    drop,
    hashmod,
    labelmap,
    labeldrop,
    labelkeep,
}

that will generate warnings:

349 |     labelmap,
    |     ^^^^^^^^ help: convert the identifier to upper camel case: `Labelmap`

we can rename these properties in the same way we rename members, but we need to use heck::ToPascalCase instead with the same #[serde(rename = "original_name")] procedure.

Flag choices

We have a -z aka --snake-case flag for renaming currently (included under -A).

We could add another -p aka --pascal-case or something similar (and include it under -A), but it sounds like it would be mutually exclusive with -z.

So think we probably should scrap the current flag and make a flag for "rust conventional casings" which means snake_case for struct members, and PascalCase for enum members.

Not sure what that flag should be named though. -r for rust casing? -c for case-rename?

Allow reading CRD from stdin rather than file

kopium -f crd.yaml works, but cat crd.yaml | kopium -f - does not unfortunately.

minor snag, but if it's fixable in structopt, or has a viable alternative in clap (but preserves autocomplte) i would want to do it

struct members should be snake_case

To be consistent with k8s-openapi, and general rust conventions; we should output the following:

#[serde(rename_all = "camelCase")]
pub struct ClusterSpec {
    pub cluster_network: Option<ClusterNetwork>,
    pub control_plane_endpoint: Option<ApiEndpoint>,
    pub control_plane_ref: Option<core::v1::ObjectReference>,
    pub infrastructure_ref: Option<core::v1::ObjectReference>,
    pub paused: Option<bool>,
    pub topology: Option<Topology>,
}

rather than what we are outputting:

pub struct ClusterSpec {
    pub clusterNetwork: Option<ClusterNetwork>,
    pub controlPlaneEndpoint: Option<ApiEndpoint>,
    pub controlPlaneRef: Option<core::v1::ObjectReference>,
    pub infrastructureRef: Option<core::v1::ObjectReference>,
    pub paused: Option<bool>,
    pub topology: Option<Topology>,
}

(the latter would require an explicit #[allow(non_snake_case)] override..)

Doing this would require:

  • some method / library to handle the conversion from camelCase to snake_case of m.name
  • emit an additional #[serde(rename_all = "camelCase")] in front of every struct

Allow generating from crd yaml rather than crd in cluster

To better improve the flow of users wanting more deterministic builds and have crd schemas committed into their repositories.

This also allows these types of CI steps to work without cluster access.

Marking this as a good-first-issue. The change would be entirely within main. It would need:

  • an alternate to fetch the CustomResourceDefinition by reading a file from a path
  • a flag in the arg parser to take an optional file path (which needs to elide the kube client creation)

Type name generation for complex spec is broken

I am running kopium against cluster-api CRDs and generated structs appear incorrect

Here is what Rust code for top-level object clusters.cluster.x-k8s.io

#[derive(CustomResource, Serialize, Deserialize, Clone, Debug)]
#[kube(group = "cluster.x-k8s.io", version = "v1beta1", kind = "Cluster", plural = "clusters")]
#[kube(namespaced)]
#[kube(schema = "disabled")]
pub struct ClusterSpec {
    pub clusterNetwork: Option<Cluster>,
    pub controlPlaneEndpoint: Option<Cluster>,
    pub controlPlaneRef: Option<Cluster>,
    pub infrastructureRef: Option<Cluster>,
    pub paused: Option<bool>,
    pub topology: Option<Cluster>,
}
...

This is definitely not correct as the struct cannot reference itself

Here is what I think it should be producing (ignore field names, types are important)

#[derive(Clone, Debug, Serialize, Deserialize, CustomResource)]
#[serde(rename_all = "camelCase")]
#[skip_serializing_none]
#[kube(
    group = "cluster.x-k8s.io",
    version = "v1beta1",
    kind = "Cluster",
    plural = "clusters",
)]
#[kube(namespaced)]
#[kube(schema = "disabled")]
pub struct ClusterSpec {
    pub cluster_network: Option<ClusterNetwork>,
    pub control_plane_endpoint: Option<ApiEndpoint>,
    pub control_plane_ref: Option<core::v1::ObjectReference>,
    pub infrastructure_ref: Option<core::v1::ObjectReference>,
    pub paused: Option<bool>,
    pub topology: Option<Topology>,
}
...

enum support in 0.12 breaks --derive Default

the enums generated by 0.12 (#65) don't label which variant is the default, so if you try using --derive Default, you will end up with a lot of errors like

error: no default declared
    --> foo/bar/baz.rs:1116:48
     |
1116 | #[derive(Serialize, Deserialize, Clone, Debug, Default)]
     |                                                ^^^^^^^
     |
     = help: make a unit variant default by placing `#[default]` above it
     = note: this error originates in the derive macro `Default` (in Nightly builds, run with -Z macro-backtrace for more info)

alertmanager crd crashes on empty enum string crd

From their schema:

              logFormat:
                description: Log format for Alertmanager to be configured with.
                enum:
                - ""
                - logfmt
                - json

this makes us want to create a variant with an empty name which does not work.

A suggested fix is to check for name.is_empty() and override it to EmptyString or something, but that runs the risk of clashing with other (potential) values in the enum (like if they had had an "empty" logformat). We could use a big KopiumEmptyString type thing, but that feels very ugly and wrong.

Another way to look at this is as just a degenerate case of #92, and this makes us use unspecified variant names for all elements instead. I think that's probably better (and less magic).

not handling non-string enum

Hi ๐Ÿ‘‹

When using kopium to generate Kubernetes Gateway API resources, I found that it's not capable of generating one of the APIs, HTTPRoute specifically:

$ curl -sSL 'https://raw.githubusercontent.com/kubernetes-sigs/gateway-api/main/config/crd/standard/gateway.networking.k8s.io_httproutes.yaml?ref=v0.5.1' | kopium -Af -
// WARNING: generated by kopium - manual changes will be overwritten
Error: not handling non-string enum

I found the line here. Looks like there's just more TODO here to add support? Is there already work tracking this TODO?

Kopium uses `Array` instead of `Vec` for array fields

I was generating the rust code for this CRD and noticed that the code generated is using Array instead of Vec for some fields.

Here's the CRD

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.6.2
installer.open-cluster-management.io/last-applied-configuration: ce84b11fb18eb5436f84845f6c462e1f6773cc46
creationTimestamp: "2022-02-14T14:03:43Z"
generation: 1
labels:
installer.name: multiclusterhub
installer.namespace: open-cluster-management
name: agents.agent-install.openshift.io
resourceVersion: "270891"
uid: 741e93d1-2f89-4945-8975-09a90983ba8f
spec:
conversion:
strategy: None
group: agent-install.openshift.io
names:
kind: Agent
listKind: AgentList
plural: agents
singular: agent
scope: Namespaced
versions:

  • additionalPrinterColumns:
    • description: The name of the cluster the Agent registered to.
      jsonPath: .spec.clusterDeploymentName.name
      name: Cluster
      type: string
    • description: The Approve state of the Agent.
      jsonPath: .spec.approved
      name: Approved
      type: boolean
    • description: The role (master/worker) of the Agent.
      jsonPath: .status.role
      name: Role
      type: string
    • description: The HostStage of the Agent.
      jsonPath: .status.progress.currentStage
      name: Stage
      type: string
    • description: The hostname of the Agent.
      jsonPath: .status.inventory.hostname
      name: Hostname
      priority: 1
      type: string
    • description: The requested hostname for the Agent.
      jsonPath: .spec.hostname
      name: Requested Hostname
      priority: 1
      type: string
      name: v1beta1
      schema:
      openAPIV3Schema:
      description: Agent is the Schema for the hosts API
      properties:
      apiVersion:
      description: 'APIVersion defines the versioned schema of this representation
      of an object. Servers should convert recognized schemas to the latest
      internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
      type: string
      kind:
      description: 'Kind is a string value representing the REST resource this
      object represents. Servers may infer this from the endpoint the client
      submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
      type: string
      metadata:
      type: object
      spec:
      description: AgentSpec defines the desired state of Agent
      properties:
      approved:
      type: boolean
      clusterDeploymentName:
      description: ClusterReference represents a Cluster Reference. It has
      enough information to retrieve cluster in any namespace
      properties:
      name:
      description: Name is unique within a namespace to reference a
      cluster resource.
      type: string
      namespace:
      description: Namespace defines the space within which the cluster
      name must be unique.
      type: string
      type: object
      hostname:
      type: string
      ignitionConfigOverrides:
      description: Json formatted string containing the user overrides for
      the host's ignition config
      type: string
      installation_disk_id:
      description: InstallationDiskID defines the installation destination
      disk (must be equal to the inventory disk id).
      type: string
      installerArgs:
      description: Json formatted string containing the user overrides for
      the host's coreos installer args
      type: string
      machineConfigPool:
      type: string
      role:
      description: "HostRole host role \n swagger:model host-role"
      type: string
      required:
      - approved
      - role
      type: object
      status:
      description: AgentStatus defines the observed state of Agent
      properties:
      bootstrap:
      type: boolean
      conditions:
      items:
      description: Condition represents the state of the operator's reconciliation
      functionality.
      properties:
      lastHeartbeatTime:
      format: date-time
      type: string
      lastTransitionTime:
      format: date-time
      type: string
      message:
      type: string
      reason:
      type: string
      status:
      type: string
      type:
      description: ConditionType is the state of the operator's reconciliation
      functionality.
      type: string
      required:
      - status
      - type
      type: object
      type: array
      debugInfo:
      description: DebugInfo includes information for debugging the installation
      process.
      properties:
      eventsURL:
      description: EventsURL specifies an HTTP/S URL that contains events
      which occured during the cluster installation process
      type: string
      state:
      description: Current state of the Agent
      type: string
      stateInfo:
      description: Additional information pertaining to the status of
      the Agent
      type: string
      type: object
      hostname:
      type: string
      inventory:
      properties:
      bmcAddress:
      type: string
      bmcV6Address:
      type: string
      boot:
      properties:
      currentBootMode:
      type: string
      pxeInterface:
      type: string
      type: object
      cpu:
      properties:
      architecture:
      type: string
      clockMegahertz:
      description: 'Name in REST API: frequency'
      format: int64
      type: integer
      count:
      format: int64
      type: integer
      flags:
      items:
      type: string
      type: array
      modelName:
      type: string
      type: object
      disks:
      items:
      properties:
      bootable:
      type: boolean
      byID:
      type: string
      byPath:
      type: string
      driveType:
      type: string
      hctl:
      type: string
      id:
      type: string
      installationEligibility:
      properties:
      eligible:
      type: boolean
      notEligibleReasons:
      items:
      type: string
      type: array
      required:
      - notEligibleReasons
      type: object
      ioPerf:
      properties:
      syncDurationMilliseconds:
      description: 99th percentile of fsync duration in milliseconds
      format: int64
      type: integer
      type: object
      model:
      type: string
      name:
      type: string
      path:
      type: string
      serial:
      type: string
      sizeBytes:
      format: int64
      type: integer
      smart:
      type: string
      vendor:
      type: string
      wwn:
      type: string
      required:
      - id
      type: object
      type: array
      hostname:
      type: string
      interfaces:
      items:
      properties:
      biosDevName:
      type: string
      clientID:
      type: string
      flags:
      items:
      type: string
      type: array
      hasCarrier:
      type: boolean
      ipV4Addresses:
      items:
      type: string
      type: array
      ipV6Addresses:
      items:
      type: string
      type: array
      macAddress:
      type: string
      mtu:
      format: int64
      type: integer
      name:
      type: string
      product:
      type: string
      speedMbps:
      format: int64
      type: integer
      vendor:
      type: string
      required:
      - flags
      - ipV4Addresses
      - ipV6Addresses
      type: object
      type: array
      memory:
      properties:
      physicalBytes:
      format: int64
      type: integer
      usableBytes:
      format: int64
      type: integer
      type: object
      reportTime:
      description: 'Name in REST API: timestamp'
      format: date-time
      type: string
      systemVendor:
      properties:
      manufacturer:
      type: string
      productName:
      type: string
      serialNumber:
      type: string
      virtual:
      type: boolean
      type: object
      type: object
      ntpSources:
      items:
      properties:
      sourceName:
      type: string
      sourceState:
      description: "SourceState source state \n swagger:model source_state"
      type: string
      type: object
      type: array
      progress:
      properties:
      currentStage:
      description: "HostStage host stage \n swagger:model host-stage"
      type: string
      progressInfo:
      type: string
      stageStartTime:
      description: 'Name in REST API: stage_started_at'
      format: date-time
      type: string
      stageUpdateTime:
      description: 'Name in REST API: stage_updated_at'
      format: date-time
      type: string
      type: object
      role:
      description: "HostRole host role \n swagger:model host-role"
      type: string
      validationsInfo:
      additionalProperties:
      items:
      properties:
      id:
      type: string
      message:
      type: string
      status:
      type: string
      required:
      - id
      - message
      - status
      type: object
      type: array
      description: ValidationsInfo is a JSON-formatted string containing
      the validation results for each validation id grouped by category
      (network, hosts-data, etc.)
      type: object
      type: object
      type: object
      served: true
      storage: true
      subresources:
      status: {}
      status:
      acceptedNames:
      kind: Agent
      listKind: AgentList
      plural: agents
      singular: agent
      conditions:
  • lastTransitionTime: "2022-02-14T14:03:43Z"
    message: no conflicts found
    reason: NoConflicts
    status: "True"
    type: NamesAccepted
  • lastTransitionTime: "2022-02-14T14:03:43Z"
    message: the initial names have been accepted
    reason: InitialNamesAccepted
    status: "True"
    type: Established
    storedVersions:
  • v1beta1
use kube::CustomResource;
use serde::{Serialize, Deserialize};
use std::collections::BTreeMap;

#[derive(CustomResource, Serialize, Deserialize, Clone, Debug)]
#[kube(group = "agent-install.openshift.io", version = "v1beta1", kind = "Agent", plural = "agents")]
#[kube(namespaced)]
#[kube(schema = "disabled")]
pub struct AgentSpec {
    pub approved: bool,
    pub clusterDeploymentName: Option<AgentClusterDeploymentName>,
    pub hostname: Option<String>,
    pub ignitionConfigOverrides: Option<String>,
    pub installation_disk_id: Option<String>,
    pub installerArgs: Option<String>,
    pub machineConfigPool: Option<String>,
    pub role: String,
}

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct AgentClusterDeploymentName {
    pub name: Option<String>,
    pub namespace: Option<String>,
}

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct AgentStatus {
    pub bootstrap: Option<bool>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub conditions: Vec<AgentStatusConditions>,
    pub debugInfo: Option<AgentStatusDebugInfo>,
    pub hostname: Option<String>,
    pub inventory: Option<AgentStatusInventory>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub ntpSources: Vec<AgentStatusNtpSources>,
    pub progress: Option<AgentStatusProgress>,
    pub role: Option<String>,
    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
    pub validationsInfo: BTreeMap<String, Array>,
}

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct AgentStatusConditions {
    pub lastHeartbeatTime: Option<String>,
    pub lastTransitionTime: Option<String>,
    pub message: Option<String>,
    pub reason: Option<String>,
    pub status: String,
    pub r#type: String,
}

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct AgentStatusDebugInfo {
    pub eventsURL: Option<String>,
    pub state: Option<String>,
    pub stateInfo: Option<String>,
}

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct AgentStatusInventory {
    pub bmcAddress: Option<String>,
    pub bmcV6Address: Option<String>,
    pub boot: Option<AgentStatusInventoryBoot>,
    pub cpu: Option<AgentStatusInventoryCpu>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub disks: Vec<AgentStatusInventoryDisks>,
    pub hostname: Option<String>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub interfaces: Vec<AgentStatusInventoryInterfaces>,
    pub memory: Option<AgentStatusInventoryMemory>,
    pub reportTime: Option<String>,
    pub systemVendor: Option<AgentStatusInventorySystemVendor>,
}

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct AgentStatusInventoryBoot {
    pub currentBootMode: Option<String>,
    pub pxeInterface: Option<String>,
}

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct AgentStatusInventoryCpu {
    pub architecture: Option<String>,
    pub clockMegahertz: Option<i64>,
    pub count: Option<i64>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub flags: Vec<String>,
    pub modelName: Option<String>,
}

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct AgentStatusInventoryDisks {
    pub bootable: Option<bool>,
    pub byID: Option<String>,
    pub byPath: Option<String>,
    pub driveType: Option<String>,
    pub hctl: Option<String>,
    pub id: String,
    pub installationEligibility: Option<AgentStatusInventoryDisksInstallationEligibility>,
    pub ioPerf: Option<AgentStatusInventoryDisksIoPerf>,
    pub model: Option<String>,
    pub name: Option<String>,
    pub path: Option<String>,
    pub serial: Option<String>,
    pub sizeBytes: Option<i64>,
    pub smart: Option<String>,
    pub vendor: Option<String>,
    pub wwn: Option<String>,
}

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct AgentStatusInventoryDisksInstallationEligibility {
    pub eligible: Option<bool>,
    pub notEligibleReasons: Vec<String>,
}

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct AgentStatusInventoryDisksIoPerf {
    pub syncDurationMilliseconds: Option<i64>,
}

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct AgentStatusInventoryInterfaces {
    pub biosDevName: Option<String>,
    pub clientID: Option<String>,
    pub flags: Vec<String>,
    pub hasCarrier: Option<bool>,
    pub ipV4Addresses: Vec<String>,
    pub ipV6Addresses: Vec<String>,
    pub macAddress: Option<String>,
    pub mtu: Option<i64>,
    pub name: Option<String>,
    pub product: Option<String>,
    pub speedMbps: Option<i64>,
    pub vendor: Option<String>,
}

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct AgentStatusInventoryMemory {
    pub physicalBytes: Option<i64>,
    pub usableBytes: Option<i64>,
}

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct AgentStatusInventorySystemVendor {
    pub manufacturer: Option<String>,
    pub productName: Option<String>,
    pub serialNumber: Option<String>,
    pub virtual: Option<bool>,
}

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct AgentStatusNtpSources {
    pub sourceName: Option<String>,
    pub sourceState: Option<String>,
}

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct AgentStatusProgress {
    pub currentStage: Option<String>,
    pub progressInfo: Option<String>,
    pub stageStartTime: Option<String>,
    pub stageUpdateTime: Option<String>,
}

add integration tests for a crd on k3d

  • add a crd from prometheus-operator (e.g. prometheusrule)
  • add k3d github action
  • start an integration test with the generated code on Api<PrometheusRule>
  • ensure the cluster has one prometheusrule installed
  • verify that api.list works and parses)

allow handling IntOrString

Currently, structs that allow IntOrString like kubernetes resources/limits are coerced to strings.

It would be good to handle this properly, by implementing a custom serializer/deserializer around a newtype IntOrString.
Currently, CRDs that use IntOrString might not work well with kopium because of this.

handle preserve-unknown-fields

via https://twitter.com/matteojoliveau/status/1490747666707496961

A CRD that sets x-kubernetes-preserve-unknown-fields can effectively put whatever they want into the schema without specifying it in the schema.

Currently, kopium does not generate anything for this CRD, and this property is not handled at all.

We should handle the property, and where it is seen, it should include an arbitrary json value that can be flattened into the struct as a member:

struct FooSpecWithPreserveProperty {
    #[serde(flatten)]
    pub unknowns: serde_json::Value,
}

This is probably not that bad to for a new contributor to try out as it's mostly just work in the analyzer.
In particular, it requires more logic in the initial type examination to handle the extra property, but probably a bit subtlety since we are not actually examining a member to get the this member and type: we get it from the object instead (if the object has the property, we need this extra overflow type that always looks like the above).

It also needs a test crd, maybe the image.kpack.io thing from the tweet if we can find it, all i can find is a doc spec for it but not the crd.

Simplify interfaces and restructure logic

Jotting down some quick refactoring thoughts before bed. Will likely look at this very soon.

main.rs is getting crowded and it does not need to have all the specifics of formatting output, which can live in lib.rs or a transform.rs

this file should have:

  • reserved constants
  • extra members to help factor out some of the annotation printing
  • rename methods that allows us to test casings
  • tests for structs -> printed output

Not sure exactly where the right trade-off between confusing-indirection vs. too much in one fine lies. But it's getting a bit too hairy now with enums. Might end up merging #63 because need similar annotation mutation for renaming fields for enums.

Additionally; analyze.rs should have an easier analyze like entry point without the current and level vars. These have to be set all the time, which makes unit tests noisier/more complicated than they need to be.

ServiceMonitor generates invalid maps

Running:

curl -sSL https://raw.githubusercontent.com/prometheus-operator/prometheus-operator/main/example/prometheus-operator-crd/monitoring.coreos.com_servicemonitors.yaml  \
  |  kopium -f - -A > servicemonito.rs

with a very small member:

   params:
        additionalProperties:
          items:
            type: string
          type: array
        description: Optional HTTP URL parameters
        type: object

results in an invalid entry:

pub struct ServiceMonitorEndpoints {
    /// ...
    /// Optional HTTP URL parameters
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub params: Option<BTreeMap<String, ServiceMonitorEndpointsParams>>,
    /// ...
}

where there's obviously no ServiceMonitorEndpointsParams. it should be map String -> String

parametrise outputting

Add extra flags to clap args and handle them:

  • --hide-prelude bool (in case people need custom crate names)
  • --api-version 1 optional setter (otherwise we should pick .first version as is) - #15
  • --derive PartialEq optional (multi) setter (appends to derive list per struct)

Derive option does not allow --derive JsonSchema

A pretty fundamental thing to have in some cases and it would help with our currently disabled schema generation.

Add support for it and add it to the prelude (think this is necessary) if using it.

Missing Struct when generating from CRD

The following CRD does not generate the struct AppProjectStatusJwtTokensByRoleItems, but references it (correctly):

          status:
            description: AppProjectStatus contains status information for AppProject
              CRs
            properties:
              jwtTokensByRole:
                additionalProperties:
                  description: JWTTokens represents a list of JWT tokens
                  properties:
                    items:
                      items:
                        description: JWTToken holds the issuedAt and expiresAt values
                          of a token
                        properties:
                          exp:
                            format: int64
                            type: integer
                          iat:
                            format: int64
                            type: integer
                          id:
                            type: string
                        required:
                        - iat
                        type: object
                      type: array
                  type: object
                description: JWTTokensByRole contains a list of JWT tokens issued
                  for a given role
                type: object
            type: object

Generates:

/// AppProjectStatus contains status information for AppProject CRs
#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema)]
pub struct AppProjectStatus {
    /// JWTTokensByRole contains a list of JWT tokens issued for a given role
    #[serde(default, skip_serializing_if = "Option::is_none", rename = "jwtTokensByRole")]
    pub jwt_tokens_by_role: Option<BTreeMap<String, AppProjectStatusJwtTokensByRole>>,
}

/// JWTTokensByRole contains a list of JWT tokens issued for a given role
#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema)]
pub struct AppProjectStatusJwtTokensByRole {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub items: Option<Vec<AppProjectStatusJwtTokensByRoleItems>>,
}

The struct AppProjectStatusJwtTokensByRoleItems does not get generated.

To reproduce: curl -sSL https://raw.githubusercontent.com/argoproj/argo-cd/v2.4.0/manifests/crds/appproject-crd.yaml | kopium -Af -

Using a fresh cargo install kopium

empty `type` field is not handled

When trying to generate code for the cert-manager ClusterIssuer CRD, kopium fails with:

$ kopium clusterissuers.cert-manager.io
Error: unknown empty dict type for config

It seems that the CRD contains a field that has no type set (config):

                              webhook:
                                description: Configure an external webhook based DNS01 challenge solver to manage DNS01 challenge records.
                                type: object
                                required:
                                  - groupName
                                  - solverName
                                properties:
                                  config:
                                    description: Additional configuration that should be passed to the webhook apiserver when challenges are processed. This can contain arbitrary JSON data. Secret values should not be specified in this stanza. If secret values are needed (e.g. credentials for a DNS service), you should use a SecretKeySelector to reference a Secret resource. For details on the schema of this field, consult the webhook provider implementation's documentation.
                                    x-kubernetes-preserve-unknown-fields: true
                                  groupName:
                                    description: The API group name that should be used when POSTing ChallengePayload resources to the webhook apiserver. This should be the same as the GroupName specified in the webhook provider implementation.
                                    type: string
                                  solverName:
                                    description: The name of the solver to use, as defined in the webhook provider implementation. This will typically be the name of the provider, e.g. 'cloudflare'.
                                    type: string

Manually adding type: object makes kopium generate the code correctly.

I am actually not sure if this is allowed, but at least Kubernetes seems to accept emtpy type fields.

split doc comments by sentence

The current output from --docs is one giant line and this could be better. We should split it onto several lines such that:

/// First sentence for main doc line
///
/// second sentence preceded by a blank line as is convention
/// every other sentence on its own line at this point on
struct x {}

Unfortunately, the description properties come from golang and are not necessary broken by anything (no periods to separate sentences, just a regular whitespace).

So to actually split by sentence, we would need something like https://bminixhofer.github.io/nnsplit/ to detect sentence borders.
The english model they have seem quite good from the github pages link, and it's only 4MB (it could possibly be embedded).

A PR that explores this would be very much appreciated (and very likely merged if it's working well).

This proposed change would only touch main.rs, but see #19 for the initial implementation of doc comment extraction.

One-of required fields should generate Rust enums, not Option<T>

As only one value can be supplied at a time, it would be better to generate a Rust enum, so that the type system can enforce the one-of constraint.

Example:

              podSelector:
                description: Selects pods in the same namespace.
                oneOf:
                - required:
                  - matchExpressions
                - required:
                  - matchLabels
                properties:
                  matchExpressions:
                    items:
                      properties:
                        key:
                          type: string
                        operator:
                          enum:
                          - In
                          - NotIn
                          - Exists
                          - DoesNotExist
                          type: string
                        values:
                          items:
                            type: string
                          type: array
                      required:
                      - key
                      - operator
                      type: object
                    type: array
                  matchLabels:
                    type: object
                    x-kubernetes-preserve-unknown-fields: true
                type: object

This generates:

/// Selects pods in the same namespace.
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ServerPodSelector {
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub matchExpressions: Vec<ServerPodSelectorMatchExpressions>,
    pub matchLabels: Option<ServerPodSelectorMatchLabels>,
}

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ServerPodSelectorMatchExpressions {
    pub key: String,
    pub operator: String,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub values: Vec<String>,
}

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ServerPodSelectorMatchLabels {
}

Complete CRD https://gist.github.com/alex-hunt-materialize/2743b1e2e58a49c4df0a11ecb39f46ab

crash on flux's GitRepository crd

output:

#![allow(non_snake_case)]

use kube::CustomResource;
use serde::{Serialize, Deserialize};
use std::collections::BTreeMap;

#[derive(CustomResource, Serialize, Deserialize, Clone, Debug)]
#[kube(group = "source.toolkit.fluxcd.io", version = "v1beta2", kind = "GitRepository", plural = "gitrepositories")]
#[kube(namespaced)]
#[kube(status = "GitRepositoryStatus")]
#[kube(schema = "disabled")]
pub struct GitRepositorySpec {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub accessFrom: Option<GitRepositoryAccessFrom>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub gitImplementation: Option<GitRepositoryGitImplementation>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub ignore: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub include: Option<Vec<GitRepositoryInclude>>,
    pub interval: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub recurseSubmodules: Option<bool>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub r#ref: Option<GitRepositoryRef>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub secretRef: Option<GitRepositorySecretRef>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub suspend: Option<bool>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub timeout: Option<String>,
    pub url: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub verify: Option<GitRepositoryVerify>,
}

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct GitRepositoryAccessFrom {
    pub namespaceSelectors: Vec<GitRepositoryAccessFromNamespaceSelectors>,
}

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct GitRepositoryAccessFromNamespaceSelectors {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub matchLabels: Option<BTreeMap<String, String>>,
}

#[derive(Serialize, Deserialize, Clone, Debug)]
pub enum GitRepositoryGitImplementation {
thread 'main' panicked at '"go-git" is not a valid Ident', /home/clux/.cargo/registry/src/github.com-1ecc6299db9ec823/proc-macro2-1.0.42/src/fallback.rs:730:9
stack backtrace:
   0: rust_begin_unwind
             at /rustc/fe5b13d681f25ee6474be29d748c65adcd91f69e/library/std/src/panicking.rs:584:5
   1: core::panicking::panic_fmt
             at /rustc/fe5b13d681f25ee6474be29d748c65adcd91f69e/library/core/src/panicking.rs:143:14
   2: proc_macro2::fallback::validate_ident
             at /home/clux/.cargo/registry/src/github.com-1ecc6299db9ec823/proc-macro2-1.0.42/src/fallback.rs:730:9
   3: proc_macro2::fallback::Ident::_new
             at /home/clux/.cargo/registry/src/github.com-1ecc6299db9ec823/proc-macro2-1.0.42/src/fallback.rs:672:9
   4: proc_macro2::fallback::Ident::new
             at /home/clux/.cargo/registry/src/github.com-1ecc6299db9ec823/proc-macro2-1.0.42/src/fallback.rs:682:9
   5: proc_macro2::imp::Ident::new
             at /home/clux/.cargo/registry/src/github.com-1ecc6299db9ec823/proc-macro2-1.0.42/src/wrapper.rs:685:50
   6: proc_macro2::Ident::new
             at /home/clux/.cargo/registry/src/github.com-1ecc6299db9ec823/proc-macro2-1.0.42/src/lib.rs:953:21
   7: quote::__private::ident_maybe_raw
             at /home/clux/.cargo/registry/src/github.com-1ecc6299db9ec823/quote-1.0.20/src/runtime.rs:391:9
   8: quote::__private::mk_ident
             at /home/clux/.cargo/registry/src/github.com-1ecc6299db9ec823/quote-1.0.20/src/runtime.rs:384:5
   9: kopium::Kopium::generate::{{closure}}
             at /home/clux/kube/kopium/src/main.rs:246:29
  10: <core::future::from_generator::GenFuture<T> as core::future::future::Future>::poll
             at /rustc/fe5b13d681f25ee6474be29d748c65adcd91f69e/library/core/src/future/mod.rs:91:19
  11: kopium::Kopium::dispatch::{{closure}}
             at /home/clux/kube/kopium/src/main.rs:158:31
  12: <core::future::from_generator::GenFuture<T> as core::future::future::Future>::poll
             at /rustc/fe5b13d681f25ee6474be29d748c65adcd91f69e/library/core/src/future/mod.rs:91:19
  13: kopium::main::{{closure}}
             at /home/clux/kube/kopium/src/main.rs:130:20
  14: <core::future::from_generator::GenFuture<T> as core::future::future::Future>::poll
             at /rustc/fe5b13d681f25ee6474be29d748c65adcd91f69e/library/core/src/future/mod.rs:91:19
  15: tokio::park::thread::CachedParkThread::block_on::{{closure}}
             at /home/clux/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.20.1/src/park/thread.rs:263:54
  16: tokio::coop::with_budget::{{closure}}
             at /home/clux/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.20.1/src/coop.rs:102:9
  17: std::thread::local::LocalKey<T>::try_with
             at /rustc/fe5b13d681f25ee6474be29d748c65adcd91f69e/library/std/src/thread/local.rs:442:16
  18: std::thread::local::LocalKey<T>::with
             at /rustc/fe5b13d681f25ee6474be29d748c65adcd91f69e/library/std/src/thread/local.rs:418:9
  19: tokio::coop::with_budget
             at /home/clux/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.20.1/src/coop.rs:95:5
  20: tokio::coop::budget
             at /home/clux/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.20.1/src/coop.rs:72:5
  21: tokio::park::thread::CachedParkThread::block_on
             at /home/clux/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.20.1/src/park/thread.rs:263:31
  22: tokio::runtime::enter::Enter::block_on
             at /home/clux/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.20.1/src/runtime/enter.rs:152:13
  23: tokio::runtime::thread_pool::ThreadPool::block_on
             at /home/clux/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.20.1/src/runtime/thread_pool/mod.rs:90:9
  24: tokio::runtime::Runtime::block_on
             at /home/clux/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.20.1/src/runtime/mod.rs:484:43
  25: kopium::main
             at /home/clux/kube/kopium/src/main.rs:130:5
  26: core::ops::function::FnOnce::call_once
             at /rustc/fe5b13d681f25ee6474be29d748c65adcd91f69e/library/core/src/ops/function.rs:227:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.

after trying to output a member from:

              gitImplementation:
                default: go-git
                description: Determines which git client library to use. Defaults
                  to go-git, valid values are ('go-git', 'libgit2').
                enum:
                - go-git
                - libgit2
                type: string

at the printing stage because of a dash in an ident. need to rename dashes to underscores and preserve these.

struct name duplication

currently struct names are "{kind}{capitalized(key)}`, leading to structs like:

  • PrometheusRuleSpec
  • PrometheusRuleGroup
  • PrometheusRuleRules

but this opens up a potential flow where two structs in different parts of the tree use the same name (leading to the same struct being generated).

Two options we could do:

  1. assume structs with same name are the same underlying struct (e.g. resources key always use a common Resources struct)
  2. denormalize and cascade prefixes into struct names

Option 1 is simple to do (just requires taking the first struct with a given name when printing).

Option 2 will be nice for small structs, but hugely verbose for massive CRDs. For prometheusrules it would result in:

  • PrometheusRuleSpec
  • PrometheusRuleSpecGroup
  • PrometheusRuleSpecGroupRules

Which is OK, I would have liked to trim out Spec, but more importantly it will result in absolutely massive structs for the larger alertmanager/prometheus crds:

  • AlertmanagerSpecAffinityNodeAffinityRequiredDuringSchedulingIgnoredDuringExecution
  • PrometheusSpecAffinityPodAntiAffinityPreferredDuringSchedulingIgnoredDuringExecutionPodAffinityTermLabelSelectorMatchExpressions

We do need to deal with clashes as they are likely, but not super keen on the naive approach either. There is no other data that can help us in the spec (.title is only a thing we fill in with schemars that contains the original struct name).

I'm leaning towards doing the naive approach for now (because it's the only safe approach). Then we can work out if we can support some flags to either deduplicate on key names or optionally allow not cascading names if users wants it.

In either case we need to:

  • extend OutputMember with a dedup_type to match dedup_name in OutputStruct too much complexity in analyzer
  • switch to use dedup names in outputter

so do that, and also:

  • experiment with flags to swap between them
    • if supporting non-cascaded names; also deduplicate structs by .name avoing this complexity
  • experiment with flags to deduplicate structs on keys
  • trim Spec from struct name (sane to do because we require spec/status)

handle default field

Problem

Some schemas can set explicit default: replace to define what defaults are, but we will always just output #[serde(default)] on non-required properties.

Solution

Add an OutputImpls struct and collect things like impl Default for MemberStruct that will ensure #[serde(default)] works and takes into account the correct default values. This is especially important for enums where no default can be derived.

EDIT: This must also handle enums.

Optional array properties should be `Option<Vec<T>>`, not `Vec<T>` with `skip_serializing_if = "Vec::is_empty"`

Currently, optional array properties are required in the generated struct, but skipped if serializing. This is incorrect behavior, since an empty Vec is not the same as not supplying a Vec.

This breaks server-side apply-ing new resources to remove things from the Vec, as well as cases where a property is required in a one-of, but you actually want to send the empty Vec as one of those properties.

We should instead make it an Option<Vec<T>> with #[serde(default, skip_serializing_if = "Option::is_none")].

Example:

              podSelector:
                description: Selects pods in the same namespace.
                oneOf:
                - required:
                  - matchExpressions
                - required:
                  - matchLabels
                properties:
                  matchExpressions:
                    items:
                      properties:
                        key:
                          type: string
                        operator:
                          enum:
                          - In
                          - NotIn
                          - Exists
                          - DoesNotExist
                          type: string
                        values:
                          items:
                            type: string
                          type: array
                      required:
                      - key
                      - operator
                      type: object
                    type: array
                  matchLabels:
                    type: object
                    x-kubernetes-preserve-unknown-fields: true
                type: object

This generates:

/// Selects pods in the same namespace.
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ServerPodSelector {
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub matchExpressions: Vec<ServerPodSelectorMatchExpressions>,
    pub matchLabels: Option<ServerPodSelectorMatchLabels>,
}

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ServerPodSelectorMatchExpressions {
    pub key: String,
    pub operator: String,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub values: Vec<String>,
}

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ServerPodSelectorMatchLabels {
}

Complete CRD https://gist.github.com/alex-hunt-materialize/2743b1e2e58a49c4df0a11ecb39f46ab

What we should get:

/// Selects pods in the same namespace.
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
pub struct ServerPodSelector {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub matchExpressions: Option<Vec<ServerPodSelectorMatchExpressions>>,
    pub matchLabels: Option<ServerPodSelectorMatchLabels>,
}

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ServerPodSelectorMatchExpressions {
    pub key: String,
    pub operator: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub values: Option<Vec<String>>,
}

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ServerPodSelectorMatchLabels {
}

alertmanager crd needs support for invalid enum names

In their schema:

                          matchType:
                            description: Match operation available with AlertManager
                              >= v0.22.0 and takes precedence over Regex (deprecated)
                              if non-empty.
                            enum:
                            - '!='
                            - =
                            - =~
                            - '!~'
                            type: string

This forces us to basically come up with our own names for the enum variants (because we can't simply drop invalid characters because they are all invalid in identifiers - even if wrapped in a rawstring).

So we have an immediate first step:

  • implement a Container::invalid_enum_identifiers(&self) -> bool that checks if any of the members have such a name in the enum
  • call that function in main, and if it's true; use generic Variant0, Variant1 names for them (but with #[serde(rename)] attrs to keep it compliant)

..and as a potential third step; args/config for letting users customize the values of these Variant names since it's pretty unergonomic to work with. First priority is to get it to compile though.

add doc strings to generated code

  • extract .descriptions of props
  • put the information into the OutputMember
  • convert information to printline(s) after analysis
  • add a --doc bool flag to clap args to control whether or not to use it

add auto-complete script

Want to add dynamic completion so that you do kopium TAB and you completion script does kubectl get crds (names only), or alternatively runs the rust code Api<CustomResourceDefinition> with api.list(&Default::default()).await? and uses that.

Think this can probably be done through clap-generate, once the dynamic completion issue lands clap-rs/clap#1232.

Until then, we switched to structopt, so a solution here would be:

  • add a list-crds subcommand
  • write a kopium.complete.sh that completes with list-crds
  • add a completions subcommand that outputs the completion file

allow toggling what map containers to use

Currently kopium generates BTreeMap for all map types except arbitrary json (stored in HashMap).

While this is OK on the surface level, I think we should probably move all containers to HashMap as the cheapest default, and let users do a big global --btreemap flag to switch all of them over to BTreeMap when users need deterministic ordering.

When deriving JsonSchema, optionally attach generated schema to kube

When --derive JsonSchema is set (unless --hide-kube), we can also auto generate the schema for kube.

This could have a few different use cases though:

  • people not needing jsonschema derive, but wanting a manual schem
  • people needing jsonschema, but still wanting to hand generate #[kube(schema = manual)] (possibly to fine-tune)
  • people needing jsonschema, but not kube integration (for e.g. helm schema validation)
  • people needing jsonschema, and wants the generated one

Because of this, it's probably best to have a separate flag for schema selection based on our schema-mode options:

  • --schema=disabled (Default; compiles without schema, but user cannot apply the crd disabled without hacks)
  • --schema=manual (Optional; user is expected to impl JsonSchema for YourSpec, separately for code to compile)
  • --schema=derived (Optional; auto-derive and implies --derive JsonSchema)

infer unsigned integers

Currently the kubernetes schemas only mention support int32 or int64 integer types, but the schemas contain a minimum: f64 where the minimum is set to 0.0 in unsigned cases.

We could maybe check for it and turn the signed ints into unsigneds at that point:

"int32" => {
    match value.minimum {
        Some(0.0) => "u32".to_string(),
        _ => "i32".to_string(),
}

but that's illegal in the future:

warning: floating-point types cannot be used in patterns
   --> src/main.rs:384:26
    |
384 |                     Some(0.0) => "u32".to_string(),
    |                          ^^^
    |
    = note: `#[warn(illegal_floating_point_literal_pattern)]` on by default
    = warning: this was previously accepted by the compiler but is being phased out; it will become a hard error in a future release!
    = note: for more information, see issue #41620 <https://github.com/rust-lang/rust/issues/41620

Could maybe do this by looking at it pre-deserialization to compare against "0.0". But that is if it is even a sane thing to do. Input welcome.

Note that this is only a problem when coming from go; schemars will emit rust's full int range, and we will handle them if set explicitly.

handle byte type

kubernetes schemas may set a byte type. We should maybe package that up in Bytes struct, but need to test it. Need someone with bytes in a CRD to test this.

allow filtering out the status object

Sometimes when writing auxiliary tools/controllers for existing crds, we are sometimes only necessary to generate the spec object for a type. In these cases it would be OK to omit the status objects of the resources.

We should add a --hide-status flag that hides the status object and everything only used by the status object.

This can be done by pruning the schema yaml under:

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
spec:
  versions:
  - name: v1beta1
    schema:
      openAPIV3Schema:
        properties:
          status: <- remove this key for every spec.versions

before running the analyzer.

Leaving this here as a help wanted for now. It might be an interesting place to play around with the schema. We haven't done this type of short-circuiting before, so it might need its own file.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.