This dataobject represents a target environment that source code can be deployed to.
Permissions are controlled by environment, see the various many-many relationships.
public function __construct($controller, $name, DNEnvironment $environment, DNProject $project) { if ($environment->HasPipelineSupport()) { // Determine if commits are filtered $canBypass = Permission::check(DNRoot::DEPLOYNAUT_BYPASS_PIPELINE); $canDryrun = $environment->DryRunEnabled && Permission::check(DNRoot::DEPLOYNAUT_DRYRUN_PIPELINE); $commits = $environment->getDependentFilteredCommits(); if (empty($commits)) { // There are no filtered commits, so show all commits $field = $this->buildCommitSelector($project); $validator = new DeployForm_CommitValidator(); } elseif ($canBypass) { // Build hybrid selector that allows users to follow pipeline or use any commit $field = $this->buildCommitSelector($project, $commits); $validator = new DeployForm_CommitValidator(); } else { // Restrict user to only select pipeline filtered commits $field = $this->buildPipelineField($commits); $validator = new DeployForm_PipelineValidator(); } // Generate actions allowed for this user $actions = new FieldList(FormAction::create('startPipeline', "Begin the release process on " . $environment->Name)->addExtraClass('btn btn-primary')->setAttribute('onclick', "return confirm('This will begin a release pipeline. Continue?');")); if ($canDryrun) { $actions->push(FormAction::create('doDryRun', "Dry-run release process")->addExtraClass('btn btn-info')->setAttribute('onclick', "return confirm('This will begin a release pipeline, but with the following exclusions:\\n" . " - No messages will be sent\\n" . " - No capistrano actions will be invoked\\n" . " - No deployments or snapshots will be created.');")); } if ($canBypass) { $actions->push(FormAction::create('doDeploy', "Direct deployment (bypass pipeline)")->addExtraClass('btn btn-warning')->setAttribute('onclick', "return confirm('This will start a direct deployment, bypassing the pipeline " . "process in place.\\n\\nAre you sure this is necessary?');")); } } else { // without a pipeline simply allow any commit to be selected $field = $this->buildCommitSelector($project); $validator = new DeployForm_CommitValidator(); $actions = new FieldList(FormAction::create('doDeploy', "Deploy to " . $environment->Name)->addExtraClass('btn btn-primary')->setAttribute('onclick', "return confirm('This will start a direct deployment.\\n\\nContinue?');")); } parent::__construct($controller, $name, new FieldList($field), $actions, $validator); }
/** * Use snowcake to do the deployment */ public function deploy(DNEnvironment $environment, $sha, DeploynautLogFile $log, DNProject $project, $leaveMaintenancePage = false) { $log->write(sprintf('Deploying "%s" to "%s"', $sha, $environment->getFullName())); if (!defined('SNOWCAKE_PATH')) { $log->write('SNOWCAKE_PATH is not defined'); throw new RuntimeException('SNOWCAKE_PATH is not defined'); } // Construct our snowcake command $name = $environment->SnowcakeName . '-' . substr($sha, 0, 8) . '-' . mt_rand(); // Filter invalid characters out of $name (Value 'ssorg_uat-fdceda2e-1400725889-bake' at 'stackName' failed to satisfy constraint: // "Member must satisfy regular expression pattern: [a-zA-Z][-a-zA-Z0-9]*)" $name = str_replace('_', '-', $name); $command = sprintf('%s deploy %s %s %s', SNOWCAKE_PATH, $environment->SnowcakeName, $name, $sha); $log->write(sprintf('Running command: %s', $command)); $process = new Process($command, dirname(dirname(SNOWCAKE_PATH))); $process->setTimeout(3600); $process->run(function ($type, $buffer) use($log) { $log->write($buffer); }); if (!$process->isSuccessful()) { throw new RuntimeException($process->getErrorOutput()); } $log->write(sprintf('Deploy of "%s" to "%s" finished', $sha, $environment->getFullName())); }
/** * @param SS_HTTPRequest $request * * @return string */ public function show(\SS_HTTPRequest $request) { $targetEnvironment = null; $targetEnvironmentId = $request->getVar('environmentId'); if (!empty($targetEnvironmentId)) { $targetEnvironment = DNEnvironment::get()->byId((int) $targetEnvironmentId); } $refs = []; $prevDeploys = []; $uatEnvironment = $this->project->DNEnvironmentList()->filter('Usage', DNEnvironment::UAT)->first(); $uatBuild = $uatEnvironment ? $uatEnvironment->CurrentBuild() : null; if ($uatBuild && $uatBuild->exists() && $targetEnvironment && $targetEnvironment->Usage === DNEnvironment::PRODUCTION) { $refs[self::REF_TYPE_FROM_UAT] = ['id' => self::REF_TYPE_FROM_UAT, 'label' => 'Promote the version currently on UAT', 'description' => 'Promote the version currently on UAT', 'promote_build' => $this->formatter->getDeploymentData($uatBuild)]; } $refs[self::REF_TYPE_BRANCH] = ['id' => self::REF_TYPE_BRANCH, 'label' => 'Branch version', 'description' => 'Deploy the latest version of a branch', 'list' => $this->getGitBranches($this->project)]; $refs[self::REF_TYPE_TAG] = ['id' => self::REF_TYPE_TAG, 'label' => 'Tag version', 'description' => 'Deploy a tagged release', 'list' => $this->getGitTags($this->project)]; // @todo: the original was a tree that was keyed by environment, the // front-end dropdown needs to be changed to support that. brrrr. foreach ($this->getGitPrevDeploys($this->project) as $env) { foreach ($env as $deploy) { $prevDeploys[] = $deploy; } } $refs[self::REF_TYPE_PREVIOUS] = ['id' => self::REF_TYPE_PREVIOUS, 'label' => 'Redeploy a release that was previously deployed (to any environment)', 'description' => 'Deploy a previous release', 'list' => $prevDeploys]; $refs[self::REF_TYPE_SHA] = ['id' => self::REF_TYPE_SHA, 'label' => 'Deploy a specific SHA', 'description' => 'Deploy a specific SHA']; $options = []; if ($targetEnvironment) { foreach ($targetEnvironment->getSupportedOptions() as $option) { $options[] = ['name' => $option->getName(), 'title' => $option->getTitle(), 'defaultValue' => $option->getDefaultValue()]; } } // get the last time git fetch was run $lastFetchedDate = 'never'; $lastFetchedAgo = null; $fetch = DNGitFetch::get()->filter(['ProjectID' => $this->project->ID, 'Status' => 'Finished'])->sort('LastEdited', 'DESC')->first(); if ($fetch) { $lastFetchedDate = $fetch->obj('LastEdited')->Date(); $lastFetchedAgo = $fetch->obj('LastEdited')->Ago(); } return $this->getAPIResponse(['refs' => $refs, 'options' => $options, 'last_fetched_date' => $lastFetchedDate, 'last_fetched_ago' => $lastFetchedAgo], 200); }
/** * @var array * @return \DeploymentStrategy */ protected function createStrategy($options) { $strategy = $this->environment->Backend()->planDeploy($this->environment, $options); $data = $strategy->toArray(); $interface = $this->project->getRepositoryInterface(); if ($interface instanceof \ArrayData && $this->canCompareCodeVersions($interface, $data['changes'])) { $compareurl = sprintf('%s/compare/%s...%s', $interface->URL, $data['changes']['Code version']['from'], $data['changes']['Code version']['to']); $data['changes']['Code version']['compareUrl'] = $compareurl; // special case for .platform.yml field so we don't show a huge blob of changes, // but rather a link to where the .platform.yml changes were made in the code if (isset($data['changes']['.platform.yml other'])) { $data['changes']['.platform.yml other']['compareUrl'] = $compareurl; $data['changes']['.platform.yml other']['description'] = ''; } } $this->extend('updateDeploySummary', $data); // Ensure changes that would have been updated are persisted in the object, // such as the comparison URL, so that they will be written to the Strategy // field on the DNDeployment object as part of {@link createDeployment()} $strategy->setChanges($data['changes']); return $strategy; }
/** * Sync the in-db project list with a list of file paths * @param array $paths Array of pathnames * @param boolean $remove Should obsolete environments be removed? */ public function syncWithPaths($paths, $dryRun = false) { // Normalise paths in DB foreach ($this as $item) { $real = realpath($item->Filename); if ($real && $real != $item->Filename) { $item->Filename = $real; $item->write(); } } foreach ($paths as $path) { $path = basename($path); if ($this->filter('Filename', $path)->count()) { continue; } $this->message('Adding "' . basename($path) . '" to db'); if (!$dryRun) { $environment = DNEnvironment::create_from_path($path); $environment->ProjectID = $this->projectID; $environment->write(); } } }
/** * Check if this member can move archive into the environment. * * @param DNEnvironment $targetEnv Environment to check. * @param Member|null $member The {@link Member} object to test against. If null, uses Member::currentMember(); * * @return boolean true if $member can upload archives linked to this environment, false if they can't. */ public function canMoveTo($targetEnv, $member = null) { if ($this->Environment()->Project()->ID != $targetEnv->Project()->ID) { // We don't permit moving snapshots between projects at this stage. return false; } if (!$member) { $member = Member::currentUser(); } // Must be logged in to check permissions if (!$member) { return false; } // Admin can always move. if (Permission::checkMember($member, 'ADMIN')) { return true; } // Checks if the user can actually access the archive. if (!$this->canDownload($member)) { return false; } // Hooks into ArchiveUploaders permission to prevent proliferation of permission checkboxes. // Bypasses the quota check - we don't need to check for it as long as we move the snapshot within the project. return $targetEnv->ArchiveUploaders()->byID($member->ID) || $member->inGroups($targetEnv->ArchiveUploaderGroups()); }
/** * Utility function for triggering the db rebuild and flush. * Also cleans up and generates new error pages. * @param DeploynautLogFile $log */ public function rebuild(DNEnvironment $environment, $log) { $name = $environment->getFullName(); $command = $this->getCommand('deploy:migrate', 'web', $environment, null, $log); $command->run(function ($type, $buffer) use($log) { $log->write($buffer); }); if (!$command->isSuccessful()) { $log->write(sprintf('Rebuild of "%s" failed: %s', $name, $command->getErrorOutput())); throw new RuntimeException($command->getErrorOutput()); } $log->write(sprintf('Rebuild of "%s" done', $name)); }
/** * This is mostly copy-pasted from Anthill/Smoketest. * * @param \DNEnvironment $environment * @param \DeploynautLogFile $log * @return bool */ protected function smokeTest(\DNEnvironment $environment, \DeploynautLogFile $log) { $url = $environment->getBareURL(); $timeout = 600; $tick = 60; if (!$url) { $log->write('Skipping site accessible check: no URL found.'); return true; } $start = time(); $infoTick = time() + $tick; $log->write(sprintf('Waiting for "%s" to become accessible... (timeout: %smin)', $url, $timeout / 60)); // configure curl so that curl_exec doesn't wait a long time for a response $ch = curl_init(); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5); curl_setopt($ch, CURLOPT_TIMEOUT, 5); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); curl_setopt($ch, CURLOPT_MAXREDIRS, 10); // set a high number of max redirects (but not infinite amount) to avoid a potential infinite loop curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_USERAGENT, 'Rainforest'); $success = false; // query the site every second. Note that if the URL doesn't respond, // curl_exec will take 5 seconds to timeout (see CURLOPT_CONNECTTIMEOUT and CURLOPT_TIMEOUT above) do { if (time() > $start + $timeout) { $log->write(sprintf(' * Failed: check for %s timed out after %smin', $url, $timeout / 60)); return false; } $response = curl_exec($ch); // check the HTTP response code for HTTP protocols $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); if ($status && !in_array($status, [500, 501, 502, 503, 504])) { $success = true; } // check for any curl errors, mostly for checking the response state of non-HTTP protocols, // but applies to checks of any protocol if ($response && !curl_errno($ch)) { $success = true; } // Produce an informational ticker roughly every $tick if (time() > $infoTick) { $message = []; // Collect status information from different sources. if ($status) { $message[] = sprintf('HTTP status code is %s', $status); } if (!$response) { $message[] = 'response is empty'; } if ($error = curl_error($ch)) { $message[] = sprintf('request error: %s', $error); } $log->write(sprintf(' * Still waiting: %s...', implode(', ', $message))); $infoTick = time() + $tick; } sleep(1); } while (!$success); curl_close($ch); $log->write(' * Success: site is accessible!'); return true; }
/** * Provides a DNEnvironmentList of environments found in this project. */ public function DNEnvironmentList() { return DNEnvironment::get()->filter('ProjectID', $this->ID)->setProjectID($this->ID); }
/** * Validate a specific alert configuration from configuration YAML is correct. * * @param string $name * @param array $config * @param DNProject $project * @param DeploynautLogFile $log * @return boolean */ public function validateAlert($name, $config, $project, $log) { // validate we have an environment set for the alert if (!isset($config['environment'])) { $log->write(sprintf('WARNING: Failed to configure alert "%s". Missing "environment" key in .alerts.yml. Skipped.', $name)); return false; } // validate we have an environmentcheck suite name to check if (!isset($config['check_url'])) { $log->write(sprintf('WARNING: Failed to configure alert "%s". Missing "check_url" key in .alerts.yml. Skipped.', $name)); return false; } // validate we have contacts for the alert if (!isset($config['contacts'])) { $log->write(sprintf('WARNING: Failed to configure alert "%s". Missing "contacts" key in .alerts.yml. Skipped.', $name)); return false; } // validate that each value in the config is valid, build up a list of contacts we'll use later foreach ($config['contacts'] as $contactEmail) { // special case for ops if ($contactEmail == 'ops') { continue; } $contact = $project->AlertContacts()->filter('Email', $contactEmail)->first(); if (!($contact && $contact->exists())) { $log->write(sprintf('WARNING: Failed to configure alert "%s". No such contact "%s". Skipped.', $name, $contactEmail)); return false; } } // validate the environment specified in the alert actually exists if (!DNEnvironment::get()->filter('Name', $config['environment'])->first()) { $log->write(sprintf('WARNING: Failed to configure alert "%s". Invalid environment "%s" in .alerts.yml. Skipped.', $name, $config['environment'])); return false; } return true; }
/** * @param string $action * @return string */ public function Link($action = '') { return \Controller::join_links($this->environment->Link(), self::ACTION_APPROVALS, $action); }
/** * Return a dependent {@link DNEnvironment} based on this pipeline's dependent environment configuration. * @return DNEnvironment */ public function getDependentEnvironment() { // dependent environment not available $projectName = $this->getConfigSetting('PipelineConfig', 'DependsOnProject'); $environmentName = $this->getConfigSetting('PipelineConfig', 'DependsOnEnvironment'); if (empty($projectName) || empty($environmentName)) { return null; } $project = DNProject::get()->filter('Name', $projectName)->first(); if (!($project && $project->exists())) { throw new Exception(sprintf('Could not find dependent project "%s"', $projectName)); } $environment = DNEnvironment::get()->filter(array('ProjectID' => $project->ID, 'Name' => $environmentName))->first(); if (!($environment && $environment->exists())) { throw new Exception(sprintf('Could not find dependent environment "%s" in project "%s"', $environmentName, $projectName)); } return $environment; }
/** * Used by the sync task * * @param string $path * @return \DNEnvironment */ public static function create_from_path($path) { $e = new DNEnvironment(); $e->Filename = $path; $e->Name = basename($e->Filename, '.rb'); // add each administrator member as a deployer of the new environment $adminGroup = Group::get()->filter('Code', 'administrators')->first(); if ($adminGroup && $adminGroup->exists()) { foreach ($adminGroup->Members() as $member) { $e->Deployers()->add($member); } } return $e; }
public function onAfterWrite() { parent::onAfterWrite(); if ($this->Usage == 'Production' || $this->Usage == 'UAT') { $conflicting = DNEnvironment::get()->filter('ProjectID', $this->ProjectID)->filter('Usage', $this->Usage)->exclude('ID', $this->ID); foreach ($conflicting as $otherEnvironment) { $otherEnvironment->Usage = 'Unspecified'; $otherEnvironment->write(); } } }
/** * Used by the sync task * * @param string $path * @return \DNEnvironment */ public static function create_from_path($path) { $e = DNEnvironment::create(); $e->Filename = $path; $e->Name = basename($e->Filename, '.rb'); // add each administrator member as a deployer of the new environment $adminGroup = Group::get()->filter('Code', 'administrators')->first(); $e->DeployerGroups()->add($adminGroup); return $e; }
public function ping(\DNEnvironment $environment, DeploynautLogFile $log, DNProject $project) { $log->write(sprintf('Ping "%s"', $environment->getFullName())); }
public function run($request) { $args = $request->getVar('args'); $dryRun = $args && in_array('--dry-run', $args); $log = function ($message) { $message = sprintf('[%s] ', date('Y-m-d H:i:s')) . $message; echo $message . PHP_EOL; }; if (!Director::is_cli()) { $log('This task must be run under the command line'); return; } if ($dryRun) { $log('Running in dry-run mode. No data will be deleted'); } $count = 0; foreach (DNEnvironment::get() as $environment) { $project = $environment->Project(); if (!$project || !$project->exists()) { $log(sprintf('Environment (ID %s, Name: %s, Created: %s) is linked to a non-existent project. Deleting', $environment->ID, $environment->Name, $environment->Created)); if (!$dryRun) { $environment->delete(); $environment->destroy(); } $count++; } } foreach (DNDeployment::get() as $deployment) { $environment = $deployment->Environment(); if (!$environment || !$environment->exists()) { $log(sprintf('Deployment (ID %s, Created: %s) is linked to a non-existent environment. Deleting', $deployment->ID, $deployment->Created)); if (!$dryRun) { $deployment->delete(); $deployment->destroy(); } $count++; } } foreach (DNDataTransfer::get() as $transfer) { $environment = $transfer->Environment(); if (!$environment || !$environment->exists()) { $log(sprintf('Data transfer (ID %s, Created: %s) is linked to a non-existent environment. Deleting', $transfer->ID, $transfer->Created)); if (!$dryRun) { $transfer->delete(); $transfer->destroy(); } $count++; } } foreach (DNDataArchive::get() as $archive) { $environment = $archive->Environment(); if (!$environment || !$environment->exists()) { $log(sprintf('Archive (ID %s, Created: %s) is linked to a non-existent environment. Deleting', $archive->ID, $archive->Created)); if (!$dryRun) { $archive->delete(); $archive->destroy(); } $count++; } } foreach (DNGitFetch::get() as $fetch) { $project = $fetch->Project(); if (!$project || !$project->exists()) { $log(sprintf('Git fetch (ID %s, Created: %s) is linked to a non-existent project. Deleting', $fetch->ID, $fetch->Created)); if (!$dryRun) { $fetch->delete(); $fetch->destroy(); } $count++; } } foreach (DNPing::get() as $ping) { $environment = $ping->Environment(); if (!$environment || !$environment->exists()) { $log(sprintf('Ping (ID %s, Created: %s) is linked to a non-existent environment. Deleting', $ping->ID, $ping->Created)); if (!$dryRun) { $ping->delete(); $ping->destroy(); } $count++; } } $log(sprintf('Finished. Processed %s records', $count)); }
/** * */ public function testGetConfigFilename() { $expected = $this->envPath . '/testproject/uat.rb'; $this->assertEquals($expected, $this->env->getConfigFilename()); }
public function onAfterWrite() { parent::onAfterWrite(); if ($this->Usage === self::PRODUCTION || $this->Usage === self::UAT) { $conflicting = DNEnvironment::get()->filter('ProjectID', $this->ProjectID)->filter('Usage', $this->Usage)->exclude('ID', $this->ID); foreach ($conflicting as $otherEnvironment) { $otherEnvironment->Usage = self::UNSPECIFIED; $otherEnvironment->write(); } } }