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

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]: Preserves unknown fields in the Kubernetes object
  • [EmbeddedResource]: Marks a property as an embedded Kubernetes resource

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; }
}
}

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.