From 96a6425203fd74125b93a63d3082bb685ca82882 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Mon, 30 Aug 2021 17:48:48 +0200 Subject: [PATCH 1/2] feat: add a factory to create an instance from the current kubeconfig files --- src/Exceptions/KubeConfigFileNotFound.php | 10 +++ src/Traits/Cluster/LoadsFromKubeConfig.php | 74 ++++++++++++++++++++++ tests/KubeConfigTest.php | 45 +++++++++++++ tests/cluster/kubeconfig-2.yaml | 10 +++ 4 files changed, 139 insertions(+) create mode 100644 src/Exceptions/KubeConfigFileNotFound.php create mode 100644 tests/cluster/kubeconfig-2.yaml diff --git a/src/Exceptions/KubeConfigFileNotFound.php b/src/Exceptions/KubeConfigFileNotFound.php new file mode 100644 index 00000000..ba114ef0 --- /dev/null +++ b/src/Exceptions/KubeConfigFileNotFound.php @@ -0,0 +1,10 @@ + + */ +class KubeConfigFileNotFound extends PhpK8sException +{ +} diff --git a/src/Traits/Cluster/LoadsFromKubeConfig.php b/src/Traits/Cluster/LoadsFromKubeConfig.php index 27d8f12b..9a2c0184 100644 --- a/src/Traits/Cluster/LoadsFromKubeConfig.php +++ b/src/Traits/Cluster/LoadsFromKubeConfig.php @@ -3,10 +3,13 @@ namespace RenokiCo\PhpK8s\Traits\Cluster; use Exception; +use Illuminate\Support\Arr; use RenokiCo\PhpK8s\Exceptions\KubeConfigClusterNotFound; use RenokiCo\PhpK8s\Exceptions\KubeConfigContextNotFound; +use RenokiCo\PhpK8s\Exceptions\KubeConfigFileNotFound; use RenokiCo\PhpK8s\Exceptions\KubeConfigUserNotFound; use RenokiCo\PhpK8s\Kinds\K8sResource; +use RenokiCo\PhpK8s\KubernetesCluster; trait LoadsFromKubeConfig { @@ -30,6 +33,59 @@ public static function setTempFolder(string $tempFolder) static::$tempFolder = $tempFolder; } + /** + * Creates a KubernetesCluster instance according to the current environment. + * + * This method implements the same connection algorithm as kubectl. + * First, it will read the KUBECONFIG environment variable and merge the referenced YAML files. + * If KUBECONFIG isn't set, the method will try to load $HOME/.kube/config. + * + * The current context will be used unless a context name is explicitly passed as parameter of this method. + * + * @see https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/ + * @see https://github.com/kubernetes/client-go/blob/f6ce18ae578c8cca64d14ab9687824d9e1305a67/tools/clientcmd/loader.go#L139 + * + * @return LoadsFromKubeConfig + * @throws KubeConfigClusterNotFound + * @throws KubeConfigContextNotFound + * @throws KubeConfigFileNotFound + * @throws KubeConfigUserNotFound + */ + public static function create(?string $currentContext = null): KubernetesCluster + { + if (isset($_SERVER['KUBECONFIG'])) { + $paths = array_unique(explode(':', $_SERVER['KUBECONFIG'])); + } else { + $paths = [($_SERVER['HOME'] ?? '').'/.kube/config']; + } + + $kubeconfig = []; + foreach ($paths as $path) { + if ('' === $path || ! @is_readable($path) || false === $yaml = yaml_parse_file($path)) { + continue; + } + + $kubeconfig = self::mergeKubeconfig($kubeconfig, $yaml); + } + + if ([] === $kubeconfig) { + throw new KubeConfigFileNotFound(sprintf('Kubernetes configuration not found (paths: "%s")', implode('", "', $paths))); + } + + if (null === $currentContext && isset($kubeconfig['current-context'])) { + $currentContext = $kubeconfig['current-context']; + } + + if (null === $currentContext) { + throw new KubeConfigContextNotFound('Kubernetes context not set.'); + } + + $cluster = new KubernetesCluster(''); + $cluster->loadKubeConfigFromArray($kubeconfig, $currentContext); + + return $cluster; + } + /** * Load configuration from a Kube Config context. * @@ -154,4 +210,22 @@ protected function writeTempFileForContext(string $context, string $fileName, st return $tempFilePath; } + + private static function mergeKubeconfig(array $a1, array $a2): array + { + $a1 += $a2; + foreach ($a1 as $key => $value) { + if ( + is_array($value) && + isset($a2[$key]) && + is_array($a2[$key]) && + ! Arr::isAssoc($value) && + ! Arr::isAssoc($a2[$key]) + ) { + $a1[$key] = array_merge($a1[$key], $a2[$key]); + } + } + + return $a1; + } } diff --git a/tests/KubeConfigTest.php b/tests/KubeConfigTest.php index f3ce0ae0..91db42dc 100644 --- a/tests/KubeConfigTest.php +++ b/tests/KubeConfigTest.php @@ -4,6 +4,7 @@ use RenokiCo\PhpK8s\Exceptions\KubeConfigClusterNotFound; use RenokiCo\PhpK8s\Exceptions\KubeConfigContextNotFound; +use RenokiCo\PhpK8s\Exceptions\KubeConfigFileNotFound; use RenokiCo\PhpK8s\Exceptions\KubeConfigUserNotFound; use RenokiCo\PhpK8s\Kinds\K8sResource; use RenokiCo\PhpK8s\KubernetesCluster; @@ -20,6 +21,16 @@ public function setUp(): void KubernetesCluster::setTempFolder(__DIR__.DIRECTORY_SEPARATOR.'temp'); } + /** + * {@inheritDoc} + */ + public function tearDown(): void + { + parent::tearDown(); + + unset($_SERVER['KUBECONFIG']); + } + public function test_kube_config_from_yaml_file_with_base64_encoded_ssl() { $cluster = new KubernetesCluster('http://127.0.0.1:8080'); @@ -187,4 +198,38 @@ public function test_in_cluster_config() K8sResource::setDefaultNamespace('default'); } + + /** + * @dataProvider contextProvider + */ + public function test_from_environment_variable(?string $context, string $domain) + { + $_SERVER['KUBECONFIG'] = __DIR__.'/cluster/kubeconfig.yaml::'.__DIR__.'/cluster/kubeconfig-2.yaml'; + + $cluster = KubernetesCluster::create($context); + $this->assertSame("https://$domain:8443/?", $cluster->getCallableUrl('/', [])); + } + + public function contextProvider(): iterable + { + yield [null, 'minikube']; + yield ['minikube-2', 'minikube-2']; + yield ['minikube-3', 'minikube-3']; + } + + public function test_from_environment_variable_file_not_found() + { + $this->expectException(KubeConfigFileNotFound::class); + + $_SERVER['KUBECONFIG'] = '/notfound'; + KubernetesCluster::create(); + } + + public function test_from_environment_variable_context_not_set() + { + $this->expectException(KubeConfigContextNotFound::class); + + $_SERVER['KUBECONFIG'] = __DIR__.'/cluster/kubeconfig-2.yaml'; + KubernetesCluster::create(); + } } diff --git a/tests/cluster/kubeconfig-2.yaml b/tests/cluster/kubeconfig-2.yaml new file mode 100644 index 00000000..a3f2a77f --- /dev/null +++ b/tests/cluster/kubeconfig-2.yaml @@ -0,0 +1,10 @@ +clusters: + - cluster: + certificate-authority-data: c29tZS1jYQo= # "some-ca" + server: https://minikube-3:8443 + name: minikube-3 +contexts: + - context: + cluster: minikube-3 + user: minikube + name: minikube-3 From af14f5851b077f2e16fd68720eb0845cbbb757ba Mon Sep 17 00:00:00 2001 From: Alex Renoki Date: Fri, 3 Sep 2021 19:55:21 +0300 Subject: [PATCH 2/2] Removed static KubernetesCluster::create Added fromKubeConfigVariable Updated docblocks Renamed functions to better represent their job --- src/Exceptions/KubeConfigFileNotFound.php | 10 --- src/Traits/Cluster/LoadsFromKubeConfig.php | 79 ++++++++++------------ tests/KubeConfigTest.php | 30 +++----- 3 files changed, 43 insertions(+), 76 deletions(-) delete mode 100644 src/Exceptions/KubeConfigFileNotFound.php diff --git a/src/Exceptions/KubeConfigFileNotFound.php b/src/Exceptions/KubeConfigFileNotFound.php deleted file mode 100644 index ba114ef0..00000000 --- a/src/Exceptions/KubeConfigFileNotFound.php +++ /dev/null @@ -1,10 +0,0 @@ - - */ -class KubeConfigFileNotFound extends PhpK8sException -{ -} diff --git a/src/Traits/Cluster/LoadsFromKubeConfig.php b/src/Traits/Cluster/LoadsFromKubeConfig.php index 9a2c0184..d43d72ed 100644 --- a/src/Traits/Cluster/LoadsFromKubeConfig.php +++ b/src/Traits/Cluster/LoadsFromKubeConfig.php @@ -6,10 +6,8 @@ use Illuminate\Support\Arr; use RenokiCo\PhpK8s\Exceptions\KubeConfigClusterNotFound; use RenokiCo\PhpK8s\Exceptions\KubeConfigContextNotFound; -use RenokiCo\PhpK8s\Exceptions\KubeConfigFileNotFound; use RenokiCo\PhpK8s\Exceptions\KubeConfigUserNotFound; use RenokiCo\PhpK8s\Kinds\K8sResource; -use RenokiCo\PhpK8s\KubernetesCluster; trait LoadsFromKubeConfig { @@ -34,56 +32,41 @@ public static function setTempFolder(string $tempFolder) } /** - * Creates a KubernetesCluster instance according to the current environment. + * Loads the configuration fro the KubernetesCluster instance + * according to the current KUBECONFIG environment variable. * - * This method implements the same connection algorithm as kubectl. - * First, it will read the KUBECONFIG environment variable and merge the referenced YAML files. - * If KUBECONFIG isn't set, the method will try to load $HOME/.kube/config. - * - * The current context will be used unless a context name is explicitly passed as parameter of this method. - * - * @see https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/ - * @see https://github.com/kubernetes/client-go/blob/f6ce18ae578c8cca64d14ab9687824d9e1305a67/tools/clientcmd/loader.go#L139 - * - * @return LoadsFromKubeConfig - * @throws KubeConfigClusterNotFound - * @throws KubeConfigContextNotFound - * @throws KubeConfigFileNotFound - * @throws KubeConfigUserNotFound + * @param string|null $context + * @return $this + * @throws \RenokiCo\PhpK8s\Exceptions\KubeConfigClusterNotFound + * @throws \RenokiCo\PhpK8s\Exceptions\KubeConfigContextNotFound + * @throws \RenokiCo\PhpK8s\Exceptions\KubeConfigUserNotFound */ - public static function create(?string $currentContext = null): KubernetesCluster + public function fromKubeConfigVariable(string $context = null) { - if (isset($_SERVER['KUBECONFIG'])) { - $paths = array_unique(explode(':', $_SERVER['KUBECONFIG'])); - } else { - $paths = [($_SERVER['HOME'] ?? '').'/.kube/config']; + if (! isset($_SERVER['KUBECONFIG'])) { + return $this; } + $paths = array_unique(explode(':', $_SERVER['KUBECONFIG'])); $kubeconfig = []; + foreach ($paths as $path) { - if ('' === $path || ! @is_readable($path) || false === $yaml = yaml_parse_file($path)) { + if (! @is_readable($path) || ($yaml = yaml_parse_file($path)) === false) { continue; } - $kubeconfig = self::mergeKubeconfig($kubeconfig, $yaml); + $kubeconfig = static::mergeKubeconfigContents($kubeconfig, $yaml); } - if ([] === $kubeconfig) { - throw new KubeConfigFileNotFound(sprintf('Kubernetes configuration not found (paths: "%s")', implode('", "', $paths))); + if ($kubeconfig === []) { + return $this; } - if (null === $currentContext && isset($kubeconfig['current-context'])) { - $currentContext = $kubeconfig['current-context']; + if (! $context && isset($kubeconfig['current-context'])) { + $context = $kubeconfig['current-context']; } - if (null === $currentContext) { - throw new KubeConfigContextNotFound('Kubernetes context not set.'); - } - - $cluster = new KubernetesCluster(''); - $cluster->loadKubeConfigFromArray($kubeconfig, $currentContext); - - return $cluster; + $this->loadKubeConfigFromArray($kubeconfig, $context); } /** @@ -211,21 +194,29 @@ protected function writeTempFileForContext(string $context, string $fileName, st return $tempFilePath; } - private static function mergeKubeconfig(array $a1, array $a2): array + /** + * Merge the two kubeconfig contents. + * + * @param array $kubeconfig1 + * @param array $kubeconfig2 + * @return array + */ + protected static function mergeKubeconfigContents(array $kubeconfig1, array $kubeconfig2): array { - $a1 += $a2; - foreach ($a1 as $key => $value) { + $kubeconfig1 += $kubeconfig2; + + foreach ($kubeconfig1 as $key => $value) { if ( is_array($value) && - isset($a2[$key]) && - is_array($a2[$key]) && + isset($kubeconfig2[$key]) && + is_array($kubeconfig2[$key]) && ! Arr::isAssoc($value) && - ! Arr::isAssoc($a2[$key]) + ! Arr::isAssoc($kubeconfig2[$key]) ) { - $a1[$key] = array_merge($a1[$key], $a2[$key]); + $kubeconfig1[$key] = array_merge($kubeconfig1[$key], $kubeconfig2[$key]); } } - return $a1; + return $kubeconfig1; } } diff --git a/tests/KubeConfigTest.php b/tests/KubeConfigTest.php index 91db42dc..6988fcf0 100644 --- a/tests/KubeConfigTest.php +++ b/tests/KubeConfigTest.php @@ -4,7 +4,6 @@ use RenokiCo\PhpK8s\Exceptions\KubeConfigClusterNotFound; use RenokiCo\PhpK8s\Exceptions\KubeConfigContextNotFound; -use RenokiCo\PhpK8s\Exceptions\KubeConfigFileNotFound; use RenokiCo\PhpK8s\Exceptions\KubeConfigUserNotFound; use RenokiCo\PhpK8s\Kinds\K8sResource; use RenokiCo\PhpK8s\KubernetesCluster; @@ -200,36 +199,23 @@ public function test_in_cluster_config() } /** - * @dataProvider contextProvider + * @dataProvider environmentVariableContextProvider */ - public function test_from_environment_variable(?string $context, string $domain) + public function test_from_environment_variable(string $context = null, string $expectedDomain) { $_SERVER['KUBECONFIG'] = __DIR__.'/cluster/kubeconfig.yaml::'.__DIR__.'/cluster/kubeconfig-2.yaml'; - $cluster = KubernetesCluster::create($context); - $this->assertSame("https://$domain:8443/?", $cluster->getCallableUrl('/', [])); + $cluster = new KubernetesCluster("https://{$expectedDomain}:8443"); + + $cluster->fromKubeConfigVariable($context); + + $this->assertSame("https://{$expectedDomain}:8443/?", $cluster->getCallableUrl('/', [])); } - public function contextProvider(): iterable + public function environmentVariableContextProvider(): iterable { yield [null, 'minikube']; yield ['minikube-2', 'minikube-2']; yield ['minikube-3', 'minikube-3']; } - - public function test_from_environment_variable_file_not_found() - { - $this->expectException(KubeConfigFileNotFound::class); - - $_SERVER['KUBECONFIG'] = '/notfound'; - KubernetesCluster::create(); - } - - public function test_from_environment_variable_context_not_set() - { - $this->expectException(KubeConfigContextNotFound::class); - - $_SERVER['KUBECONFIG'] = __DIR__.'/cluster/kubeconfig-2.yaml'; - KubernetesCluster::create(); - } }