apple / pkl-pantry

Shared Pkl packages
Apache License 2.0
229 stars 31 forks source link

[K8s Contrib CRD] CRD conversion fails if enum is used #24

Closed Pythoner6 closed 4 months ago

Pythoner6 commented 4 months ago

If a CRD uses an enum, k8s.contrib.crd will fail, and with a rather unhelpful error message to the user:

–– Pkl Error ––
No member of union type matched value 'new Mapping { ["apiVersion"] = "apiextensions.k8s.io/v1"; ["kind"] = "CustomResourceDefinition"; ["metadata"] { ["annotations"] { ["controller-gen.kubebuilder.io/version"] = "v0.11.3"; ["helm.sh/resource-policy"] = "keep" }; ["name"] = "cephbucketnotifications.ceph.rook.io" }; ["spec"] { ["group"] = "ceph.rook.io"; ["names"] { ["kind"] = "CephBucketNotification"; ["listKind"] = "CephBucketNotificationList"; ["plural"] = "cephbucketnotifications"; ["singular"] = "cephbucketnotification" }; ["scope"] = "Namespaced"; ["versions"] { new Mapping { ["name"] = "v1"; ["schema"] { ["openAPIV3Schema"] { ["description"] = "CephBucketNotification represents a Bucket Notifications"; ["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"] = "BucketNotificationSpec represent the spec of a Bucket Notification"; ["properties"] { ["events"] { ["description"] = "List of events that should trigger the notification"; ["items"] { ["description"] = "BucketNotificationSpec represent the event type of the bucket notification"; ["enum"] { "s3:ObjectCreated:*"; "s3:ObjectCreated:Put"; "s3:ObjectCreated:Post"; "s3:ObjectCreated:Copy"; "s3:ObjectCreated:CompleteMultipartUpload"; "s3:ObjectRemoved:*"; "s3:ObjectRemoved:Delete"; "s3:ObjectRemoved:DeleteMarkerCreated" }; ["type"] = "string" }; ["type"] = "array" }; ["filter"] { ["description"] = "Spec of notification filter"; ["properties"] { ["keyFilters"] { ["description"] = "Filters based on the object's key"; ["items"] { ["description"] = "NotificationKeyFilterRule represent a single key rule in the Notification Filter spec"; ["properties"] { ["name"] { ["description"] = "Name of the filter - prefix/suffix/regex"; ["enum"] { "prefix"; "suffix"; "regex" }; ["type"] = "string" }; ["value"] { ["description"] = "Value to filter on"; ["type"] = "string" } }; ["required"] { "name"; "value" }; ["type"] = "object" }; ["type"] = "array" }; ["metadataFilters"] { ["description"] = "Filters based on the object's metadata"; ["items"] { ["description"] = "NotificationFilterRule represent a single rule in the Notification Filter spec"; ["properties"] { ["name"] { ["description"] = "Name of the metadata or tag"; ["minLength"] = 1; ["type"] = "string" }; ["value"] { ["description"] = "Value to filter on"; ["type"] = "string" } }; ["required"] { "name"; "value" }; ["type"] = "object" }; ["type"] = "array" }; ["tagFilters"] { ["description"] = "Filters based on the object's tags"; ["items"] { ["description"] = "NotificationFilterRule represent a single rule in the Notification Filter spec"; ["properties"] { ["name"] { ["description"] = "Name of the metadata or tag"; ["minLength"] = 1; ["type"] = "string" }; ["value"] { ["description"] = "Value to filter on"; ["type"] = "string" } }; ["required"] { "name"; "value" }; ["type"] = "object" }; ["type"] = "array" } }; ["type"] = "object" }; ["topic"] { ["description"] = "The name of the topic associated with this notification"; ["minLength"] = 1; ["type"] = "string" } }; ["required"] { "topic" }; ["type"] = "object" }; ["status"] { ["description"] = "Status represents the status of an object"; ["properties"] { ["conditions"] { ["items"] { ["description"] = "Condition represents a status condition on any Rook-Ceph Custom Resource."; ["properties"] { ["lastHeartbeatTime"] { ["format"] = "date-time"; ["type"] = "string" }; ["lastTransitionTime"] { ["format"] = "date-time"; ["type"] = "string" }; ["message"] { ["type"] = "string" }; ["reason"] { ["description"] = "ConditionReason is a reason for a condition"; ["type"] = "string" }; ["status"] { ["type"] = "string" }; ["type"] { ["description"] = "ConditionType represent a resource's status"; ["type"] = "string" } }; ["type"] = "object" }; ["type"] = "array" }; ["observedGeneration"] { ["description"] = "ObservedGeneration is the latest generation observed by the controller."; ["format"] = "int64"; ["type"] = "integer" }; ["phase"] { ["type"] = "string" } }; ["type"] = "object"; ["x-kubernetes-preserve-unknown-fields"] = true } }; ["required"] { "metadata"; "spec" }; ["type"] = "object" } }; ["served"] = true; ["storage"] = true; ["subresources"] { ["status"] {} } } } } }'

36 | if (result is ConversionFailure) throw(result.message) else result
                                      ^^^^^^^^^^^^^^^^^^^^^
at pkl.experimental.deepToTyped.deepToTyped#apply.<function#1> (https://github.com/apple/pkl-pantry/blob/pkl.experimental.deepToTyped@1.0.0/packages/pkl.experimental.deepToTyped/deepToTyped.pkl#L36-36)

30 | let (result =
     ^^^^^^^^^^^^^
at pkl.experimental.deepToTyped.deepToTyped#apply (https://github.com/apple/pkl-pantry/blob/pkl.experimental.deepToTyped@1.0.0/packages/pkl.experimental.deepToTyped/deepToTyped.pkl#L30-36)

105 | deepToTyped.apply(ModuleGenerator.CRD, crd) as ModuleGenerator.CRD
      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
at k8s.contrib.crd.generate#crds.<function#1>[#1] (https://github.com/apple/pkl-pantry/blob/k8s.contrib.crd@1.0.0/packages/k8s.contrib.crd/generate.pkl#L105-105)

101 | let (parser = new yaml.Parser { useMapping = true })
      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
at k8s.contrib.crd.generate#crds (https://github.com/apple/pkl-pantry/blob/k8s.contrib.crd@1.0.0/packages/k8s.contrib.crd/generate.pkl#L101-108)

128 | for (_crd in crds) {
                   ^^^^
at k8s.contrib.crd.generate#modules (https://github.com/apple/pkl-pantry/blob/k8s.contrib.crd@1.0.0/packages/k8s.contrib.crd/generate.pkl#L128-128)

142 | for (mod in modules) {
                  ^^^^^^^
at k8s.contrib.crd.generate#output.files (https://github.com/apple/pkl-pantry/blob/k8s.contrib.crd@1.0.0/packages/k8s.contrib.crd/generate.pkl#L142-142)

Manually parsing and then calling deep to typed on the offending CRD (but without the type union used by k8s.contrib.crd) lead to a slightly better error message:

–– Pkl Error ––
Unsupported type for conversion: Any

36 | if (result is ConversionFailure) throw(result.message) else result
                                      ^^^^^^^^^^^^^^^^^^^^^
at pkl.experimental.deepToTyped.deepToTyped#apply.<function#1> (projectpackage://pkg.pkl-lang.org/pkl-pantry/pkl.experimental.deepToTyped@1.0.0#/deepToTyped.pkl)

30 | let (result =
     ^^^^^^^^^^^^^
at pkl.experimental.deepToTyped.deepToTyped#apply (projectpackage://pkg.pkl-lang.org/pkl-pantry/pkl.experimental.deepToTyped@1.0.0#/deepToTyped.pkl)

11 | crd: CustomResourceDefinition = deepToTyped.apply(CRD, parsed)
                                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
at test#crd (file:///home/joseph/dev/nix-pkl/test.pkl, line 11)

106 | text = renderer.renderDocument(value)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
at pkl.base#Module.output.text (https://github.com/apple/pkl/blob/0.25.2/stdlib/base.pkl#L106)

Which eventually lead me to the offending property, which is enum as defined here https://github.com/apple/pkl-k8s/blob/main/generated-package/apiextensions-apiserver/pkg/apis/apiextensions/v1/CustomResourceDefinition.pkl#L272 (there's also two other properties in there that use the type Any: default and example)

An example minimal CRD which reproduces the issue:

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
spec:
  group: foo.example.com
  names:
    kind: Foo
    plural: foos
  scope: Namespaced
  versions:
    - name: v1
      schema:
        openAPIV3Schema:
          properties:
            doesntwork:
              enum:
                - foo
              type: string
          type: object
      served: true
      storage: true
mruoss commented 4 months ago

I stumbled upon the same issue. Also, the same exception is raised if default is used (same CRD as above but enum replaced with default:

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
spec:
  group: foo.example.com
  names:
    kind: Foo
    plural: foos
  scope: Namespaced
  versions:
    - name: v1
      schema:
        openAPIV3Schema:
          properties:
            doesntwork:
              default: foo
              type: string
          type: object
      served: true
      storage: true
jackkleeman commented 4 months ago

I think I stumbled into this as well; thank you for tracking it down and your fix makes sense to me

mruoss commented 4 months ago

Is this fixed with #29?

Pythoner6 commented 4 months ago

Yep, looks like this is now working with k8s.contrib.crd 1.0.1 :)