k8s / client
A Kubernetes client for PHP.
Requires
- php: >=7.2
- ext-json: *
- doctrine/annotations: ^1.0
- k8s/api: *
- k8s/core: ^1.3
- klkvsk/json-decode-stream: ^1.0
- php-http/discovery: ^1.0
- psr/http-client: ^1.0
- psr/http-factory: ^1.0
- psr/simple-cache: ^1.0
- symfony/yaml: >=3.4
Requires (Dev)
- friendsofphp/php-cs-fixer: ^2.0
- k8s/http-symfony: ^1.0
- k8s/ws-ratchet: ^1.0
- mockery/mockery: ~1.3.0
- php-http/httplug: ^2.0
- phpstan/phpstan: ^0.12
- phpunit/phpunit: ^8.5
Suggests
- k8s/http-guzzle: Guzzle based HttpClient for k8s client auto-configuration.
- k8s/http-symfony: Symfony based HttpClient for k8s client auto-configuration.
- k8s/ws-ratchet: Ratchet based websocket adapter for websocket based API requests.
- k8s/ws-swoole: Swoole based websocket adapter for websocket based API requests.
- symfony/cache: For a PSR-16 implementation to cache Kubernetes model metadata information.
README
k8s-client is a Kubernetes API client for PHP.
- HTTP Client agnostic (supports any PSR-18 compatible HTTP Client)
- Supports all major API operations (read, watch, list, patch, delete, exec, attach, logs, port-forward, proxy, etc)
- Supports all Kinds from the Kubernetes API (via auto-generated Kind models with annotations and type-hints).
- Pluggable websocket adapter support (For executing commands in pods, attaching, port-forwarding, etc)
The Kind models are auto-generated nightly for the last 10 versions of the Kubernetes API.
- Installation
- Examples
- List All Pods
- Watch All Deployments in a Namespace
- Create a Pod
- Create a Deployment
- Patch a Deployment
- Get Logs for a Pod
- Follow Logs for a Pod
- Execute a Command in a Pod
- Attach to the Running Process of a Pod
- Download Files from a Pod
- Upload Files to a Pod
- Port Forwarding from a Pod
- Proxy HTTP Requests to a Pod
Installation
Install using composer:
# Install the base client composer require k8s/client # Plan on using the Symfony HttpClient? Install the auto-configuration helper for it. composer require k8s/http-symfony # Plan on needing to use things like executing commands, port-forwarding? # Install a websocket adapter. composer require k8s/ws-ratchet
This library requires a PSR-18 compatible HTTP Client, such as Guzzle or Symfony's HttpClient. It can also be given a PSR-16 compatible Simple Cache implementation to help speed up the library.
Using a Specific Kubernetes API version
Each Kubernetes version may have different resources and operations. If you require a specific version, then you can
require the version of the k8s/api
library that you need to use. That library contains all the API specific versions
and models that are consumed by this library.
For instance, to use API version 1.18:
composer require k8s/api:"~1.18.0"
Note: The version of k8s/api
does not exactly reflect the version of the Kubernetes API. The patch version of
Kubernetes may not be the same as the k8s/api
patch version.
Installing a Websocket Adapter
Certain Kuberenetes API endpoints (such as exec, to run commands in a container) require websockets to communicate. If you need support for this, install one of these adapters:
-
ReactPHP based websocket adapter (https://github.com/k8s-client/ws-ratchet):
composer require k8s/ws-ratchet
-
Swoole based websocket adapter (https://github.com/k8s-client/ws-swoole):
composer require k8s/ws-swoole
See each library's readme for more configuration information.
Constructing the Client Automatically
The easiest way to construct the client is from a pre-defined KubeConfig:
use K8s\Client\K8sFactory; # Attempt to load the default kubeconfig file: $k8s = (new K8sFactory())->loadFromKubeConfig(); # Attempt to load a specific kubeconfig file from a full file path: $k8s = (new K8sFactory())->loadFromKubeConfigFile('/my/special/.kube/config'); # Load a kubeconfig from string kubeconfig data: $k8s = (new K8sFactory())->loadFromKubeConfigData($kubeConfigData);
Note: This requires the use of an HttpClient factory helper. Install one of these packages:
k8s/http-symfony
(Symfony based HttpClient)k8s/http-guzzle
(Guzzle based HttpClient)
Constructing the Client Manually
Construct the client with your needed options:
use K8s\Client\K8s; use K8s\Client\Options; # Supply the base path to the Kubernetes API endpoint: $options = new Options('https://127.0.0.1:8443'); # To use an API token for authentication, set it in the options: $options->setToken('some-secret-token-value-goes-here'); $k8s = new K8s($options);
Note: If you need to perform certificate based authentication, check the options for the HttpClient you are using. Also check the configuration for the websocket adapter you are using.
Examples
List all Pods
use K8s\Api\Model\Api\Core\v1\Pod; use K8s\Client\K8sFactory; $k8s = (new K8sFactory())->loadFromKubeConfig(); /** @var Pod $pod */ foreach ($k8s->listAll(Pod::class) as $pod) { echo sprintf( "%s\t%s\t%s", $pod->getPodIP(), $pod->getNamespace(), $pod->getName() ) . PHP_EOL; }
Watch all Deployments in a Namespace
use K8s\Api\Model\Api\Apps\v1\Deployment; use K8s\Api\Model\ApiMachinery\Apis\Meta\v1\WatchEvent; use K8s\Client\K8sFactory; $k8s = (new K8sFactory())->loadFromKubeConfig(); $count = 0; # This will watch all deployments in the default namespace. # Change the namespace either in the Options above or as a parameter to the watchNamespaced method below. $k8s->watchNamespaced(function (WatchEvent $event) use (&$count) { $count++; /** @var Deployment $object */ $object = $event->getObject(); echo sprintf( "%s\t%s\t%s\t%s", $event->getType(), $object->getName(), $object->getReplicas(), implode(',', (array)$object->getLabels()) ) . PHP_EOL; # Return false if some condition is met to stop watching. if ($count >= 5) { return false; } }, Deployment::class);
Create a Pod
Using model classes:
use K8s\Api\Model\Api\Core\v1\Container; use K8s\Api\Model\Api\Core\v1\Pod; use K8s\Client\K8sFactory; $k8s = (new K8sFactory())->loadFromKubeConfig(); # Create a pod with the name "web" using the nginx:latest image... $pod = new Pod( 'web', [new Container('web', 'nginx:latest')] ); # Create will return the updated Pod object after creation in this instance... $pod = $k8s->create($pod); var_dump($pod);
Using array data:
use K8s\Client\K8sFactory; $k8s = (new K8sFactory())->loadFromKubeConfig(); # Create a pod with the name "web" using the nginx:latest image... # Create will return the updated Pod object after creation in this instance... $pod = $k8s->create($k8s->newKind([ 'apiVersion' => 'v1', 'kind' => 'Pod', 'metadata' => [ 'name' => 'web', ], 'spec' => [ 'containers' => [ [ 'image' => 'nginx:latest', 'name' => 'web', ], ] ], ])); var_dump($pod);
Create a Deployment
Using model classes:
use K8s\Api\Model\Api\Apps\v1\Deployment; use K8s\Api\Model\ApiMachinery\Apis\Meta\v1\LabelSelector; use K8s\Api\Model\Api\Core\v1\Container; use K8s\Api\Model\Api\Core\v1\PodTemplateSpec; use K8s\Client\K8sFactory; $k8s = (new K8sFactory())->loadFromKubeConfig(); # All deployments need a "template" that describes the Pod spec $template = new PodTemplateSpec( 'frontend', [new Container('frontend', 'nginx:latest')] ); # The template must have a label that matches the label selector below $template->setLabels(['app' => 'web']); # Create a deployment called "frontend" with the given template. $deployment = new Deployment( 'frontend', new LabelSelector([], ['app' => 'web']), $template ); $result = $k8s->create($deployment); # Create for a deployment will return a Status object for the creation var_dump($result);
Using array data:
use K8s\Client\K8sFactory; $k8s = (new K8sFactory())->loadFromKubeConfig(); # Create a deployment with the given array data matching what you want. $result = $k8s->create($k8s->newKind([ 'apiVersion' => 'apps/v1', 'kind' => 'Deployment', 'metadata' => [ 'name' => 'frontend', 'labels' => [ 'app' => 'web', ] ], 'spec' => [ 'selector' => [ 'matchLabels' => [ 'app' => 'web', ] ], 'template' => [ 'metadata' => [ 'labels' => [ 'app' => 'web', ] ], 'spec' => [ 'containers' => [ [ 'image' => 'nginx:latest', 'name' => 'frontend', ], ], ], ], ], ])); # Create for a deployment will return a Status object for the creation var_dump($result);
Proxy HTTP requests to a Pod
The proxy method sends an HTTP request to a path of a pod, service, or node. It makes no assumptions about what type of HTTP request you want to send, so it accepts a standard PSR-7 RequestInterface and returns a ResponseInterface.
use Http\Discovery\Psr17FactoryDiscovery; use K8s\Api\Model\Api\Core\v1\Pod; use K8s\Client\K8sFactory; $k8s = (new K8sFactory())->loadFromKubeConfig(); # Get the pod you want to proxy to first $pod = $k8s->read('web', Pod::class); # Create the HTTP request you'd like to send to it $requestFactory = Psr17FactoryDiscovery::findRequestFactory(); $request = $requestFactory->createRequest('GET', '/'); # Send the request to proxy, dump the results # The result will be the raw PSR-7 ResultInterface class. $result = $k8s->proxy($pod, $request); echo (string)$result->getBody().PHP_EOL;
Get Logs for a Pod
use K8s\Client\K8sFactory; $k8s = (new K8sFactory())->loadFromKubeConfig(); # Read logs from a pod called "web". # Also append all log entries with a timestamp (ISO8601) $log = $k8s->logs('web') ->withTimestamps() ->read(); var_dump($log);
Follow Logs for a Pod
use K8s\Client\K8sFactory; $k8s = (new K8sFactory())->loadFromKubeConfig(); $count = 0; # Follow logs from a pod called "web". # Also append all log entries with a timestamp (ISO8601) $k8s->logs('web') ->withTimestamps() ->follow(function (string $log) use (&$count) { $count++; var_dump($log); # Return false at any point to stop following the logs. if ($count >= 5) { return false; } });
Execute a command in a Pod container
use K8s\Client\K8sFactory; $k8s = (new K8sFactory())->loadFromKubeConfig(); # Print the result of "whoami". $k8s->exec('web', '/usr/bin/whoami') ->useStdout() ->run(function (string $channel, string $data) { echo sprintf( '%s => %s', $channel, $data ) . PHP_EOL; });
Attach to the running process of a container in a Pod
use K8s\Client\K8sFactory; $k8s = (new K8sFactory())->loadFromKubeConfig(); # Attaches to the main running process of the container in the Pod $k8s->attach('my-pod') # You must specify at least one of useStdout(), useStderr(), useStdin() ->useStdout() # Prints out any STDOUT from the main running process # Can also pass it an instance of ContainerExecInterface ->run(function (string $channel, string $data) { echo sprintf( "%s => %s", $channel, $data ) . PHP_EOL; });
Patch a Deployment
use K8s\Api\Model\Api\Apps\v1\Deployment; use K8s\Client\Patch\JsonPatch; use K8s\Client\K8sFactory; $k8s = (new K8sFactory())->loadFromKubeConfig(); $patch = new JsonPatch(); # Since labels are an array, this actually replaces existing labels $patch->add('/metadata/labels', ['app' => 'web']); # Replaces the current replica value with 2 $patch->replace('/spec/replicas', 2); # We first need to read the deployment we want to patch. $deployment = $k8s->read('frontend', Deployment::class); # Now we patch the deployment using the patch object. The returned value will be the updated deployment. $deployment = $k8s->patch($deployment, $patch); echo sprintf( 'Replicas: %s, Labels: %s', $deployment->getReplicas(), implode(',', $deployment->getLabels()) ) . PHP_EOL;
Upload Files to a Pod
use K8s\Client\K8sFactory; $k8s = (new K8sFactory())->loadFromKubeConfig(); $k8s->uploader('my-pod') # Add files from paths. # The first argument is the source location, the second is the destination for it on the container. ->addFile('/path/to/local/file.txt', '/tmp/file.txt') # Add files from string data. # The first argument is the destination path on the container. The second is the file contents as a string. ->addFileFromString('/tmp/hi.txt', 'Oh, hi Mark.') # This actually initiates the upload process. ->upload();
Download Files from a Pod
use K8s\Client\K8sFactory; $k8s = (new K8sFactory())->loadFromKubeConfig(); $archive = $k8s->downloader('my-pod') # Optionally choose to compress the downloaded files (gzip -- tar.gz) ->compress() # The file(s) or directory to download. Can be an array of files, or just a single directory or file. ->from('/etc') # If you don't specify to() it will download to a temp file. ->to(__DIR__ . '/' . 'podFiles.tar.gz') # Initiate the download process. ->download(); # The full path to the downloaded files archive.. echo (string)$archive . PHP_EOL; # Extract the downloaded files to a directory called "podFiles" in the current directory.. mkdir(__DIR__ . '/podFiles'); $archive->extractTo(__DIR__ . '/podFiles');
Port Forwarding from a Pod
Note: The below example assumes a pod called portforward-example
exists with port 80 serving HTTP (such as a base nginx image).
Create a class that reacts to port forwarding events:
namespace App; use K8s\Client\Websocket\Contract\PortChannelInterface; use K8s\Client\Websocket\Contract\PortForwardInterface; use K8s\Client\Websocket\PortChannels; class PortForwarder implements PortForwardInterface { /** * @var PortChannels */ private $portChannels; /** * @inheritDoc */ public function onInitialize(PortChannels $portChannels) : void { $this->portChannels = $portChannels; # On initialize, send this HTTP request across. # Due to "Connection: close" HTTP instruction, the websocket will close after the response is received. # In a more realistic situation, you'd probably want to keep this open, and react in the onDataReceived method. $data = "GET / HTTP/1.1\r\n"; $data .= "Host: 127.0.0.1\r\n"; $data .= "Connection: close\r\n"; $data .= "Accept: */*\r\n"; $data .= "\r\n"; $this->portChannels->writeToPort(80, $data); } /** * @inheritDoc */ public function onDataReceived(string $data, PortChannelInterface $portChannel) : void { echo sprintf( 'Received data on port %s:', $portChannel->getPortNumber() ) . PHP_EOL; echo $data . PHP_EOL; } /** * @inheritDoc */ public function onErrorReceived(string $data, PortChannelInterface $portChannel) : void { echo sprintf( 'Received error on port %s: %s', $portChannel->getPortNumber(), $data ) . PHP_EOL; } /** * @inheritDoc */ public function onClose() : void { # Do something here to clean-up resources when the connection is closed... } }
Use the above class as a handler for the port forward process:
use App\PortForwarder; use K8s\Client\K8sFactory; $k8s = (new K8sFactory())->loadFromKubeConfig(); $handler = new PortForwarder(); # Assuming a Pod with a basic HTTP port 80 exposed... $k8s->portforward('portforward-example', 80) ->start($handler);