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

CRD supports multi-version Schema, Subresources and AdditionalPrintColumns #70211

Merged
merged 5 commits into from
Nov 7, 2018

Conversation

roycaihw
Copy link
Member

@roycaihw roycaihw commented Oct 25, 2018

What type of PR is this?
/kind feature

What this PR does / why we need it:
Part of the CRD conversion webhook design: customresource-conversion-webhook.md#top-level-fields-to-per-version-fields
Main CRD conversion webhook PR: #67006
This PR is a combination of two previous separate PRs (#67553, #67521) following the updated KEP design: kubernetes/community#2737

Special notes for your reviewer:

  1. Please review the integration test change (in a standalone commit) with whitespace change ignored.
  2. This PR currently does redundant pre-creation/update-dropping-field and failing-validation for disabled feature-gated fields, please refer to the related topic in sig-api-machinery meeting (Oct 24) for more information.
  3. Two subresources integration tests (with TODO comments) will be extended to test per-version subresources after Fix custom resource handler in-memory version #70087 fixes the unstructured conversion bug. Updated

Does this PR introduce a user-facing change?:

CRD supports multi-version Schema, Subresources and AdditionalPrintColumns (NOTE that CRDs created prior to 1.13 populated the top-level additionalPrinterColumns field by default. To apply an update that changes to per-version additionalPrinterColumns, the top-level additionalPrinterColumns field must be explicitly set to null).

/sig api-machinery
/area custom-resources
/cc @sttts @mbohlool @liggitt

@k8s-ci-robot k8s-ci-robot added release-note Denotes a PR that will be considered when it comes time to generate release notes. kind/feature Categorizes issue or PR as related to a new feature. size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files. sig/api-machinery Categorizes an issue or PR as relevant to SIG API Machinery. area/custom-resources cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. labels Oct 25, 2018
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// CustomResourceColumns masks the slice so protobuf can preserve empty slice
// +protobuf.nullable=true
Copy link
Member Author

Choose a reason for hiding this comment

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

@liggitt Do we need to test the round-tripping?

Copy link
Member

Choose a reason for hiding this comment

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

same as edd5736#r228339108, we can drop this and just use []CustomResourceColumnDefinition

@@ -20,6 +20,9 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// CustomResourceColumns masks the slice so protobuf can preserve empty slice
type CustomResourceColumns []CustomResourceColumnDefinition
Copy link
Member

@liggitt liggitt Oct 25, 2018

Choose a reason for hiding this comment

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

remind me why we care about distinguishing nil and [] here if we didn't care for the top-level field?

Copy link
Member Author

Choose a reason for hiding this comment

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

The concern was brought up in #67521 (comment). My understanding is that we need to make protobuf preserve [], to allow overriding with an empty list. I didn't change the top-level field because it would be a breaking change.

Copy link
Member

Choose a reason for hiding this comment

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

rereading that thread, it looks like the feedback there was to avoid making the distinction between nil and [] (to not do this nullable typedef). I think the len(...) == 0 checks for emptiness are sufficient. Were there specific places where we needed to be able to tell nil from [] for some reason?

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks for the explanation :) No the apiserver doesn't need to distinguish nil and [] (and defaulting happens for both cases).

Copy link
Member

Choose a reason for hiding this comment

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

great, then let's drop this and just go with []CustomResourceColumnDefinition

}
value := versions[0].AdditionalPrinterColumns
for _, v := range versions {
if v.AdditionalPrinterColumns == nil || !reflect.DeepEqual(v.AdditionalPrinterColumns, value) {
Copy link
Member

@liggitt liggitt Oct 25, 2018

Choose a reason for hiding this comment

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

should we check len(...)==0 instead of nil? should we use semantic equality rather than reflect.DeepEqual? (same question applies everywhere)

Copy link
Member Author

Choose a reason for hiding this comment

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

By semantic equality you mean for two column slices, we should consider them to be the same if they have same elements but in different order?

Copy link
Member

Choose a reason for hiding this comment

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

I mean to use apiequality.Semantic.DeepEqual(). It equates nil and empty lists, and does a few other semantic comparisons (like the absolute value of resource.Quantity fields, rather than their literal value, so 1M == 1000)

@nikopen
Copy link
Contributor

nikopen commented Oct 25, 2018

/milestone v1.13

@k8s-ci-robot k8s-ci-robot added this to the v1.13 milestone Oct 25, 2018
@roycaihw
Copy link
Member Author

@liggitt Addressed comments and updated integration test (I had to rebase to pick up the conversion bug fix, but the first five commits didn't change). PTAL :)

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// CustomResourceColumns masks the slice so protobuf can preserve empty slice
// +protobuf.nullable=true
Copy link
Member

Choose a reason for hiding this comment

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

same as edd5736#r228339108, we can drop this and just use []CustomResourceColumnDefinition

// Per-version schemas may not all be set to identical values (top-level validation schema should be used instead)
// This field is alpha-level and is only honored by servers that enable the CustomResourceWebhookConversion feature.
// +optional
Schema *JSONSchemaProps `json:"schema,omitempty" protobuf:"bytes,4,opt,name=schema"`
Copy link
Member

Choose a reason for hiding this comment

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

I just noticed this does not parallel the top-level field. what was the reason for specifying the type of schema (nested openAPIV3Schema field) in the top-level field, and why does that reason not apply for the per-version schemas?

cc @sttts

Copy link
Member Author

Choose a reason for hiding this comment

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

My understanding is that for the top-level field, we were planning on having a list of validation methods for CRs, so we defined a struct and made openAPIV3Schema nested.

Now it becomes clear that we are not adding new methods to the CustomResourceValidation, so we simplify the per-version schema to just JSONSchemaProps.

also cc @mbohlool @lavalamp on the related conversation: #67840

Copy link
Member

Choose a reason for hiding this comment

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

Now it becomes clear that we are not adding new methods to the CustomResourceValidation, so we simplify the per-version schema to just JSONSchemaProps.

also cc @mbohlool @lavalamp on the related conversation: #67840

#67840 actually proposes supporting a schema format other than openapi v3, which is exactly the use case for nesting openapi v3 as one of potentially multiple schema types. I think we should make the per-version schema structs in v1beta1 match the top-level structs, so that we can round-trip to whatever we end up with in v1, and have a path to transition to a different schema if that's where #67840 ends up. @lavalamp @sttts do you agree?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes. In the KEP we proposed dropping the nested struct and going with just openapi v3, then in #67840 we brought up the idea around replacing openapi v3. I'm happy to go with the nested struct if we think that will make it easier to support another schema format, before we merge this PR.

Copy link
Contributor

Choose a reason for hiding this comment

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

I am fine with keeping top level struct format with nested openapi v3. While last proposal on #67840 is to keep a more restricted version of openapi v3 but keeping the options open make more sense.

Copy link
Member Author

Choose a reason for hiding this comment

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

More related context: #64483

I'm going to add a commit with

Schema *CustomResourceValidation `json:"schema,omitempty" protobuf:"bytes,4,opt,name=schema"`

first, which may look like:

spec:
  versions:
    - name: v1
      schema:
        openAPIV3Schema: ...       // covers schema, defaults and static validation
        anotherSchemaLanguage: ... // to be added in future?
      prune: true

validator, _, err := apiservervalidation.NewSchemaValidator(crd.Spec.Validation)
validationSchema, err := getCRDSchemaForVersion(&crd.Spec, v.Name)
if err != nil {
return nil, err
Copy link
Member

Choose a reason for hiding this comment

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

will this return the full content of the CRD spec to the custom resource caller in an error? that seems very verbose and probably targeted at the wrong user... the custom resource API caller isn't necessarily the same as the CRD author. maybe call utilruntime.HandleError and return a generic error message about the subresources to the API caller?

Copy link
Member Author

Choose a reason for hiding this comment

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

Good point. Yes we shouldn't return the full CRD spec to a CR client when ServeHTTP. I think the same comment also applies to https://github.com/kubernetes/kubernetes/pull/70211/files/956879420560759244d2210acff668902ef20780#diff-2f720a7e61a4ddfcfe9a9e353be8784bR225.

May I ask what kind of generic error message do you have in mind? While the error can be triggered by spec with malformed subresources/schema/columns, the incoming HTTP request isn't necessarily asking for subresources-- it's called here in ServeHTTP:

So something like the server could not properly server the CRD subresources/schema/columns (corresponding to which field might be malformed)?

Copy link
Member

Choose a reason for hiding this comment

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

yeah, something like that would be fine


subresources, err := getCRDSubresourcesForVersion(&crd.Spec, v.Name)
if err != nil {
return nil, err
Copy link
Member

Choose a reason for hiding this comment

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

same comment about not returning the full CRD schema to the caller in an error here

table, err := tableconvertor.New(crd.Spec.AdditionalPrinterColumns)
columns, err := getCRDColumnsForVersion(&crd.Spec, v.Name)
if err != nil {
return nil, err
Copy link
Member

Choose a reason for hiding this comment

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

same error comment here

continue
}
if crdSpec.Validation != nil && v.Schema != nil {
return nil, fmt.Errorf("malformed CustomResourceDefinitionSpec: top-level and per-version schemas must be mutual exclusive; spec: %v, version: %s", *crdSpec, version)
Copy link
Member

Choose a reason for hiding this comment

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

be sure to include the CRD name in the error so whoever sees it can resolve it. we probably don't need to dump the whole spec... just the CRD name, version name, and the fact that there was a top-level and per-version value specified is enough (same comment applies to the other errors as well)

@lavalamp
Copy link
Member

lavalamp commented Oct 31, 2018 via email

if v.Schema != nil {
return v.Schema, nil
}
return crd.Spec.Validation, nil
Copy link
Contributor

Choose a reason for hiding this comment

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

are we doing override here? we agreed not to do overrides. That means in getCRDSchemaForVersion we should call the other helper to see if there is any per-version Schema. If there is we should return v.Schema even if it is nil.

Copy link
Member Author

Choose a reason for hiding this comment

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

No, it's not doing override. I think the auto-clear strategy makes sure that both ways work the same. In the case that you described (v.Schema is nil while there exists other per-version schema), crd.Spec.Validation will also be nil, so getCRDSchemaForVersion returns nil in the end.

Copy link
Contributor

Choose a reason for hiding this comment

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

You are right. Still to anybody reading this code, it look like we are overriding. It would be a cleaner code if we apply the logic here too and it wouldn't hurt.

Copy link
Contributor

Choose a reason for hiding this comment

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

or you can check if top level field is not nil and return it at the start of the method with a comment explaining why if it is not nil, it means all per versions will be. I still prefer the first approach though :)

if v.Subresources != nil {
return v.Subresources, nil
}
return crd.Spec.Subresources, nil
Copy link
Contributor

Choose a reason for hiding this comment

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

ditto

if len(v.AdditionalPrinterColumns) > 0 {
return v.AdditionalPrinterColumns, nil
}
return crd.Spec.AdditionalPrinterColumns, nil
Copy link
Contributor

Choose a reason for hiding this comment

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

ditto

@@ -40,6 +40,12 @@ const (
//
// CustomResourceSubresources defines the subresources for CustomResources
CustomResourceSubresources utilfeature.Feature = "CustomResourceSubresources"

// owner: @mbohlool
Copy link
Contributor

Choose a reason for hiding this comment

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

add yourself as owner here too please.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done

@roycaihw
Copy link
Member Author

/test pull-kubernetes-e2e-kops-aws

if version != v.Name {
continue
}
if hasPerVersionSchema(crd.Spec.Versions) {
Copy link
Contributor

Choose a reason for hiding this comment

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

you can do this check earlier before for loop and return crd.Spec.Validation as early as possible then you will have less nested blocks.

Copy link
Member

Choose a reason for hiding this comment

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

agree that unnesting like this would improve readability. up to you if you want to unnest like this:

if !hasPerVersionSchema(crd.Spec.Versions) {
  return crd.Spec.Validation, nil
}
if crd.Spec.Validation != nil {
  return nil, fmt.Errorf("malformed CustomResourceDefinition %s version %s: top-level and per-version schemas must be mutual exclusive", crd.Name, version)
}
for _, v := range crd.Spec.Versions {
  if version == v.Name {
    return v.Schema, nil
  }
}
return nil, fmt.Errorf("version %s not found in CustomResourceDefinition: %v", version, crd.Name)

@roycaihw
Copy link
Member Author

roycaihw commented Nov 6, 2018

@liggitt Thanks for the suggestions. I amended latest change to the last commit. Please take a look

@@ -149,6 +158,24 @@ type CustomResourceDefinitionVersion struct {
// Storage flags the version as storage version. There must be exactly one flagged
// as storage version.
Storage bool
// Schema describes the schema for CustomResource used in validation, pruning, and defaulting.
// Top-level and per-version schemas are mutually exclusive.
// Per-version schemas may not all be set to identical values (top-level validation schema should be used instead)
Copy link
Contributor

Choose a reason for hiding this comment

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

What does "may not" mean here?

Copy link
Member Author

Choose a reason for hiding this comment

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

Updated to "must not"

// This field is alpha-level and is only honored by servers that enable the CustomResourceWebhookConversion feature.
// +optional
Schema *CustomResourceValidation
// Subresources describes the subresources for CustomResources
Copy link
Contributor

Choose a reason for hiding this comment

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

CustomResource

Copy link
Member Author

Choose a reason for hiding this comment

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

Updated.

This was copied from the top-level Subresources comment. I updated the top-level one as well. We have similar comments for other fields (e.g. Validation describes the validation methods for CustomResources). It might worth another PR to figure out the scope and do the clean up.

@roycaihw
Copy link
Member Author

roycaihw commented Nov 6, 2018

/test pull-kubernetes-integration

@roycaihw
Copy link
Member Author

roycaihw commented Nov 6, 2018

@sttts Comments addressed. Please take a look :)

@liggitt I interpreted "squash in the comment commit" in #70211 (review) as squashing new changes in an "address comments:" commit, so that we don't have to review the functionality part from the beginning. Please let me know if you want me to squash everything together.

@liggitt
Copy link
Member

liggitt commented Nov 7, 2018

lgtm

once @sttts acks, can squash the review commits in and we can tag for merge

@sttts
Copy link
Contributor

sttts commented Nov 7, 2018

Lgtm. Leaving #70211 (comment) to @liggitt.

/approve

@roycaihw
Copy link
Member Author

roycaihw commented Nov 7, 2018

Commits squashed. Please take a look

@liggitt
Copy link
Member

liggitt commented Nov 7, 2018

/lgtm
/approve

@k8s-ci-robot k8s-ci-robot added the lgtm "Looks good to me", indicates that a PR is ready to be merged. label Nov 7, 2018
@k8s-ci-robot
Copy link
Contributor

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: liggitt, roycaihw, sttts

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@k8s-ci-robot k8s-ci-robot added the approved Indicates a PR has been approved by an approver from all required OWNERS files. label Nov 7, 2018
@k8s-ci-robot k8s-ci-robot merged commit 7155b8a into kubernetes:master Nov 7, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
approved Indicates a PR has been approved by an approver from all required OWNERS files. area/custom-resources cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. kind/api-change Categorizes issue or PR as related to adding, removing, or otherwise changing an API kind/feature Categorizes issue or PR as related to a new feature. lgtm "Looks good to me", indicates that a PR is ready to be merged. release-note Denotes a PR that will be considered when it comes time to generate release notes. sig/api-machinery Categorizes an issue or PR as relevant to SIG API Machinery. size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants