diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3426e00e..e3a96859 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -71,6 +71,14 @@ jobs: composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update composer update --${{ matrix.prefer }} --prefer-dist --no-interaction --no-suggest + - name: Setup in-cluster config + run: | + sudo mkdir -p /var/run/secrets/kubernetes.io/serviceaccount + echo "some-token" | sudo tee /var/run/secrets/kubernetes.io/serviceaccount/token + echo "c29tZS1jZXJ0Cg==" | sudo tee /var/run/secrets/kubernetes.io/serviceaccount/ca.crt + echo "some-namespace" | sudo tee /var/run/secrets/kubernetes.io/serviceaccount/namespace + sudo chmod -R 777 /var/run/secrets/kubernetes.io/serviceaccount/ + - name: Run tests run: | vendor/bin/phpunit --coverage-text --coverage-clover=coverage.xml diff --git a/docs/Cluster.md b/docs/Cluster.md index d5219614..d5d418c9 100644 --- a/docs/Cluster.md +++ b/docs/Cluster.md @@ -51,3 +51,21 @@ For testing purposes or local checkups, you can disable SSL checks: ```php $cluster->withoutSslChecks(); ``` + +## In-Cluster Configuration + +Kubernetes allows Pods to access [the internal kubeapi within a container](https://kubernetes.io/docs/tasks/run-application/access-api-from-pod/). + +PhpK8s allows you to set up an in-cluster-ready client with minimal configuration. Please keep in mind that this works only within pods that run in a Kubernetes cluster. + +```php +use RenokiCo\PhpK8s\KubernetesCluster; + +$cluster = new KubernetesCluster('https://kubernetes.default.svc'); + +$cluster->inClusterConfiguration(); + +foreach ($cluster->getAllServices() as $svc) { + // +} +``` diff --git a/src/KubernetesCluster.php b/src/KubernetesCluster.php index 7d4471ae..3502723e 100644 --- a/src/KubernetesCluster.php +++ b/src/KubernetesCluster.php @@ -19,13 +19,6 @@ class KubernetesCluster */ protected $url; - /** - * The API port. - * - * @var int - */ - protected $port = 8080; - /** * The class name for the K8s resource. * diff --git a/src/Traits/Cluster/AuthenticatesCluster.php b/src/Traits/Cluster/AuthenticatesCluster.php index 106f0b54..c9d01ba9 100644 --- a/src/Traits/Cluster/AuthenticatesCluster.php +++ b/src/Traits/Cluster/AuthenticatesCluster.php @@ -2,6 +2,8 @@ namespace RenokiCo\PhpK8s\Traits\Cluster; +use RenokiCo\PhpK8s\Kinds\K8sResource; + trait AuthenticatesCluster { /** @@ -52,7 +54,7 @@ trait AuthenticatesCluster */ public function withToken(string $token = null) { - $this->token = str_replace(["\r", "\n"], '', $token); + $this->token = $this->normalize($token); return $this; } @@ -134,4 +136,39 @@ public function withoutSslChecks() return $this; } + + /** + * Load the in-cluster configuration to run the code + * under a Pod in a cluster. + * + * @return $this + */ + public function inClusterConfiguration() + { + if (file_exists($tokenPath = '/var/run/secrets/kubernetes.io/serviceaccount/token')) { + $this->loadTokenFromFile($tokenPath); + } + + if (file_exists($caPath = '/var/run/secrets/kubernetes.io/serviceaccount/ca.crt')) { + $this->withCaCertificate($caPath); + } + + if ($namespace = @file_get_contents('/var/run/secrets/kubernetes.io/serviceaccount/namespace')) { + K8sResource::setDefaultNamespace($this->normalize($namespace)); + } + + return $this; + } + + /** + * Replace \r and \n with nothing. Used to read + * strings from files that might contain extra chars. + * + * @param string $content + * @return string + */ + protected function normalize(string $content): string + { + return str_replace(["\r", "\n"], '', $content); + } } diff --git a/tests/KubeConfigTest.php b/tests/KubeConfigTest.php index e253903d..cda1651e 100644 --- a/tests/KubeConfigTest.php +++ b/tests/KubeConfigTest.php @@ -5,6 +5,7 @@ use RenokiCo\PhpK8s\Exceptions\KubeConfigClusterNotFound; use RenokiCo\PhpK8s\Exceptions\KubeConfigContextNotFound; use RenokiCo\PhpK8s\Exceptions\KubeConfigUserNotFound; +use RenokiCo\PhpK8s\Kinds\K8sResource; use RenokiCo\PhpK8s\KubernetesCluster; class KubeConfigTest extends TestCase @@ -21,13 +22,15 @@ public function setUp(): void public function test_kube_config_from_yaml_file_with_base64_encoded_ssl() { - $this->cluster->fromKubeConfigYamlFile(__DIR__.'/cluster/kubeconfig.yaml', 'minikube'); + $cluster = new KubernetesCluster('http://127.0.0.1:8080'); + + $cluster->fromKubeConfigYamlFile(__DIR__.'/cluster/kubeconfig.yaml', 'minikube'); [ 'verify' => $caPath, 'cert' => $certPath, 'ssl_key' => $keyPath, - ] = $this->cluster->getClient()->getConfig(); + ] = $cluster->getClient()->getConfig(); $this->assertEquals("some-ca\n", file_get_contents($caPath)); $this->assertEquals("some-cert\n", file_get_contents($certPath)); @@ -36,13 +39,15 @@ public function test_kube_config_from_yaml_file_with_base64_encoded_ssl() public function test_kube_config_from_yaml_file_with_paths_to_ssl() { - $this->cluster->fromKubeConfigYamlFile(__DIR__.'/cluster/kubeconfig.yaml', 'minikube-2'); + $cluster = new KubernetesCluster('http://127.0.0.1:8080'); + + $cluster->fromKubeConfigYamlFile(__DIR__.'/cluster/kubeconfig.yaml', 'minikube-2'); [ 'verify' => $caPath, 'cert' => $certPath, 'ssl_key' => $keyPath, - ] = $this->cluster->getClient()->getConfig(); + ] = $cluster->getClient()->getConfig(); $this->assertEquals('/path/to/.minikube/ca.crt', $caPath); $this->assertEquals('/path/to/.minikube/client.crt', $certPath); @@ -51,40 +56,68 @@ public function test_kube_config_from_yaml_file_with_paths_to_ssl() public function test_kube_config_from_yaml_cannot_load_if_no_cluster() { + $cluster = new KubernetesCluster('http://127.0.0.1:8080'); + $this->expectException(KubeConfigClusterNotFound::class); - $this->cluster->fromKubeConfigYamlFile(__DIR__.'/cluster/kubeconfig.yaml', 'minikube-without-cluster'); + $cluster->fromKubeConfigYamlFile(__DIR__.'/cluster/kubeconfig.yaml', 'minikube-without-cluster'); } public function test_kube_config_from_yaml_cannot_load_if_no_user() { + $cluster = new KubernetesCluster('http://127.0.0.1:8080'); + $this->expectException(KubeConfigUserNotFound::class); - $this->cluster->fromKubeConfigYamlFile(__DIR__.'/cluster/kubeconfig.yaml', 'minikube-without-user'); + $cluster->fromKubeConfigYamlFile(__DIR__.'/cluster/kubeconfig.yaml', 'minikube-without-user'); } public function test_kube_config_from_yaml_cannot_load_if_wrong_context() { + $cluster = new KubernetesCluster('http://127.0.0.1:8080'); + $this->expectException(KubeConfigContextNotFound::class); - $this->cluster->fromKubeConfigYamlFile(__DIR__.'/cluster/kubeconfig.yaml', 'inexistent-context'); + $cluster->fromKubeConfigYamlFile(__DIR__.'/cluster/kubeconfig.yaml', 'inexistent-context'); } public function test_http_authentication() { - $this->cluster->httpAuthentication('some-user', 'some-password'); + $cluster = new KubernetesCluster('http://127.0.0.1:8080'); - ['auth' => $auth] = $this->cluster->getClient()->getConfig(); + $cluster->httpAuthentication('some-user', 'some-password'); + + ['auth' => $auth] = $cluster->getClient()->getConfig(); $this->assertEquals(['some-user', 'some-password'], $auth); } public function test_bearer_token_authentication() { - $this->cluster->loadTokenFromFile(__DIR__.'/cluster/token.txt'); + $cluster = new KubernetesCluster('http://127.0.0.1:8080'); + + $cluster->loadTokenFromFile(__DIR__.'/cluster/token.txt'); - ['headers' => ['authorization' => $token]] = $this->cluster->getClient()->getConfig(); + ['headers' => ['authorization' => $token]] = $cluster->getClient()->getConfig(); $this->assertEquals('Bearer some-token', $token); } + + public function test_in_cluster_config() + { + $cluster = new KubernetesCluster('http://127.0.0.1:8080'); + + $cluster->inClusterConfiguration(); + + [ + 'headers' => ['authorization' => $token], + 'verify' => $caPath, + ] = $cluster->getClient()->getConfig(); + + $this->assertEquals('Bearer some-token', $token); + $this->assertEquals('/var/run/secrets/kubernetes.io/serviceaccount/ca.crt', $caPath); + $this->assertEquals('some-namespace', K8sResource::$defaultNamespace); + + K8sResource::setDefaultNamespace('default'); + } }