Closed gabe-l-hart closed 4 years ago
Hi @gabe-l-hart - Thanks for kicking off this discussion!
Just to make sure I understand correctly, i'll try and recap what you've presented here.
As things stand now, when a CRD does not have the validations
property, i.e it doesn't have a well defined schema, the result of importing such a CRD with cdk8s import
would be this:
/**
*
*
* @schema Widget
*/
export class Widget extends ApiObject {
/**
* Defines a "Widget" API object
* @param scope the scope in which to define this object
* @param name a scope-local name for the object
* @param options configuration options
*/
public constructor(scope: Construct, name: string, options: WidgetOptions = {}) {
super(scope, name, {
...options,
kind: 'Widget',
apiVersion: 'com.foo.bar/v1',
});
}
}
/**
* @schema Widget
*/
export interface WidgetOptions {
}
Using this Widget
resource would basically allow passing any arbitrary keys, which will fail at apply
time. For example:
new Widget(this, 'Widget, {
not_a_spec: 'foo',
not_a_metadata: 'bar'
})
What you are suggesting, is that for such CRD's, we will generate a default schema, like so:
/**
* @schema Widget
*/
export interface WidgetOptions {
readonly metadata: k8s.ObjectMeta;
readonly spec: { [k: string]: any };
}
What about resources/crds that don't accept a spec
? For example, ConfigMap
:
export interface ConfigMapOptions {
readonly binaryData?: { [key: string]: string };
readonly data?: { [key: string]: string };
readonly metadata?: ObjectMeta;
}
In addition, it looks like you are also suggesting to apply this default schema for CRD's with validation, but without a spec
and metadata
key.
So for example, a validation of this sort:
validation:
openAPIV3Schema:
properties:
foo:
type: string
baz:
type: integer
Would result in the following schema:
/**
* @schema Widget
*/
export interface WidgetSpec {
readonly foo: string;
readonly baz: int;
}
/**
* @schema Widget
*/
export interface WidgetOptions {
readonly metadata: k8s.ObjectMeta;
readonly spec: WidgetSpec;
}
However, the validation doesn't mention spec
anywhere, and it might be that the resource doesn't accept a spec
, and the intent was to validate the top level foo
and baz
properties. Does it make sense to force a spec
?
I hope I understood you correctly.
Waiting for more discussion :)
Thanks!
Hi @iliapolo, thanks for digging in on this! I think your understanding of my initial proposal is accurate, but I definitely see the holes that you're pointing out, so I'm not sure my proposed solution is the right one anymore.
I think the one thing that is missing is how the options
argument to the constructor of the current generated class interacts with the python
wrapper. Currently (0.27.0
) when a python
wrapper class is generated for a CRD without validation, there is no way (that I can find) to pass the equivalent of the options
key/value mapping through to the underlying node code. For example, this etcd CRD has no validation and the resulting generated python
wrapper is:
import abc
import builtins
import datetime
import enum
import typing
import jsii
import publication
import typing_extensions
from ._jsii import *
import cdk8s
import constructs
class EtcdCluster(
cdk8s.ApiObject,
metaclass=jsii.JSIIMeta,
jsii_type="etcddatabasecoreoscometcdcluster.EtcdCluster",
):
"""
schema:
:schema:: EtcdCluster
"""
def __init__(self, scope: constructs.Construct, name: builtins.str) -> None:
"""Defines a "EtcdCluster" API object.
:param scope: the scope in which to define this object.
:param name: a scope-local name for the object.
"""
options = EtcdClusterOptions()
jsii.create(EtcdCluster, self, [scope, name, options])
@jsii.data_type(
jsii_type="etcddatabasecoreoscometcdcluster.EtcdClusterOptions",
jsii_struct_bases=[],
name_mapping={},
)
class EtcdClusterOptions:
def __init__(self) -> None:
"""
schema:
:schema:: EtcdCluster
"""
self._values: typing.Dict[str, typing.Any] = {}
def __eq__(self, rhs: typing.Any) -> builtins.bool:
return isinstance(rhs, self.__class__) and rhs._values == self._values
def __ne__(self, rhs: typing.Any) -> builtins.bool:
return not (rhs == self)
def __repr__(self) -> str:
return "EtcdClusterOptions(%s)" % ", ".join(
k + "=" + repr(v) for k, v in self._values.items()
)
__all__ = [
"EtcdCluster",
"EtcdClusterOptions",
]
publication.publish()
The corresponding generated node
code is:
class EtcdCluster extends cdk8s_1.ApiObject {
/**
* Defines a "EtcdCluster" API object
* @param scope the scope in which to define this object
* @param name a scope-local name for the object
* @param options configuration options
*/
constructor(scope, name, options = {}) {
super(scope, name, {
...options,
kind: 'EtcdCluster',
apiVersion: 'etcd.database.coreos.com/v1beta3',
});
}
}
exports.EtcdCluster = EtcdCluster;
From what I can tell, there's no way to to populate the options
in python and have them pass through jsii
the node
class, leaving the python
version of the CRD pretty much useless. I think that fixing this ability to do arbitrary key/value passthroughts would likely solve the working problem.
It's still a bit awkward to not be able to use k8s.ObjectMeta
, but that would be fairly awkward to solve since that comes from the generated k8s
package and is not defined in the core of cdk8s
anywhere. A nicer solution might be to allow ApiObjectMetadata
through for the metadata
argument of every CRD.
@gabe-l-hart I see what you mean about the python output.
So actually this just sounds like a bug.
I'm going to use your example here and file a bug in favor of this RFC. Sound good?
@iliapolo Yes, that sounds good. Thanks for the discussion!
Closing in favor of https://github.com/awslabs/cdk8s/issues/316
Instructions:
Summary
This RFC proposes adding more sophisticated default behavior to
import
when determining theschema
for a CRD (here). The proposed changes are:Create a default
schema
similar to the following:NOTE: Not sure the specifics of the type specification (my
typescript
is a bit rusty), but this would be the equivalent of specifyingadditionalProperties: true
in the swagger spec.Override the default schema based on the content of the CRD's
validation
schemaMotivation
When using
cdk8s
with CRDs that you do not have direct control over, the current implementation for determining theschema
has two major holes:validation
section and rely on convention in CRs to infer a schemavalidation
section only specify a schema for thespec
portion of the CR and omit the standardkubernetes
boilerplate (specificallymetadata
).metadata
since that is "owned" by kubernetes and not me, the developer of the resource type.Basic Example
CRD without
validation
CRD with
validation
but nometadata
schemaDesign Summary
The design here is attempting to follow the "what would it do if I didn't read the documentation" principle. Without reading carefully, I would expect an imported CRD's
construct
to be flexible to all of the various forms that a CRD can take inkubernetes
. I would also expectcdk8s
to explicitly plan for the circumstance where a user is building anApp
that uses CRDs which that user does not have write-access to (i.e. third party CRDs).Detailed Design
The details are pretty simple actually:
schema
object incrd.ts
spec.validation.openAPIV3Schema
where schema elements in thevalidation
section take precedence.construct
forspec
when not specified such that at runtime, thespec
argument could be an arbitrary key/value store of plain-old-data.Drawbacks
The major drawback to this proposal is that it weakens the
cdk8s
stance on well-formed data structures (which is generally a great stance to take!). With this in place, it allows users to write sloppy CRDs and rely on the default behavior to just put arbitrary keys into theconstructs
which could lead to a lack of maintainability in the user's code.Rationale and Alternatives
The rationale for this proposal is to allow well-intentioned users to take advantage of good, but sloppy third-party work (i.e. CRDs without
validation
that do the job well but have no strict schema). There are several alternatives to this that could be considered:Solutions in cdk8s:
schema
that will be applied to allimport
constructsmetadata
repeating the same default template, and it would not allow a user to usek8s.ObjectMetadata
there.construct
(including itspython
bindings) to be modified after construction with arbitrary properties that would propagate through to the final renderedyaml
fileSolutions in user project:
validation
manuallyoperator
because the vendored CRD and the deployedoperator
could get out of synccdk8s import
process to adjust CRDs on-the-fly before the codegen looks at themyaml
files after rendering, but before applying them to the clusterAdoption Strategy
This would not be a breaking change, so no adoption strategy would be needed.
Unresolved questions
ObjectMetadata
construct from thek8s
package in conjunction with a construct generated from a CRD in a different package?Future Possibilities
schemas
would be a nice future extension to this idea