NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages
Kubernetes API for Laravel

Connecting to a cluster

Every request goes through a Cluster — an immutable client that knows the apiserver URL and how to authenticate. Kubernetes::url() starts a blank ad-hoc one; build it fluently with tokens, client certificates and a CA bundle:

use RoundlyConsulting\KubernetesApi\Facades\Kubernetes;

$cluster = Kubernetes::url('https://api.prod.example.com:6443')
    ->withToken(config('services.kubernetes.token'))
    ->withCertificate('/path/to/client.crt')
    ->withPrivateKey('/path/to/client.key')
    ->withCaCertificate('/path/to/ca.crt')
    ->withSslVerification()
    ->withManagerName('my-app'); // the field manager on resources this app writes

$cluster->withToken('other');   // returns a copy — $cluster still uses the original token

Kubernetes::url() inherits nothing — no token, no certificate — from the default cluster. For local development you can skip the certificates and disable verification; never do this in production:

$cluster = Kubernetes::url('https://127.0.0.1:6443')
    ->withToken('local-dev-token')
    ->withoutSslVerification();

url(), every with…() / without…(), withManagerName(), withDefaultNamespace() and namespace() return a new cluster and leave the one you called untouched — a client handed out by the facade can never be re-pointed or re-credentialed by another caller.

Authentication API

Method (each returns a copy)Effect
url($url)Apiserver base URL, e.g. https://api.prod.example.com:6443.
withToken($token)Bearer token sent with every request (a #[SensitiveParameter], so it never shows in stack traces). Replaces a token file.
withTokenFile($path)A bearer-token file, read again for every request, so a rotated token is picked up. A token already set stays the fallback while the file can’t be read.
withCertificate($path) / withPrivateKey($path)Client certificate and key for mutual TLS.
withCaCertificate($path)CA bundle the apiserver certificate is verified against.
withSslVerification() / withoutSslVerification()Toggle TLS verification — on by default.
withManagerName($name)Sent as fieldManager on every create, update, patch, scale and rollout restart, and as the User-Agent of every request.
withDefaultNamespace($namespace)Soft default namespace for namespaced resources — unlike namespace(), it refuses nothing.
withRedirects() / withoutRedirects()Opt in to following HTTP redirects, or drop the opt-in — off by default.
withRedactedErrors()KubernetesException messages carry status and reason only.
getUrl() · getToken() · getTokenFile() · hasToken() · shouldVerify() · getManagerName() · defaultNamespace() · followsRedirects() · redactsErrors()Readers for the settings above. getToken() returns the token file’s current content, else the token.
getPathToCertificate() · getPathToPrivateKey() · getPathToCaCertificate()Readers for the certificate paths.

Tokens become an Authorization: Bearer header, the certificate and key are handed to the HTTP client for mutual TLS, and the CA path replaces the system trust store for verification. The manager name is sent as the fieldManager query parameter on every create, update, patch, scale and rollout restart, so managedFields records your app as the owner of what it wrote; it is also sent as the User-Agent. A server-side apply needs a field manager, so without a manager name it falls back to kubernetes-api-for-laravel.

Token files and rotated tokens

withTokenFile() points the cluster at a bearer-token file instead of a fixed token. The file is read again for every HTTP request and exec, so a token that gets rotated — a projected service-account token, a kubeconfig tokenFile — is picked up without a restart; while the file can’t be read, a token set alongside it stays the fallback:

// A token file is read again for every request, so a rotated token is picked up.
$cluster = Kubernetes::url('https://api.prod.example.com:6443')
    ->withTokenFile('/var/run/secrets/tokens/deployer')
    ->withCaCertificate('/path/to/ca.crt');

$cluster->getTokenFile();   // '/var/run/secrets/tokens/deployer'
$cluster->getToken();       // the file's current content

$cluster->withToken($token); // a fixed token replaces the token file

In-cluster clusters work this way on their own: the kubelet rotates the projected token (at 80% of its lifetime or every 24 hours) and the cluster re-reads it, so a long-running queue, Horizon or Octane worker never sends a stale one.

From a kubeconfig or in-cluster

Point the client at a cluster in one line instead of hand-wiring the URL and credentials. fromKubeConfig() parses the kubeconfig, resolves the named context’s cluster and user, and materialises any inline certificate data to private temp files — one per distinct PEM, removed when the PHP process exits. inCluster() reads the service-account token and CA mounted into a pod:

use RoundlyConsulting\KubernetesApi\Facades\Kubernetes;

// Current context from ~/.kube/config — or from every KUBECONFIG file, merged like kubectl:
$cluster = Kubernetes::fromKubeConfig();

// A specific context, optionally from an explicit kubeconfig path:
$cluster = Kubernetes::fromKubeConfig(context: 'local-dev');
$cluster = Kubernetes::fromKubeConfig(path: '/path/to/kubeconfig', context: 'staging');

// From in-cluster service-account mounts (when running inside a pod):
$cluster = Kubernetes::inCluster()->withManagerName('my-app');
  • Without a path, every file in the KUBECONFIG environment variable is merged the way kubectl merges them — missing files are skipped, and the first file to set current-context, or to define a cluster, user or context of a given name, wins — falling back to ~/.kube/config. Without a context, current-context is used.
  • From the user entry the loader reads a static token, a tokenFile (re-read for every request) and/or client-certificate and client-key; from the cluster entry the server, certificate-authority and insecure-skip-tls-verify. Each certificate can be a file path or an inline *-data block; relative paths (certificate-authority: certs/ca.crt, a tokenFile) resolve against the directory of the kubeconfig that defines them, as kubectl does.
  • A user that signs in any other way — an exec credential plugin (the EKS, GKE and AKS style), an auth-provider or a username and password — throws KubeConfigException naming the user and the method, instead of loading without credentials and sending every request anonymously. Mint a token outside and use the url source, or point the user at a tokenFile.
  • A context’s namespace becomes the default namespace of fromKubeConfig() and connect() clusters, and of a kubeconfig-source cluster that sets no namespace of its own: the cluster’s namespace wins, then the context’s, then default. It never overrides a namespace scope.
  • A cluster’s insecure-skip-tls-verify must be a boolean — true/false, also yes/no, on/off or 1/0, quoted or not. Anything else throws KubeConfigException instead of guessing, so a quoted “false” keeps TLS verification on.
  • inCluster() builds https://KUBERNETES_SERVICE_HOST:KUBERNETES_SERVICE_PORT (IPv6 hosts included) and reads the token from /var/run/secrets/kubernetes.io/serviceaccount/token — and again for every request, so the rotated token is always the one sent. TLS verification stays on against the mounted ca.crt — if that file is missing it throws KubeConfigException rather than send the token to an unverified apiserver.
  • A missing file, context, cluster or user, invalid YAML (reported with the file and line only, never the content), a cluster without a server URL, an invalid insecure-skip-tls-verify, invalid base64, an unreadable tokenFile, missing in-cluster env vars, an empty token or a missing in-cluster CA all throw KubeConfigException. Every loader parameter that carries part of the kubeconfig is a #[SensitiveParameter], so its credentials never reach a stack trace.
  • The same sources work from config — 'source' => 'kubeconfig' or 'source' => 'in-cluster' on a clusters entry (see Named clusters).

Connecting from a KubeConfig

Both loaders produce a KubeConfig value object — the server, token and certificate paths, verify, plus tokenFile (re-read for every request, with token as the fallback) and namespace (the context’s). Kubernetes::connect() turns one into a new ad-hoc cluster, so you can build it from your own secrets store the same way; Cluster::applyConfig() applies one to a copy of an existing cluster:

use RoundlyConsulting\KubernetesApi\DataTransferObjects\KubeConfig;
use RoundlyConsulting\KubernetesApi\Facades\Kubernetes;

$cluster = Kubernetes::connect(new KubeConfig(
    server: 'https://api.staging.example.com:6443',
    token: config('services.kubernetes.staging_token'),
    clientCertificatePath: null,
    clientKeyPath: null,
    certificateAuthorityPath: storage_path('kubernetes/staging-ca.crt'),
    verify: true,
    tokenFile: null,       // a token file re-read for every request; token is the fallback
    namespace: 'shop',     // becomes the new cluster's default namespace
));

Redirects

The apiserver never redirects an API call, so no request follows a redirect unless you opt in — ordinary requests, writes and streams (watch, log follow) alike; exec accepts nothing but the WebSocket upgrade. A followed redirect would replay the bearer token, present the client certificate to the new host, replay a write’s body (a Secret’s data) on a 307 or 308, and have the target’s answer parsed as the apiserver’s.

  • A 3xx that reaches the client throws KubernetesException::redirectNotFollowed(). The message names the status and the target’s origin only (scheme://host[:port]) — never its path, query or body — and ping() reports false.
  • A cluster URL that redirects, such as http:// to https://, fails with that error: fix the URL, or opt in.
  • Per request, the cluster’s opt-in wins, then an explicit client.options.allow_redirects (the global form, honoured as given), then off.

Opt one cluster in with withRedirects() or clusters.<name>.follow_redirects. It then follows with Guzzle’s defaults — up to five hops, dropping Authorization and cookies on a hop to another origin. The opt-in is transport policy, not a credential, so applyConfig() keeps it:

// Off by default: a 3xx throws KubernetesException instead of being followed.
$cluster = Kubernetes::url('https://proxy.example.com/k8s')
    ->withToken($token)
    ->withRedirects();          // opt this cluster in (Guzzle's defaults)

$cluster->followsRedirects();   // true
$cluster->withoutRedirects();   // a copy without the opt-in

Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

More ways to support, including crypto

By donating, you agree to our donation terms.

Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.