trident icon indicating copy to clipboard operation
trident copied to clipboard

Provide actual schema for Kubernetes CustomResourceDefinition of TridentBackendConfig

Open ergonben opened this issue 11 months ago • 4 comments

Thanks for working on NetApp Trident.

Currently, the CustomResourceDefinition for kind TridentBackendConfig has no actual schema:

schema:
  openAPIV3Schema:
    type: object # That is, anything goes.
    x-kubernetes-preserve-unknown-fields: true

As I understand, this is to accommodate for the variety of backend storage platforms.

However, it should be possible to express this variety in OpenAPI, using constructs such as anyOf, array, enum, etc.

The benefits would be a precise documentation of the config (addressing #861) and automatic validation, promoting the Kubernetes integration.

Describe the solution you'd like

A CustomResourceDefinition with a schema along these lines (obviously not ready but you get the idea):

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: tridentbackendconfigs.trident.netapp.io
spec:
  versions:
    - name: v1
      schema:
        openAPIV3Schema:
          type: object
          properties:
            apiVersion:
              type: string
            kind:
              type: string
            metadata:
              type: object
            spec:
              type: object
              properties:
                version:
                  type: integer
                storageDriverName:
                  type: string
                backendName:
                  type: string
                managementLIF:
                  type: string
                dataLIF:
                  type: string
                svm:
                  type: string
                autoExportPolicy:
                  type: boolean
                autoExportCIDRs:
                  type: array
                  items:
                    type: string
                credentials:
                  type: object
                  properties:
                    name:
                      type: string

Describe alternatives you've considered

As an alternative, the schema could be partially refined. Meaning some fields may still be intentionally left flexible using type: object if an OpenAPI specification is not possible or too complex.

Additional context

On a tangent, having a schema would support the use of the official Terraform Kubernetes provider. Without a schema, the provider tries to replace TridentBackendConfigs on every Terraform apply operation, even if nothing has changed. Such a replacement does does not terminate (unless there are no PersistentVolumes on the TridentBackend), forcing the user to abort the apply operation, which leaves the TridentBackendConfig in state "Deleting" – clearly undesired. For more details about this symptom, see https://github.com/hashicorp/terraform-provider-kubernetes/issues/1382.

More about CustomResourceDefinition.

ergonben avatar Jan 20 '25 16:01 ergonben

Hi,

I suggest the same approach for other CRDs such as TridentMirrorRelationship and TridentActionMirrorUpdate

rdemarinis avatar Apr 10 '25 07:04 rdemarinis

Hi, @ergonben. You are correct that the schema-less nature of TridentBackendConfig is to accommodate several backend types whose definitions are all very different. They also change over time, making any schema a moving target. Generating a schema to encompass everything seems prohibitively complicated, unless perhaps there is some automation capable of generating that. Are you aware of tooling with that level of sophistication? And then we have to worry about CRD versioning during upgrades & downgrades, even if we stick with v1 on the CRD. One approach might be to modify the CRDs via the tridentctl install --generate-custom-yaml mechanism to add an autogenerated schema.

clintonk avatar Apr 16 '25 15:04 clintonk

@clintonk: Thanks your explanations.

I'm not aware of such tooling but then I also haven't worked much with CRDs.

By the way, if it helps anyone, this is the Terraform resource we use for our limited use case:

# Trident's CustomResourceDefinition (CRD) for TridentBackendConfigs lacks a
# schema. Without a schema, the Kubernetes provider tries to replace
# TridentBackendConfigs on every Terraform apply operation, even if nothing has
# changed. Such a replacement does does not terminate (unless there are no
# PersistentVolumes on the TridentBackend), forcing the user to abort the apply
# operation, which leaves the TridentBackendConfig in state "Deleting", which is
# clearly undesired.
#
# Thus, we overwrite the official Trident CRD with our own, until Trident
# provides a schema (https://github.com/NetApp/trident/issues/964).
#
# Other than the schema, the following CRD is copied from the Trident source
# (https://github.com/NetApp/trident/blob/v24.10.0/cli/k8s_client/yaml_factory.go#L2066, update-worthy).
resource "kubernetes_manifest" "crd_trident_backend_config" {
  manifest = {
    apiVersion = "apiextensions.k8s.io/v1"
    kind       = "CustomResourceDefinition"
    metadata = {
      name = "tridentbackendconfigs.trident.netapp.io"
    }
    spec = {
      group = "trident.netapp.io"
      versions = [
        {
          name    = "v1"
          served  = true
          storage = true
          schema = {
            openAPIV3Schema = {
              type = "object"
              properties = {
                apiVersion = { type = "string" }
                kind       = { type = "string" }
                metadata   = { type = "object" }
                spec = {
                  type = "object"
                  properties = {
                    # For Trident's ONTAP NAS config options, see
                    # https://docs.netapp.com/us-en/trident/trident-use/ontap-nas-examples.html.
                    # We only maintain the subset of options for our use case.
                    version           = { type = "integer" }
                    storageDriverName = { type = "string" }
                    backendName       = { type = "string" }
                    managementLIF     = { type = "string" }
                    dataLIF           = { type = "string" }
                    svm               = { type = "string" }
                    autoExportPolicy  = { type = "boolean" }
                    autoExportCIDRs = {
                      type  = "array"
                      items = { type = "string" }
                    }
                    credentials = {
                      type       = "object"
                      properties = { name = { type = "string" } }
                    }
                  }
                }
              }
            }
          }
          subresources = {
            status = {}
          }
          additionalPrinterColumns = [
            # `priority = 0` dropped as otherwise Terraform does not converge.
            {
              name        = "Backend Name"
              type        = "string"
              description = "The backend name"
              jsonPath    = ".status.backendInfo.backendName"
            },
            {
              name        = "Backend UUID"
              type        = "string"
              description = "The backend UUID"
              jsonPath    = ".status.backendInfo.backendUUID"
            },
            {
              name        = "Phase"
              type        = "string"
              description = "The backend config phase"
              jsonPath    = ".status.phase"
            },
            {
              name        = "Status"
              type        = "string"
              description = "The result of the last operation"
              jsonPath    = ".status.lastOperationStatus"
            },
            {
              name        = "Storage Driver"
              type        = "string"
              description = "The storage driver type"
              priority    = 1
              jsonPath    = ".spec.storageDriverName"
            },
            {
              name        = "Deletion Policy"
              type        = "string"
              description = "The deletion policy"
              priority    = 1
              jsonPath    = ".status.deletionPolicy"
            },
          ]
        },
      ]
      scope = "Namespaced"
      names = {
        plural   = "tridentbackendconfigs"
        singular = "tridentbackendconfig"
        kind     = "TridentBackendConfig"
        shortNames = [
          "tbc",
          "tbconfig",
          "tbackendconfig",
        ]
        categories = [
          "trident",
          "trident-internal",
          "trident-external",
        ]
      }
    }
  }
}

ergonben avatar Apr 17 '25 07:04 ergonben

+1 to fix this, my k8s team was appalled by this, My lead K8s engineer sent me the following

"Having a fully built-out OpenAPI v3 schema for your Custom Resource Definition (CRD) in Kubernetes, rather than a permissive spec: {}, offers significant advantages for validation and overall API robustness. Primarily, it enables the Kubernetes API server to perform server-side validation of your custom resources. This means any object submitted to the cluster that doesn't conform to the defined schema (e.g., missing required fields, incorrect data types, invalid values) will be rejected immediately, preventing the creation of malformed or non-functional resources. This robust validation improves the reliability and stability of your custom controllers, as they can assume incoming objects adhere to a well-defined contract. Furthermore, a detailed schema provides a much better developer experience, offering clear error messages, enabling IDE auto-completion, and facilitating the generation of client libraries and documentation, ultimately making your custom API easier to use and maintain."

killerclaffey avatar Dec 10 '25 21:12 killerclaffey