Custom Resources (CRDs)
PHP K8s makes it easy to work with Custom Resource Definitions (CRDs), allowing you to interact with custom Kubernetes resources as easily as built-in ones.
Quick Start
1. Define Your CRD Class
use RenokiCo\PhpK8s\Contracts\InteractsWithK8sCluster;
use RenokiCo\PhpK8s\Kinds\K8sResource;
class IngressRoute extends K8sResource implements InteractsWithK8sCluster
{
protected static $kind = 'IngressRoute';
protected static $defaultVersion = 'traefik.containo.us/v1alpha1';
protected static $namespaceable = true;
}2. Use the CRD
$ingressRoute = new IngressRoute($cluster, [
'metadata' => [
'name' => 'my-route',
'namespace' => 'default',
],
'spec' => [
'entryPoints' => ['web'],
'routes' => [
[
'match' => 'Host(`example.com`)',
'kind' => 'Rule',
'services' => [
['name' => 'my-service', 'port' => 80]
]
]
]
]
]);
$ingressRoute->create();CRD Class Structure
Required Properties
class MyCRD extends K8sResource implements InteractsWithK8sCluster
{
/**
* The Kubernetes resource kind
*/
protected static $kind = 'MyCRD';
/**
* The API version for this resource
*/
protected static $defaultVersion = 'mygroup.example.com/v1';
/**
* Whether the resource is namespaced
*/
protected static $namespaceable = true; // or false for cluster-scoped
}Optional Traits
Add functionality by using traits:
use RenokiCo\PhpK8s\Traits\Resource\HasSpec;
use RenokiCo\PhpK8s\Traits\Resource\HasStatus;
class MyCRD extends K8sResource implements InteractsWithK8sCluster
{
use HasSpec;
use HasStatus;
protected static $kind = 'MyCRD';
protected static $defaultVersion = 'mygroup.example.com/v1';
protected static $namespaceable = true;
}Using Macros for Dynamic CRDs
For quick prototyping or one-off CRDs, use macros:
use RenokiCo\PhpK8s\K8s;
// Register a CRD macro
K8s::macro('ingressRoute', function ($cluster, array $attributes = []) {
return new IngressRoute($cluster, $attributes);
});
// Use it like built-in resources
$route = K8s::ingressRoute($cluster)
->setName('my-route')
->setNamespace('default')
->setAttribute('spec.entryPoints', ['web'])
->create();Reference CRD Implementations
The library includes several CRD examples in tests/Kinds/ that demonstrate best practices for implementing custom resources. These are not part of the core library but serve as reference implementations:
Volume Snapshot CRDs (snapshot.storage.k8s.io/v1)
Prerequisites: Install the external-snapshotter CRDs.
use RenokiCo\PhpK8s\Test\Kinds\VolumeSnapshot;
use RenokiCo\PhpK8s\Test\Kinds\VolumeSnapshotClass;
use RenokiCo\PhpK8s\Test\Kinds\VolumeSnapshotContent;
// Register the CRD classes
VolumeSnapshot::register();
VolumeSnapshotClass::register();
VolumeSnapshotContent::register();
// Create a VolumeSnapshotClass
$snapClass = $cluster->volumeSnapshotClass()
->setName('csi-hostpath-snapclass')
->setDriver('hostpath.csi.k8s.io')
->setDeletionPolicy('Delete')
->create();
// Create a VolumeSnapshot from a PVC
$snapshot = $cluster->volumeSnapshot()
->setName('my-snapshot')
->setNamespace('default')
->setVolumeSnapshotClassName('csi-hostpath-snapclass')
->setSourcePvcName('my-pvc')
->create();
// Check if ready
if ($snapshot->isReady()) {
echo "Snapshot is ready to use\n";
}Gateway API CRDs (gateway.networking.k8s.io/v1)
Prerequisites: Install the Gateway API CRDs.
use RenokiCo\PhpK8s\Test\Kinds\Gateway;
use RenokiCo\PhpK8s\Test\Kinds\GatewayClass;
Gateway::register();
GatewayClass::register();
$gateway = $cluster->gateway()
->setName('my-gateway')
->setNamespace('default')
->create();Sealed Secrets CRD (bitnami.com/v1alpha1)
Prerequisites: Install Sealed Secrets.
use RenokiCo\PhpK8s\Test\Kinds\SealedSecret;
SealedSecret::register();
$sealedSecret = $cluster->sealedSecret()
->setName('my-secret')
->setNamespace('default')
->create();Reference Implementation
See tests/Kinds/ directory for complete implementation examples including:
- Class structure with traits (HasSpec, HasStatus, HasStatusConditions)
- Custom helper methods
- Test patterns for CRD resources
Common CRD Examples
Traefik IngressRoute
class IngressRoute extends K8sResource implements InteractsWithK8sCluster
{
protected static $kind = 'IngressRoute';
protected static $defaultVersion = 'traefik.containo.us/v1alpha1';
protected static $namespaceable = true;
}
$route = new IngressRoute($cluster, [
'spec' => [
'entryPoints' => ['web', 'websecure'],
'routes' => [
[
'match' => 'Host(`api.example.com`) && PathPrefix(`/v1`)',
'kind' => 'Rule',
'services' => [
['name' => 'api-service', 'port' => 8080]
],
'middlewares' => [
['name' => 'api-auth']
]
]
],
'tls' => [
'secretName' => 'api-tls-cert'
]
]
]);
$route->setName('api-route')->create();Sealed Secret
class SealedSecret extends K8sResource implements InteractsWithK8sCluster
{
protected static $kind = 'SealedSecret';
protected static $defaultVersion = 'bitnami.com/v1alpha1';
protected static $namespaceable = true;
}
$sealedSecret = new SealedSecret($cluster, [
'spec' => [
'encryptedData' => [
'password' => 'AgBy3i4OJSWK+PiTySYZZA9rO43cGDEq...',
],
]
]);
$sealedSecret->setName('my-sealed-secret')->create();Cert-Manager Certificate
class Certificate extends K8sResource implements InteractsWithK8sCluster
{
protected static $kind = 'Certificate';
protected static $defaultVersion = 'cert-manager.io/v1';
protected static $namespaceable = true;
}
$cert = new Certificate($cluster, [
'spec' => [
'secretName' => 'example-tls',
'issuerRef' => [
'name' => 'letsencrypt-prod',
'kind' => 'ClusterIssuer',
],
'dnsNames' => [
'example.com',
'www.example.com',
],
]
]);
$cert->setName('example-cert')->create();Import CRDs from YAML
$crd = $cluster->fromYamlFile('/path/to/custom-resource.yaml');
$crd->create();Advanced CRD Features
Watchable CRDs
Make your CRD watchable:
use RenokiCo\PhpK8s\Contracts\Watchable;
class MyCRD extends K8sResource implements InteractsWithK8sCluster, Watchable
{
// ... class definition
}
// Watch for changes
$cluster->getResourceByName(MyCRD::class, 'my-resource')->watch(function ($type, $resource) {
echo "{$type}: {$resource->getName()}\n";
return false;
});Scalable CRDs
If your CRD supports scaling:
use RenokiCo\PhpK8s\Contracts\Scalable;
class MyCRD extends K8sResource implements InteractsWithK8sCluster, Scalable
{
// ... class definition
public function scale(int $replicas)
{
return $this->setAttribute('spec.replicas', $replicas)->update();
}
}CRDs with Status
use RenokiCo\PhpK8s\Traits\Resource\HasStatus;
class MyCRD extends K8sResource implements InteractsWithK8sCluster
{
use HasStatus;
// ... class definition
}
$resource = $cluster->getResourceByName(MyCRD::class, 'my-resource');
$status = $resource->getStatus();Best Practices
- Organize CRD classes - Keep in dedicated directory (e.g.,
app/Kubernetes/CRDs) - Use traits - Leverage existing traits for common functionality
- Document your CRDs - Add PHPDoc comments
- Version your CRDs - Match Kubernetes CRD versions
- Test with real cluster - Verify CRD operations work correctly
- Handle errors - CRDs may have custom validation
Error Handling
use RenokiCo\PhpK8s\Exceptions\KubernetesAPIException;
try {
$crd->create();
} catch (KubernetesAPIException $e) {
if ($e->getCode() === 404) {
echo "CRD not installed in cluster";
} else {
echo "Error: {$e->getMessage()}";
}
}Complete Example
<?php
use RenokiCo\PhpK8s\Contracts\InteractsWithK8sCluster;
use RenokiCo\PhpK8s\Kinds\K8sResource;
use RenokiCo\PhpK8s\KubernetesCluster;
// Define CRD class
class VirtualService extends K8sResource implements InteractsWithK8sCluster
{
protected static $kind = 'VirtualService';
protected static $defaultVersion = 'networking.istio.io/v1beta1';
protected static $namespaceable = true;
}
// Connect to cluster
$cluster = new KubernetesCluster('http://127.0.0.1:8080');
// Create virtual service
$vs = new VirtualService($cluster, [
'metadata' => [
'name' => 'reviews',
'namespace' => 'default',
],
'spec' => [
'hosts' => ['reviews'],
'http' => [
[
'route' => [
[
'destination' => [
'host' => 'reviews',
'subset' => 'v1',
],
'weight' => 90,
],
[
'destination' => [
'host' => 'reviews',
'subset' => 'v2',
],
'weight' => 10,
],
],
],
],
],
]);
$vs->create();
echo "VirtualService created: {$vs->getName()}\n";
// Update traffic split
$vs->setAttribute('spec.http.0.route.0.weight', 50);
$vs->setAttribute('spec.http.0.route.1.weight', 50);
$vs->update();
echo "Traffic split updated to 50/50\n";Next Steps
- Advanced Macros - Deep dive into macros
- CRD Traits - Available traits for CRDs
- Examples - More CRD examples
Originally from renoki-co/php-k8s documentation, adapted for cuppett/php-k8s fork