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:
CustomKubernetesEntity: Base class with only metadataCustomKubernetesEntity<TSpec>: Entity with specificationCustomKubernetesEntity<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 insideEntitySpec, that field becomes required within the spec schema. Additionally:- Auto-inference: if any direct property of
EntitySpecis marked[Required], the Transpiler automatically marksspecitself as required at the top-level CRD schema. A[Required]property that is nested under an optional parent does not trigger this — Kubernetes only validatesrequiredconstraints when the parent object is present, so each level of the hierarchy must be annotated explicitly. - Explicit class-level: apply
[Required]directly to theEntitySpecclass to markspecas required at the top level even when no sub-property carries[Required].
- Auto-inference: if any direct property of
// 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 tokubectl getoutput[GenericAdditionalPrinterColumn]: Adds a custom column tokubectl getoutput
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:
| Parameter | Required | Description |
|---|---|---|
specReplicasPath | Yes | JSON path to desired replicas in spec, e.g. .spec.replicas |
statusReplicasPath | Yes | JSON path to observed replicas in status, e.g. .status.replicas |
labelSelectorPath | No | JSON 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
[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.