Skip to content

Commit

Permalink
WIP - Sandbox Clone Command
Browse files Browse the repository at this point in the history
  • Loading branch information
stovak committed Oct 30, 2024
1 parent a40261d commit 54e65f7
Show file tree
Hide file tree
Showing 3 changed files with 271 additions and 14 deletions.
7 changes: 5 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "*"
}
}
94 changes: 82 additions & 12 deletions src/Commands/SandboxCloneCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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 <source> <destination>
* 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;
}



}


Expand All @@ -69,4 +139,4 @@ public function sandboxClone(



// kubectl config view --minify -o jsonpath='{..namespace}'
//
184 changes: 184 additions & 0 deletions src/Models/KubeContext.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
<?php

namespace Pantheon\TerminusSiteClone\Models;


use Pantheon\Terminus\Config\DefaultsConfig;
use Pantheon\Terminus\Config\DotEnvConfig;
use Pantheon\Terminus\Config\EnvConfig;
use Pantheon\Terminus\Config\YamlConfig;
use Pantheon\Terminus\Exceptions\TerminusException;
use Pantheon\Terminus\Terminus;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\ConsoleOutput;
use Symfony\Component\Console\Output\OutputInterface;

/**
*
*/
class KubeContext
{
/**
* @var string
*/
private ?string $cluster;
/**
* @var string
*/
private ?string $ns;

/**
* @param $context
* @param $kubeconfig
*/
public function __construct(string $cluster, string $namespace)
{
$this->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;
}

}

0 comments on commit 54e65f7

Please sign in to comment.