diff --git a/composer.json b/composer.json index bd80a5d..827a7ae 100644 --- a/composer.json +++ b/composer.json @@ -12,9 +12,12 @@ } }, "require-dev": { - "pantheon-systems/terminus": "^3.5" + "pantheon-systems/terminus": "^3.5", + "friendsofphp/php-cs-fixer": "^3.64", + "squizlabs/php_codesniffer": "^3.10" }, "require": { - "php": "^8.2" + "php": "^8.2", + "ext-json": "*" } } diff --git a/src/Commands/SandboxCloneCommand.php b/src/Commands/SandboxCloneCommand.php index fd42e6b..cb0d23c 100644 --- a/src/Commands/SandboxCloneCommand.php +++ b/src/Commands/SandboxCloneCommand.php @@ -12,6 +12,7 @@ use Pantheon\Terminus\Site\SiteAwareTrait; use Pantheon\Terminus\Commands\Remote\WPCommand; use Symfony\Component\Filesystem\Filesystem; +use Pantheon\TerminusSiteClone\Models\KubeContext; /** * Site Clone Command @@ -21,10 +22,9 @@ class SandboxCloneCommand extends SingleBackupCommand implements RequestAwareInt { use RequestAwareTrait; use WorkflowProcessingTrait; - use SiteAwareTrait; - private $source; - private $destination; + private \Pantheon\Terminus\Models\Site $sourceSite; + private \Pantheon\Terminus\Models\Site $destinationSite; /** * Clones a site from a production pantheon site to a new site @@ -34,32 +34,102 @@ class SandboxCloneCommand extends SingleBackupCommand implements RequestAwareInt * * @param string $source The source site to clone from * @param string $destination The destination site to clone to + * @param array $sourceOptions Options for the source site + * @param array $destinationOptions Options for the destination site * * @usage terminus sandbox:clone * Clones the site from the source to the destination + * + * @assumptions + * - The source site is a pantheon site in the live production environment + * - The destination site is a new site in the current active kube sandbox + * - There is a logged in user with your current terminus production install + * e.g. `terminus auth:login` returns successfully. + * - The user has a pantheon employee certificate and the path to that cert + * is stored in the PANTHEON_CERT environment variable + * - The logged-in terminus platform user has the necessary permissions to clone the site + * - You have set up the TERMINUS_CACHE_DIR for termibox to $HOME/.termibox as per the + * instructions here: + * https://getpantheon.atlassian.net/wiki/spaces/VULCAN/pages/1596064047/Using+Terminus+with+Sandboxes + * - There is a valid cached terminus session for the sandbox in $HOME/.termibox */ public function sandboxClone( - string $source, - string $destination, - array $sourceOptions = ['env' => 'live'], - array $destinationOptions = ['env' => 'dev'] + string $sourceSiteName, + string $destinationSiteName, + array $sourceOptions = [ + 'env' => 'live', + 'cluster' => null, + 'namespace' => null, + ], + array $destinationOptions = [ + 'env' => 'dev', + 'cluster' => null, + 'namespace' => null + ], ){ // get the value of the PANTHEON_CERT env var and use it to load the // user's employee certificate first. $cert = getenv('PANTHEON_CERT'); + if (empty($cert)) { + throw new TerminusException('In order to use this plugin you need a pantheon employee certificate.'); + } + + // Where is the work being done? + $sourceContext = $this->getKubeContext($sourceOptions); + $destinationContext = $this->getKubeContext($destinationOptions); + + if (! $sourceContext instanceof KubeContext || $sourceContext->valid() === false) { + throw new TerminusException('Could not get/parse the kube source context.'); + } + if (! $destinationContext instanceof KubeContext || $destinationContext->valid() === false) { + throw new TerminusException('Could not get/parse the kube destination context.'); + } + $this->log()->notice('PANTHEON_CERT: ' . $cert); $this->log()->notice('Loading employee certificate...'); - $this->request()->setCertificate($cert); - $this->source = $source; - $this->destination = $destination; + // This class should have been pre-populated with a guzzle client + // if not, make one. + $cli = $this->request->getClient(); + if (! $cli instanceof \GuzzleHttp\Client) { + $cli = new \GuzzleHttp\Client(); + } - $this->validateSource(); + + + $this->validateSource($sourceSiteName, $sourceContext); $this->validateDestination(); $this->cloneSite(); } + + + function getKubeContext($options) ?KubeContext { + // get the current context and use as default values + $json = exec("kubectl config view --minify -o jsonpath='{..context}'"); + $toReturn = new KubeContext(); + if (!empty($json)) { + $toReturn = new KubeContext::fromJson($kubeContext); + } + // if the values are set in the options, override all the other values + if (isset($options['cluster']) ) { + $toReturn->setCluster($options['cluster']); + } + if (isset($options['namespace']) ) { + $toReturn->setNamespace($options['namespace']); + } + return $toReturn; + } + + function protected getSiteModelFromSiteName(KubeContext $context, string $siteName): \Pantheon\Terminus\Models\Site { + // get the site model from the site name + + return $site; + } + + + } @@ -69,4 +139,4 @@ public function sandboxClone( -// kubectl config view --minify -o jsonpath='{..namespace}' +// diff --git a/src/Models/KubeContext.php b/src/Models/KubeContext.php new file mode 100644 index 0000000..3a9238b --- /dev/null +++ b/src/Models/KubeContext.php @@ -0,0 +1,184 @@ +cluster = $cluster; + $this->ns = $namespace; + } + + public static function fromJson(string $json): KubeContext + { + $decoded = json_decode($json, true); + if (empty($decoded)) { + $err = json_last_error_msg(); + throw new TerminusException('Could not parse the kubernetes context: ' . $err); + } + $this->cluster = $decoded['cluster']; + $this->ns = $decoded['namespace']; + } + + + /** + * @return string + */ + public function getCluster(): string + { + return $this->cluster; + } + + /** + * @param string $context + * @return void + */ + public function setCluster(string $cluster) + { + $this->cluster = $cluster; + } + + /** + * @return string + */ + public function getNs(): string + { + return $this->ns; + } + + /** + * @param string $kubeconfig + * @return void + */ + public function setNs(string $ns) + { + $this->ns = $ns; + } + + /** + * @return bool + */ + public function valid(): bool + { + return !empty($this->ns); + } + + /** + * @return Terminus + */ + public function getTerminus(InputInterface $input, OutputInterface $output = null): Terminus + { + return new Terminus($this->getConfig(), $input, $output); + } + + /** + * @return array + */ + public function getTerminus(InputInterface $input, OutputInterface $output = null): array + { + // if there's no output provided, just use the default console output + if ($output == null) { + $output = new ConsoleOutput(); + } + $config = new DefaultsConfig(); + // Default root path is the terminus root directory + // note: this may be inside of a phar file + $root_path = $config->get('root'); + // Default home path is the user's home directory + $home_path = $config->get('user_home'); + if ($this->ns !== null && $this->ns !== 'production') { + // if the namespace is not production, we assume it is a sandbox namespace + // and use the defaults folder in the sandbox's home directory + $home_path = $config->get('user_home') . DIRECTORY_SEPARATOR . "." . $this->ns; + $config->set('base_dir', $home_path); + } + // DefaultConstants for every version of terminus + $config->extend(new YamlConfig($root_path . '/config/constants.yml')); + // you can override the constants with a local config file + // inside the sandbox or production directory with a file named config.yml + $config->extend(new YamlConfig($home_path . '/config.yml')); + // you can override the constants with a local env file + // just be sure to preface all the variables with TERMINUS_ + $config->extend(new DotEnvConfig(getcwd())); + $config->extend(new EnvConfig()); + $dependencies_folder_absent = false; + if ($dependencies_version) { + $dependenciesBaseDir = $config->get('dependencies_base_dir'); + $terminusDependenciesDir = $dependenciesBaseDir . '-' . $dependencies_version; + $config->set('terminus_dependencies_dir', $terminusDependenciesDir); + if (file_exists($terminusDependenciesDir . '/vendor/autoload.php')) { + include_once("$terminusDependenciesDir/vendor/autoload.php"); + } else { + $dependencies_folder_absent = true; + } + } + if ($this->ns !== null && $this->ns !== 'production') { + // if the namespace is not production, we assume it is a sandbox namespace + // and get the IP address of the sandbox's load balancer + $external_ip = exec('kubectl get svc pantheonapi -o jsonpath="{.status.loadBalancer.ingress[0].ip}"'); + $config->set("host", "https://${external_ip}"); + // This is the ssh host for the sandbox + $ssh_host = exec( + 'kubectl get nodes --selector alpha.pantheon.io/cos-namespace=sandbox-lops,alpha.pantheon.io/type=appserver -o json | jq -r ".items[0].status.addresses[1].address"' + ); + $config->set("ssh_host", $ssh_host); + } + + + $terminus = new static($config, $input, $output); + + if ($dependencies_folder_absent && $terminus->hasPlugins()) { + $omit_reload_warning = true; + $input_string = (string)$input; + $plugin_reload_command_names = [ + 'self:plugin:reload', + 'self:plugin:refresh', + 'plugin:reload', + 'plugin:refresh', + ]; + foreach ($plugin_reload_command_names as $command_name) { + if (strpos($input_string, $command_name) !== false) { + $omit_reload_warning = true; + break; + } + } + + if (!$omit_reload_warning) { + $terminus->logger->warning( + 'Could not load plugins because Terminus was upgraded. ' . + 'Please run terminus self:plugin:reload to refresh.', + ); + } + } + return $terminus; + } + +} \ No newline at end of file