Skip to main content

Custom Entities

Custom Entities in KubeOps represent Custom Resource Definitions (CRDs) in Kubernetes. They allow you to extend the Kubernetes API with your own resource types.

Creating a Custom Entity

To create a custom entity, create a class that inherits from one of the base entity classes and decorate it with the [KubernetesEntity] attribute:

[KubernetesEntity(Group = "demo.kubeops.dev", ApiVersion = "v1", Kind = "DemoEntity")]
public class V1DemoEntity : CustomKubernetesEntity<V1DemoEntity.V1DemoEntitySpec, V1DemoEntity.V1DemoEntityStatus>
{
public class V1DemoEntitySpec
{
public string Username { get; set; } = string.Empty;
}

public class V1DemoEntityStatus
{
public string DemoStatus { get; set; } = string.Empty;
}
}

Entity Types

KubeOps provides three base classes for creating custom entities:

  1. CustomKubernetesEntity: Base class with only metadata
  2. CustomKubernetesEntity<TSpec>: Entity with specification
  3. CustomKubernetesEntity<TSpec, TStatus>: Entity with specification and status

Entity Scope

Entities can be either namespaced or cluster-wide. Use the [EntityScope] attribute to specify the scope:

[KubernetesEntity(Group = "demo.kubeops.dev", ApiVersion = "v1", Kind = "DemoEntity")]
[EntityScope(EntityScope.Namespaced)] // or EntityScope.Cluster
public class V1DemoEntity : CustomKubernetesEntity<V1DemoEntity.V1DemoEntitySpec>
{
public class V1DemoEntitySpec
{
public string Username { get; set; } = string.Empty;
}
}

Spec and Status

Spec

The Spec property contains the desired state of your resource. It's defined as a nested class within your entity:

public class V1DemoEntitySpec
{
[Required]
public string Username { get; set; } = string.Empty;

[Description("The number of replicas to run")]
public int Replicas { get; set; } = 1;
}

Because Username is a direct [Required] property of EntitySpec, the Transpiler automatically adds spec to the top-level required array in the generated CRD. This means Kubernetes will reject any resource where spec: is omitted or null — not just resources where username is missing inside spec.

Status

The Status property (optional) contains the current state of your resource:

public class V1DemoEntityStatus
{
public string CurrentState { get; set; } = string.Empty;
public DateTime LastUpdated { get; set; }
}

The CRD transpiler maps common CLR scalar types to OpenAPI schema types. For example, Guid is emitted as type: string with format: uuid, and DateTime / DateTimeOffset are emitted as type: string with format: date-time.

Entity Attributes

KubeOps provides various attributes to customize and validate your entities:

Entity Definition Attributes

  • [KubernetesEntity]: Defines the basic entity information (Group, Version, Kind)
  • [EntityScope]: Specifies if the entity is namespaced or cluster-wide
  • [StorageVersion]: Marks an entity as the storage version for version conversion
  • [KubernetesEntityShortNames]: Defines short names for the CRD (e.g., "deploy" for "deployment")

Validation Attributes

  • [Required]: Marks a property as required. When applied to a property inside EntitySpec, that field becomes required within the spec schema. Additionally:
    • Auto-inference: if any direct property of EntitySpec is marked [Required], the Transpiler automatically marks spec itself as required at the top-level CRD schema. A [Required] property that is nested under an optional parent does not trigger this — Kubernetes only validates required constraints when the parent object is present, so each level of the hierarchy must be annotated explicitly.
    • Explicit class-level: apply [Required] directly to the EntitySpec class to mark spec as required at the top level even when no sub-property carries [Required].
// Auto-inference: spec is required because Username is a direct required property
public class EntitySpec
{
[Required]
public string Username { get; set; } = null!;
}

// Explicit class-level: spec is required regardless of sub-properties
[Required]
public class EntitySpec
{
public string Username { get; set; } = string.Empty;
}

// Does NOT make spec required — [Required] is on the 2nd level under an optional parent:
public class EntitySpec
{
public NestedSpec Nested { get; set; } = new(); // optional

public class NestedSpec
{
[Required] // only validated when Nested is present; spec stays optional
public string Name { get; set; } = null!;
}
}
  • [Pattern]: Defines a regex pattern for string validation
  • [Length]: Specifies minimum and maximum length for strings or arrays
  • [RangeMinimum] and [RangeMaximum]: Defines numeric value ranges
  • [MultipleOf]: Specifies that a number must be a multiple of a given value
  • [Items]: Defines minimum and maximum items for arrays
  • [ValidationRule]: Defines custom validation rules using CEL expressions. Can be applied to properties and to class types — in both cases it emits x-kubernetes-validations on the corresponding schema node. When applied to both a class and a property of that type, the rules are merged (class rules first, then property rules). Class-level rules are also collected across the class inheritance chain: a rule placed on a base class is inherited by every derived entity or nested type, and all rules found along the chain are merged onto the schema node

Documentation Attributes

  • [Description]: Adds a description to a property or entity
  • [ExternalDocs]: Links to external documentation

Display Attributes

  • [AdditionalPrinterColumn]: Adds a column to kubectl get output
  • [GenericAdditionalPrinterColumn]: Adds a custom column to kubectl get output

Special Attributes

  • [Ignore]: Excludes a property or entity from CRD generation
  • [PreserveUnknownFields]: Allows unknown fields on the annotated object (x-kubernetes-preserve-unknown-fields: true). The known fields are still transpiled and validated, so you keep a structural schema for what you model while permitting extra fields. Works the same whether placed on a property or on a class/type: if the type cannot be represented (it contains a circular reference or an otherwise non-transpilable member), it gracefully falls back to an opaque type: object with x-kubernetes-preserve-unknown-fields: true instead of failing — making this the recommended way to model complex, externally generated, or self-referencing types.
  • [EmbeddedResource]: Marks a property as an embedded Kubernetes resource. The property type is never traversed; the schema is always an opaque embedded type: object.
note

The CRD transpiler maps property types recursively. A circular type reference that is not opted out via [PreserveUnknownFields] or [Ignore] cannot be represented as a finite OpenAPI schema and raises a descriptive TranspilationFailedException during generation. Annotate the offending property or type with [PreserveUnknownFields] or [Ignore], or restructure the type to remove the cycle.

Example with Multiple Attributes

[KubernetesEntity(Group = "demo.kubeops.dev", ApiVersion = "v1", Kind = "DemoEntity")]
[EntityScope(EntityScope.Namespaced)]
[KubernetesEntityShortNames("demo")]
[Description("A demo entity for testing purposes")]
public class V1DemoEntity : CustomKubernetesEntity<V1DemoEntity.V1DemoEntitySpec, V1DemoEntity.V1DemoEntityStatus>
{
public class V1DemoEntitySpec
{
[Required]
[Description("The username for the demo entity")]
[Length(3, 50)]
public string Username { get; set; } = string.Empty;

[Description("Number of replicas to run")]
[RangeMinimum(1)]
[RangeMaximum(10)]
public int Replicas { get; set; } = 1;

[Pattern(@"^[a-z0-9-]+$")]
[Description("The namespace where resources should be created")]
public string? TargetNamespace { get; set; }
}

public class V1DemoEntityStatus
{
[Description("Current state of the entity")]
public string CurrentState { get; set; } = string.Empty;

[Description("Last time the entity was updated")]
public DateTime LastUpdated { get; set; }
}
}

Attribute Inheritance

CRD-shaping attributes are collected across the class inheritance chain. An attribute placed on a base class is picked up by every derived entity, so shared CRD configuration can live on a common base type.

You can also create reusable, named attributes by subclassing an existing attribute and forwarding the values to its base constructor:

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public sealed class ReadyPrinterColumnAttribute : GenericAdditionalPrinterColumnAttribute
{
public ReadyPrinterColumnAttribute()
: base(".status.conditions[?(@.type==\"Ready\")].status", "Ready", "string") { }
}

[KubernetesEntity(Group = "demo.kubeops.dev", ApiVersion = "v1", Kind = "DemoEntity")]
[ReadyPrinterColumn]
public class V1DemoEntity : CustomKubernetesEntity { }

Scale Subresource

The [ScaleSubresource] attribute enables the Kubernetes scale subresource on a CRD. This allows HorizontalPodAutoscalers (HPAs) to manage the replica count of your custom resource.

The attribute accepts two required parameters and one optional parameter — all JSON paths into the resource object:

ParameterRequiredDescription
specReplicasPathYesJSON path to desired replicas in spec, e.g. .spec.replicas
statusReplicasPathYesJSON path to observed replicas in status, e.g. .status.replicas
labelSelectorPathNoJSON path to the serialized label selector, e.g. .status.selector

Example — scale without status subresource

When the entity has no Status property only the scale subresource is emitted. The status subresource is not added automatically.

[KubernetesEntity(Group = "demo.kubeops.dev", ApiVersion = "v1", Kind = "DemoEntity")]
[ScaleSubresource(".spec.replicas", ".status.replicas")]
public class V1DemoEntity : CustomKubernetesEntity<V1DemoEntity.V1DemoEntitySpec>
{
public class V1DemoEntitySpec
{
public int Replicas { get; set; } = 1;
}
}

Generated CRD:

subresources:
scale:
specReplicasPath: .spec.replicas
statusReplicasPath: .status.replicas

Example — scale and status subresources together

When the entity inherits from CustomKubernetesEntity<TSpec, TStatus>, both status and scale subresources are emitted. labelSelectorPath is optional and maps the serialized label selector that HPAs use for targeted scaling.

[KubernetesEntity(Group = "demo.kubeops.dev", ApiVersion = "v1", Kind = "DemoEntity")]
[ScaleSubresource(".spec.replicas", ".status.replicas", ".status.selector")]
public class V1DemoEntity : CustomKubernetesEntity<V1DemoEntity.V1DemoEntitySpec, V1DemoEntity.V1DemoEntityStatus>
{
public class V1DemoEntitySpec
{
public int Replicas { get; set; } = 1;
}

public class V1DemoEntityStatus
{
public int Replicas { get; set; }
public required string Selector { get; init; }
}
}

Generated CRD:

subresources:
status: {}
scale:
specReplicasPath: .spec.replicas
statusReplicasPath: .status.replicas
labelSelectorPath: .status.selector
note

[ScaleSubresource] and the status subresource are controlled independently. A Status property activates status: {} regardless of [ScaleSubresource], and [ScaleSubresource] adds scale: regardless of whether a Status property exists.