Skip to content

Commit

Permalink
Merge pull request #75 from renoki-co/feature/in-cluster-auth
Browse files Browse the repository at this point in the history
[2.x] In-cluster configuration for Pod-based apps
  • Loading branch information
rennokki authored Mar 22, 2021
2 parents 1545ca3 + 29c00eb commit 3013b2a
Show file tree
Hide file tree
Showing 5 changed files with 108 additions and 19 deletions.
8 changes: 8 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions docs/Cluster.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
//
}
```
7 changes: 0 additions & 7 deletions src/KubernetesCluster.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,6 @@ class KubernetesCluster
*/
protected $url;

/**
* The API port.
*
* @var int
*/
protected $port = 8080;

/**
* The class name for the K8s resource.
*
Expand Down
39 changes: 38 additions & 1 deletion src/Traits/Cluster/AuthenticatesCluster.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace RenokiCo\PhpK8s\Traits\Cluster;

use RenokiCo\PhpK8s\Kinds\K8sResource;

trait AuthenticatesCluster
{
/**
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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);
}
}
55 changes: 44 additions & 11 deletions tests/KubeConfigTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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));
Expand All @@ -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);
Expand All @@ -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');
}
}

0 comments on commit 3013b2a

Please sign in to comment.